commit 49b3cea942912828719c46c2a04817990ef99766 Author: Choi A.K. Date: Sat Aug 30 10:33:46 2025 +0900 init commit diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..80703b2 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,59 @@ +# Виртуальное окружение +.venv/ +venv/ +env/ +.env + +# Кэш Python +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg + +# Логи +logs/ +*.log + +# Локальные настройки и данные +.env.local +*.db +*.sqlite3 + +# Git и GitHub файлы +.git/ +.github/ +.gitignore +.gitattributes + +# IDE файлы +.idea/ +.vscode/ +*.swp +*.swo + +# История и временные файлы +.history/ +*.tmp +*.bak + +# Файлы Windows +Thumbs.db +ehthumbs.db +Desktop.ini +$RECYCLE.BIN/ diff --git a/.env-example b/.env-example new file mode 100644 index 0000000..07edb8a --- /dev/null +++ b/.env-example @@ -0,0 +1,26 @@ +# Telegram Bot API +TELEGRAM_TOKEN=your_telegram_bot_token +ADMIN_USER_IDS=123456789,987654321 # ID пользователей-администраторов через запятую + +# Synology NAS +SYNOLOGY_HOST=192.168.1.100 +SYNOLOGY_PORT=5000 # Обычно 5000 для HTTP и 5001 для HTTPS +SYNOLOGY_USERNAME=your_username +SYNOLOGY_PASSWORD=your_password +SYNOLOGY_SECURE=True # Использовать HTTPS +SYNOLOGY_VERIFY_SSL=False # Проверка SSL-сертификата +SYNOLOGY_TIMEOUT=10 # Таймаут для API запросов в секундах +SYNOLOGY_API_VERSION=1 # Версия API +SYNOLOGY_POWER_API=SYNO.Core.System # API для управления питанием + +# WOL (Wake-on-LAN) +MAC_ADDRESS=00:11:22:33:44:55 # MAC-адрес Synology NAS +WOL_BROADCAST=255.255.255.255 # Broadcast-адрес для WOL +WOL_PORT=9 # Порт для WOL (обычно 7 или 9) + +# Logging +LOG_LEVEL=INFO + +# Docker specific +DOCKER_ENV=true # Указывает, что приложение запущено в Docker +HEALTHCHECK_PORT=8080 # Порт для healthcheck diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0a0a319 --- /dev/null +++ b/.gitignore @@ -0,0 +1,44 @@ +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Logs +logs/ +*.log + +# IDE specific files +.idea/ +.vscode/ +*.swp +*.swo + +# OS specific files +.DS_Store +Thumbs.db diff --git a/.history/.dockerignore_20250830102713 b/.history/.dockerignore_20250830102713 new file mode 100644 index 0000000..33bbec7 --- /dev/null +++ b/.history/.dockerignore_20250830102713 @@ -0,0 +1,58 @@ +# Виртуальное окружение +.venv/ +venv/ +env/ + +# Кэш Python +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg + +# Логи +logs/ +*.log + +# Локальные настройки и данные +.env.local +*.db +*.sqlite3 + +# Git и GitHub файлы +.git/ +.github/ +.gitignore +.gitattributes + +# IDE файлы +.idea/ +.vscode/ +*.swp +*.swo + +# История и временные файлы +.history/ +*.tmp +*.bak + +# Файлы Windows +Thumbs.db +ehthumbs.db +Desktop.ini +$RECYCLE.BIN/ diff --git a/.history/.dockerignore_20250830103154 b/.history/.dockerignore_20250830103154 new file mode 100644 index 0000000..33bbec7 --- /dev/null +++ b/.history/.dockerignore_20250830103154 @@ -0,0 +1,58 @@ +# Виртуальное окружение +.venv/ +venv/ +env/ + +# Кэш Python +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg + +# Логи +logs/ +*.log + +# Локальные настройки и данные +.env.local +*.db +*.sqlite3 + +# Git и GitHub файлы +.git/ +.github/ +.gitignore +.gitattributes + +# IDE файлы +.idea/ +.vscode/ +*.swp +*.swo + +# История и временные файлы +.history/ +*.tmp +*.bak + +# Файлы Windows +Thumbs.db +ehthumbs.db +Desktop.ini +$RECYCLE.BIN/ diff --git a/.history/.dockerignore_20250830103255 b/.history/.dockerignore_20250830103255 new file mode 100644 index 0000000..80703b2 --- /dev/null +++ b/.history/.dockerignore_20250830103255 @@ -0,0 +1,59 @@ +# Виртуальное окружение +.venv/ +venv/ +env/ +.env + +# Кэш Python +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg + +# Логи +logs/ +*.log + +# Локальные настройки и данные +.env.local +*.db +*.sqlite3 + +# Git и GitHub файлы +.git/ +.github/ +.gitignore +.gitattributes + +# IDE файлы +.idea/ +.vscode/ +*.swp +*.swo + +# История и временные файлы +.history/ +*.tmp +*.bak + +# Файлы Windows +Thumbs.db +ehthumbs.db +Desktop.ini +$RECYCLE.BIN/ diff --git a/.history/.env-example_20250830103005 b/.history/.env-example_20250830103005 new file mode 100644 index 0000000..07edb8a --- /dev/null +++ b/.history/.env-example_20250830103005 @@ -0,0 +1,26 @@ +# Telegram Bot API +TELEGRAM_TOKEN=your_telegram_bot_token +ADMIN_USER_IDS=123456789,987654321 # ID пользователей-администраторов через запятую + +# Synology NAS +SYNOLOGY_HOST=192.168.1.100 +SYNOLOGY_PORT=5000 # Обычно 5000 для HTTP и 5001 для HTTPS +SYNOLOGY_USERNAME=your_username +SYNOLOGY_PASSWORD=your_password +SYNOLOGY_SECURE=True # Использовать HTTPS +SYNOLOGY_VERIFY_SSL=False # Проверка SSL-сертификата +SYNOLOGY_TIMEOUT=10 # Таймаут для API запросов в секундах +SYNOLOGY_API_VERSION=1 # Версия API +SYNOLOGY_POWER_API=SYNO.Core.System # API для управления питанием + +# WOL (Wake-on-LAN) +MAC_ADDRESS=00:11:22:33:44:55 # MAC-адрес Synology NAS +WOL_BROADCAST=255.255.255.255 # Broadcast-адрес для WOL +WOL_PORT=9 # Порт для WOL (обычно 7 или 9) + +# Logging +LOG_LEVEL=INFO + +# Docker specific +DOCKER_ENV=true # Указывает, что приложение запущено в Docker +HEALTHCHECK_PORT=8080 # Порт для healthcheck diff --git a/.history/.env-example_20250830103154 b/.history/.env-example_20250830103154 new file mode 100644 index 0000000..07edb8a --- /dev/null +++ b/.history/.env-example_20250830103154 @@ -0,0 +1,26 @@ +# Telegram Bot API +TELEGRAM_TOKEN=your_telegram_bot_token +ADMIN_USER_IDS=123456789,987654321 # ID пользователей-администраторов через запятую + +# Synology NAS +SYNOLOGY_HOST=192.168.1.100 +SYNOLOGY_PORT=5000 # Обычно 5000 для HTTP и 5001 для HTTPS +SYNOLOGY_USERNAME=your_username +SYNOLOGY_PASSWORD=your_password +SYNOLOGY_SECURE=True # Использовать HTTPS +SYNOLOGY_VERIFY_SSL=False # Проверка SSL-сертификата +SYNOLOGY_TIMEOUT=10 # Таймаут для API запросов в секундах +SYNOLOGY_API_VERSION=1 # Версия API +SYNOLOGY_POWER_API=SYNO.Core.System # API для управления питанием + +# WOL (Wake-on-LAN) +MAC_ADDRESS=00:11:22:33:44:55 # MAC-адрес Synology NAS +WOL_BROADCAST=255.255.255.255 # Broadcast-адрес для WOL +WOL_PORT=9 # Порт для WOL (обычно 7 или 9) + +# Logging +LOG_LEVEL=INFO + +# Docker specific +DOCKER_ENV=true # Указывает, что приложение запущено в Docker +HEALTHCHECK_PORT=8080 # Порт для healthcheck diff --git a/.history/.env_20250830063713 b/.history/.env_20250830063713 new file mode 100644 index 0000000..890528b --- /dev/null +++ b/.history/.env_20250830063713 @@ -0,0 +1,15 @@ +# Telegram Bot Configuration +TELEGRAM_TOKEN=your_telegram_bot_token +ADMIN_USER_IDS=123456789,987654321 + +# Synology NAS Configuration +SYNOLOGY_HOST=192.168.1.100 +SYNOLOGY_PORT=5000 +SYNOLOGY_USERNAME=admin +SYNOLOGY_PASSWORD=your_password +SYNOLOGY_SECURE=False +SYNOLOGY_TIMEOUT=10 + +# Wake-on-LAN Configuration +SYNOLOGY_MAC=00:11:22:33:44:55 +WOL_PORT=9 diff --git a/.history/.env_20250830063839 b/.history/.env_20250830063839 new file mode 100644 index 0000000..890528b --- /dev/null +++ b/.history/.env_20250830063839 @@ -0,0 +1,15 @@ +# Telegram Bot Configuration +TELEGRAM_TOKEN=your_telegram_bot_token +ADMIN_USER_IDS=123456789,987654321 + +# Synology NAS Configuration +SYNOLOGY_HOST=192.168.1.100 +SYNOLOGY_PORT=5000 +SYNOLOGY_USERNAME=admin +SYNOLOGY_PASSWORD=your_password +SYNOLOGY_SECURE=False +SYNOLOGY_TIMEOUT=10 + +# Wake-on-LAN Configuration +SYNOLOGY_MAC=00:11:22:33:44:55 +WOL_PORT=9 diff --git a/.history/.env_20250830071119 b/.history/.env_20250830071119 new file mode 100644 index 0000000..7c53ffa --- /dev/null +++ b/.history/.env_20250830071119 @@ -0,0 +1,15 @@ +# Telegram Bot Configuration +TELEGRAM_TOKEN=your_telegram_bot_token +ADMIN_USER_IDS=123456789,987654321 + +# Synology NAS Configuration +SYNOLOGY_HOST=192.168.0. +SYNOLOGY_PORT=5000 +SYNOLOGY_USERNAME=admin +SYNOLOGY_PASSWORD=your_password +SYNOLOGY_SECURE=False +SYNOLOGY_TIMEOUT=10 + +# Wake-on-LAN Configuration +SYNOLOGY_MAC=00:11:22:33:44:55 +WOL_PORT=9 diff --git a/.history/.env_20250830071139 b/.history/.env_20250830071139 new file mode 100644 index 0000000..66be270 --- /dev/null +++ b/.history/.env_20250830071139 @@ -0,0 +1,15 @@ +# Telegram Bot Configuration +TELEGRAM_TOKEN=your_telegram_bot_token +ADMIN_USER_IDS=123456789,987654321 + +# Synology NAS Configuration +SYNOLOGY_HOST=192.168.0.102 +SYNOLOGY_PORT=5000 +SYNOLOGY_USERNAME=superadmin +SYNOLOGY_PASSWORD=Cl0ud_1985! +SYNOLOGY_SECURE=False +SYNOLOGY_TIMEOUT=10 + +# Wake-on-LAN Configuration +SYNOLOGY_MAC=00:11:22:33:44:55 +WOL_PORT=9 diff --git a/.history/.env_20250830071153 b/.history/.env_20250830071153 new file mode 100644 index 0000000..c784fd2 --- /dev/null +++ b/.history/.env_20250830071153 @@ -0,0 +1,15 @@ +# Telegram Bot Configuration +TELEGRAM_TOKEN=your_telegram_bot_token +ADMIN_USER_IDS=123456789,987654321 + +# Synology NAS Configuration +SYNOLOGY_HOST=192.168.0.102 +SYNOLOGY_PORT=5000 +SYNOLOGY_USERNAME=superadmin +SYNOLOGY_PASSWORD=Cl0ud_1985! +SYNOLOGY_SECURE=False +SYNOLOGY_TIMEOUT=10 + +# Wake-on-LAN Configuration +SYNOLOGY_MAC=90:09:D0:8C:27:F9 +WOL_PORT=9 diff --git a/.history/.env_20250830071249 b/.history/.env_20250830071249 new file mode 100644 index 0000000..a12eb00 --- /dev/null +++ b/.history/.env_20250830071249 @@ -0,0 +1,15 @@ +# Telegram Bot Configuration +TELEGRAM_TOKEN=879254890:AAGOVgN6yF9Xx0PXlkTVncln8RJkh3BL1AY +ADMIN_USER_IDS=123456789,987654321 + +# Synology NAS Configuration +SYNOLOGY_HOST=192.168.0.102 +SYNOLOGY_PORT=5000 +SYNOLOGY_USERNAME=superadmin +SYNOLOGY_PASSWORD=Cl0ud_1985! +SYNOLOGY_SECURE=False +SYNOLOGY_TIMEOUT=10 + +# Wake-on-LAN Configuration +SYNOLOGY_MAC=90:09:D0:8C:27:F9 +WOL_PORT=9 diff --git a/.history/.env_20250830071300 b/.history/.env_20250830071300 new file mode 100644 index 0000000..898235d --- /dev/null +++ b/.history/.env_20250830071300 @@ -0,0 +1,15 @@ +# Telegram Bot Configuration +TELEGRAM_TOKEN=879254890:AAGOVgN6yF9Xx0PXlkTVncln8RJkh3BL1AY +ADMIN_USER_IDS=556399210 + +# Synology NAS Configuration +SYNOLOGY_HOST=192.168.0.102 +SYNOLOGY_PORT=5000 +SYNOLOGY_USERNAME=superadmin +SYNOLOGY_PASSWORD=Cl0ud_1985! +SYNOLOGY_SECURE=False +SYNOLOGY_TIMEOUT=10 + +# Wake-on-LAN Configuration +SYNOLOGY_MAC=90:09:D0:8C:27:F9 +WOL_PORT=9 diff --git a/.history/.env_20250830072439.example b/.history/.env_20250830072439.example new file mode 100644 index 0000000..bc8cf69 --- /dev/null +++ b/.history/.env_20250830072439.example @@ -0,0 +1,16 @@ +# Telegram Bot Configuration +TELEGRAM_TOKEN=your_telegram_bot_token +ADMIN_USER_IDS=123456789,987654321 + +# Synology NAS Configuration +SYNOLOGY_HOST=192.168.1.100 +SYNOLOGY_PORT=5000 +SYNOLOGY_USERNAME=admin +SYNOLOGY_PASSWORD=your_password +SYNOLOGY_SECURE=False +SYNOLOGY_TIMEOUT=10 +SYNOLOGY_API_VERSION=1 + +# Wake-on-LAN Configuration +SYNOLOGY_MAC=00:11:22:33:44:55 +WOL_PORT=9 diff --git a/.history/.env_20250830072817.example b/.history/.env_20250830072817.example new file mode 100644 index 0000000..bc8cf69 --- /dev/null +++ b/.history/.env_20250830072817.example @@ -0,0 +1,16 @@ +# Telegram Bot Configuration +TELEGRAM_TOKEN=your_telegram_bot_token +ADMIN_USER_IDS=123456789,987654321 + +# Synology NAS Configuration +SYNOLOGY_HOST=192.168.1.100 +SYNOLOGY_PORT=5000 +SYNOLOGY_USERNAME=admin +SYNOLOGY_PASSWORD=your_password +SYNOLOGY_SECURE=False +SYNOLOGY_TIMEOUT=10 +SYNOLOGY_API_VERSION=1 + +# Wake-on-LAN Configuration +SYNOLOGY_MAC=00:11:22:33:44:55 +WOL_PORT=9 diff --git a/.history/.env_20250830080745 b/.history/.env_20250830080745 new file mode 100644 index 0000000..aab416d --- /dev/null +++ b/.history/.env_20250830080745 @@ -0,0 +1,16 @@ +# Telegram Bot Configuration +TELEGRAM_TOKEN=879254890:AAGOVgN6yF9Xx0PXlkTVncln8RJkh3BL1AY +ADMIN_USER_IDS=556399210 + +# Synology NAS Configuration +SYNOLOGY_HOST=192.168.0.102 +SYNOLOGY_PORT=5000 +SYNOLOGY_USERNAME=superadmin +SYNOLOGY_PASSWORD=Cl0ud_1985! +SYNOLOGY_SECURE=False +SYNOLOGY_TIMEOUT=10 +SYNOLOGY_API_VERSION=1 + +# Wake-on-LAN Configuration +SYNOLOGY_MAC=90:09:D0:8C:27:F9 +WOL_PORT=9 diff --git a/.history/.env_20250830082200 b/.history/.env_20250830082200 new file mode 100644 index 0000000..828e005 --- /dev/null +++ b/.history/.env_20250830082200 @@ -0,0 +1,16 @@ +# Telegram Bot Configuration +TELEGRAM_TOKEN=879254890:AAGOVgN6yF9Xx0PXlkTVncln8RJkh3BL1AY +ADMIN_USER_IDS=556399210 + +# Synology NAS Configuration +SYNOLOGY_HOST=192.168.0.102 +SYNOLOGY_PORT=5000 +SYNOLOGY_USERNAME=superadmin +SYNOLOGY_PASSWORD=Cl0ud_1985! +SYNOLOGY_SECURE=False +SYNOLOGY_TIMEOUT=10 +SYNOLOGY_API_VERSION=2 + +# Wake-on-LAN Configuration +SYNOLOGY_MAC=90:09:D0:8C:27:F9 +WOL_PORT=9 diff --git a/.history/.env_20250830082210 b/.history/.env_20250830082210 new file mode 100644 index 0000000..af5bd96 --- /dev/null +++ b/.history/.env_20250830082210 @@ -0,0 +1,20 @@ +# Telegram Bot Configuration +TELEGRAM_TOKEN=879254890:AAGOVgN6yF9Xx0PXlkTVncln8RJkh3BL1AY +ADMIN_USER_IDS=556399210 + +# Synology NAS Configuration +SYNOLOGY_HOST=192.168.0.102 +SYNOLOGY_PORT=5000 +SYNOLOGY_USERNAME=superadmin +SYNOLOGY_PASSWORD=Cl0ud_1985! +SYNOLOGY_SECURE=False +SYNOLOGY_TIMEOUT=10 +SYNOLOGY_API_VERSION=2 + +# API Configuration +SYNOLOGY_POWER_API=SYNO.Core.Hardware.PowerRecovery +SYNOLOGY_INFO_API=SYNO.DSM.Info + +# Wake-on-LAN Configuration +SYNOLOGY_MAC=90:09:D0:8C:27:F9 +WOL_PORT=9 diff --git a/.history/.env_20250830082500 b/.history/.env_20250830082500 new file mode 100644 index 0000000..af5bd96 --- /dev/null +++ b/.history/.env_20250830082500 @@ -0,0 +1,20 @@ +# Telegram Bot Configuration +TELEGRAM_TOKEN=879254890:AAGOVgN6yF9Xx0PXlkTVncln8RJkh3BL1AY +ADMIN_USER_IDS=556399210 + +# Synology NAS Configuration +SYNOLOGY_HOST=192.168.0.102 +SYNOLOGY_PORT=5000 +SYNOLOGY_USERNAME=superadmin +SYNOLOGY_PASSWORD=Cl0ud_1985! +SYNOLOGY_SECURE=False +SYNOLOGY_TIMEOUT=10 +SYNOLOGY_API_VERSION=2 + +# API Configuration +SYNOLOGY_POWER_API=SYNO.Core.Hardware.PowerRecovery +SYNOLOGY_INFO_API=SYNO.DSM.Info + +# Wake-on-LAN Configuration +SYNOLOGY_MAC=90:09:D0:8C:27:F9 +WOL_PORT=9 diff --git a/.history/.env_20250830083237 b/.history/.env_20250830083237 new file mode 100644 index 0000000..b67e336 --- /dev/null +++ b/.history/.env_20250830083237 @@ -0,0 +1,20 @@ +# Telegram Bot Configuration +TELEGRAM_TOKEN=879254890:AAGOVgN6yF9Xx0PXlkTVncln8RJkh3BL1AY +ADMIN_USER_IDS=556399210 + +# Synology NAS Configuration +SYNOLOGY_HOST=192.168.0.102 +SYNOLOGY_PORT=5000 +SYNOLOGY_USERNAME=superadmin +SYNOLOGY_PASSWORD=Cl0ud_1985! +SYNOLOGY_SECURE=False +SYNOLOGY_TIMEOUT=10 +SYNOLOGY_API_VERSION=6 + +# API Configuration +SYNOLOGY_POWER_API=SYNO.Core.Hardware.PowerRecovery +SYNOLOGY_INFO_API=SYNO.DSM.Info + +# Wake-on-LAN Configuration +SYNOLOGY_MAC=90:09:D0:8C:27:F9 +WOL_PORT=9 diff --git a/.history/.env_20250830101005 b/.history/.env_20250830101005 new file mode 100644 index 0000000..82e6f2a --- /dev/null +++ b/.history/.env_20250830101005 @@ -0,0 +1,20 @@ +# Telegram Bot Configuration +TELEGRAM_TOKEN=879254890:AAGOVgN6yF9Xx0PXlkTVncln8RJkh3BL1AY +ADMIN_USER_IDS=556399210 + +# Synology NAS Configuration +SYNOLOGY_HOST=192.168.0.102 +SYNOLOGY_PORT=5000 +SYNOLOGY_USERNAME=superadmin +SYNOLOGY_PASSWORD=Cl0ud_1985! +SYNOLOGY_SECURE=False +SYNOLOGY_TIMEOUT=10 +SYNOLOGY_API_VERSION=6 + +# API Configuration +SYNOLOGY_POWER_API=SYNO.Core.System +SYNOLOGY_INFO_API=SYNO.DSM.Info + +# Wake-on-LAN Configuration +SYNOLOGY_MAC=90:09:D0:8C:27:F9 +WOL_PORT=9 diff --git a/.history/.env_20250830101843 b/.history/.env_20250830101843 new file mode 100644 index 0000000..82e6f2a --- /dev/null +++ b/.history/.env_20250830101843 @@ -0,0 +1,20 @@ +# Telegram Bot Configuration +TELEGRAM_TOKEN=879254890:AAGOVgN6yF9Xx0PXlkTVncln8RJkh3BL1AY +ADMIN_USER_IDS=556399210 + +# Synology NAS Configuration +SYNOLOGY_HOST=192.168.0.102 +SYNOLOGY_PORT=5000 +SYNOLOGY_USERNAME=superadmin +SYNOLOGY_PASSWORD=Cl0ud_1985! +SYNOLOGY_SECURE=False +SYNOLOGY_TIMEOUT=10 +SYNOLOGY_API_VERSION=6 + +# API Configuration +SYNOLOGY_POWER_API=SYNO.Core.System +SYNOLOGY_INFO_API=SYNO.DSM.Info + +# Wake-on-LAN Configuration +SYNOLOGY_MAC=90:09:D0:8C:27:F9 +WOL_PORT=9 diff --git a/.history/.gitignore_20250830063748 b/.history/.gitignore_20250830063748 new file mode 100644 index 0000000..0a0a319 --- /dev/null +++ b/.history/.gitignore_20250830063748 @@ -0,0 +1,44 @@ +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Logs +logs/ +*.log + +# IDE specific files +.idea/ +.vscode/ +*.swp +*.swo + +# OS specific files +.DS_Store +Thumbs.db diff --git a/.history/.gitignore_20250830063839 b/.history/.gitignore_20250830063839 new file mode 100644 index 0000000..0a0a319 --- /dev/null +++ b/.history/.gitignore_20250830063839 @@ -0,0 +1,44 @@ +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Logs +logs/ +*.log + +# IDE specific files +.idea/ +.vscode/ +*.swp +*.swo + +# OS specific files +.DS_Store +Thumbs.db diff --git a/.history/DOCKER_DEPLOYMENT_20250830103243.md b/.history/DOCKER_DEPLOYMENT_20250830103243.md new file mode 100644 index 0000000..4e5b666 --- /dev/null +++ b/.history/DOCKER_DEPLOYMENT_20250830103243.md @@ -0,0 +1,218 @@ +# Synology Power Control Bot - Руководство по развертыванию в Docker + +## Подготовка к развертыванию + +Это руководство поможет вам развернуть бота для управления питанием Synology NAS в Docker-контейнере. Развертывание в Docker имеет следующие преимущества: +- Изоляция приложения и его зависимостей +- Простота управления и обновления +- Автоматический перезапуск при сбоях +- Возможность легкого переноса между системами + +## Предварительные требования + +1. **Установка Docker и Docker Compose**: + + **Для Ubuntu/Debian**: + ```bash + # Установка Docker + sudo apt update + sudo apt install apt-transport-https ca-certificates curl software-properties-common + curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg + echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null + sudo apt update + sudo apt install docker-ce docker-ce-cli containerd.io + + # Установка Docker Compose + sudo curl -L "https://github.com/docker/compose/releases/download/v2.15.1/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose + sudo chmod +x /usr/local/bin/docker-compose + ``` + + **Для CentOS/RHEL**: + ```bash + # Установка Docker + sudo yum install -y yum-utils + sudo yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo + sudo yum install docker-ce docker-ce-cli containerd.io + sudo systemctl start docker + sudo systemctl enable docker + + # Установка Docker Compose + sudo curl -L "https://github.com/docker/compose/releases/download/v2.15.1/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose + sudo chmod +x /usr/local/bin/docker-compose + ``` + + **Для Windows**: + - Скачайте и установите Docker Desktop с [официального сайта Docker](https://www.docker.com/products/docker-desktop/) + +2. **Настройка проекта**: + ```bash + # Клонирование репозитория (если используете Git) + git clone https://github.com/yourusername/synology_power_control_bot.git + cd synology_power_control_bot + + # Или распакуйте архив с исходным кодом + ``` + +## Конфигурация + +1. **Создайте файл .env**: + Создайте файл `.env` на основе `.env-example` и настройте его с вашими параметрами: + + ```bash + cp .env-example .env + nano .env # или любой другой текстовый редактор + ``` + + Заполните следующие параметры: + - `TELEGRAM_TOKEN`: Токен вашего Telegram-бота от @BotFather + - `ADMIN_USER_IDS`: ID пользователей Telegram с доступом к боту + - `SYNOLOGY_HOST`: IP-адрес вашего Synology NAS + - `SYNOLOGY_USERNAME` и `SYNOLOGY_PASSWORD`: Учетные данные для DSM + - `MAC_ADDRESS`: MAC-адрес Synology NAS для Wake-on-LAN + +## Развертывание + +### С использованием скриптов: + +**Linux**: +```bash +chmod +x deploy.sh +./deploy.sh +``` + +**Windows**: +``` +deploy.cmd +``` + +### Вручную с Docker Compose: + +1. **Сборка и запуск**: + ```bash + docker-compose up -d --build + ``` + +2. **Проверка статуса**: + ```bash + docker-compose ps + ``` + +3. **Просмотр логов**: + ```bash + docker-compose logs -f + ``` + +4. **Остановка**: + ```bash + docker-compose down + ``` + +## Управление контейнером + +### Перезапуск бота: +```bash +docker-compose restart +``` + +### Обновление: +```bash +# Остановка +docker-compose down + +# Обновление (если используете Git) +git pull + +# Пересборка и запуск +docker-compose up -d --build +``` + +### Резервное копирование данных: +Важные данные хранятся в томе `logs`, который можно скопировать: +```bash +# Создание бэкапа логов +tar -czvf synology_bot_logs_backup.tar.gz ./logs +``` + +## Проверка работоспособности + +После развертывания можно проверить состояние бота с помощью следующих команд: + +1. **Проверка статуса контейнера**: + ```bash + docker-compose ps + ``` + +2. **Проверка health-check**: + ```bash + curl http://localhost:8080/health + ``` + Должен вернуть `OK`. + +3. **Проверка логов**: + ```bash + docker-compose logs -f + ``` + Ищите строки с успешной инициализацией бота. + +## Решение проблем + +### Контейнер не запускается или сразу завершает работу +- Проверьте логи: `docker-compose logs -f` +- Проверьте файл `.env` на наличие всех необходимых параметров +- Убедитесь, что порт 8080 не занят другим приложением + +### Проблемы с подключением к Synology NAS +- Проверьте доступность NAS из контейнера: + ```bash + docker-compose exec synology-bot ping $SYNOLOGY_HOST + ``` +- Проверьте правильность учетных данных +- Убедитесь, что API DSM включено в настройках NAS + +### Telegram-бот не отвечает +- Проверьте корректность TELEGRAM_TOKEN +- Убедитесь, что бот запущен: `/start` в чате с ботом +- Проверьте, что ваш Telegram ID указан в ADMIN_USER_IDS + +## Автоматический запуск при перезагрузке сервера + +Docker и Docker Compose по умолчанию настроены на автоматический запуск контейнеров при перезагрузке системы благодаря параметру `restart: unless-stopped` в docker-compose.yml. + +Если эта опция не работает, вы можете настроить systemd: + +1. **Создайте файл сервиса**: + ```bash + sudo nano /etc/systemd/system/synology-bot.service + ``` + +2. **Добавьте следующее содержимое**: + ``` + [Unit] + Description=Synology Power Control Bot + Requires=docker.service + After=docker.service + + [Service] + Type=oneshot + RemainAfterExit=yes + WorkingDirectory=/path/to/synology_power_control_bot + ExecStart=/usr/local/bin/docker-compose up -d + ExecStop=/usr/local/bin/docker-compose down + TimeoutStartSec=0 + + [Install] + WantedBy=multi-user.target + ``` + +3. **Активируйте и запустите сервис**: + ```bash + sudo systemctl enable synology-bot.service + sudo systemctl start synology-bot.service + ``` + +## Безопасность + +- Не передавайте файл `.env` с учетными данными третьим лицам +- Регулярно меняйте пароль от DSM +- Ограничьте доступ к боту только доверенным пользователям +- Рассмотрите возможность запуска на выделенной сети или с дополнительными ограничениями доступа diff --git a/.history/DOCKER_DEPLOYMENT_20250830103340.md b/.history/DOCKER_DEPLOYMENT_20250830103340.md new file mode 100644 index 0000000..4e5b666 --- /dev/null +++ b/.history/DOCKER_DEPLOYMENT_20250830103340.md @@ -0,0 +1,218 @@ +# Synology Power Control Bot - Руководство по развертыванию в Docker + +## Подготовка к развертыванию + +Это руководство поможет вам развернуть бота для управления питанием Synology NAS в Docker-контейнере. Развертывание в Docker имеет следующие преимущества: +- Изоляция приложения и его зависимостей +- Простота управления и обновления +- Автоматический перезапуск при сбоях +- Возможность легкого переноса между системами + +## Предварительные требования + +1. **Установка Docker и Docker Compose**: + + **Для Ubuntu/Debian**: + ```bash + # Установка Docker + sudo apt update + sudo apt install apt-transport-https ca-certificates curl software-properties-common + curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg + echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null + sudo apt update + sudo apt install docker-ce docker-ce-cli containerd.io + + # Установка Docker Compose + sudo curl -L "https://github.com/docker/compose/releases/download/v2.15.1/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose + sudo chmod +x /usr/local/bin/docker-compose + ``` + + **Для CentOS/RHEL**: + ```bash + # Установка Docker + sudo yum install -y yum-utils + sudo yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo + sudo yum install docker-ce docker-ce-cli containerd.io + sudo systemctl start docker + sudo systemctl enable docker + + # Установка Docker Compose + sudo curl -L "https://github.com/docker/compose/releases/download/v2.15.1/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose + sudo chmod +x /usr/local/bin/docker-compose + ``` + + **Для Windows**: + - Скачайте и установите Docker Desktop с [официального сайта Docker](https://www.docker.com/products/docker-desktop/) + +2. **Настройка проекта**: + ```bash + # Клонирование репозитория (если используете Git) + git clone https://github.com/yourusername/synology_power_control_bot.git + cd synology_power_control_bot + + # Или распакуйте архив с исходным кодом + ``` + +## Конфигурация + +1. **Создайте файл .env**: + Создайте файл `.env` на основе `.env-example` и настройте его с вашими параметрами: + + ```bash + cp .env-example .env + nano .env # или любой другой текстовый редактор + ``` + + Заполните следующие параметры: + - `TELEGRAM_TOKEN`: Токен вашего Telegram-бота от @BotFather + - `ADMIN_USER_IDS`: ID пользователей Telegram с доступом к боту + - `SYNOLOGY_HOST`: IP-адрес вашего Synology NAS + - `SYNOLOGY_USERNAME` и `SYNOLOGY_PASSWORD`: Учетные данные для DSM + - `MAC_ADDRESS`: MAC-адрес Synology NAS для Wake-on-LAN + +## Развертывание + +### С использованием скриптов: + +**Linux**: +```bash +chmod +x deploy.sh +./deploy.sh +``` + +**Windows**: +``` +deploy.cmd +``` + +### Вручную с Docker Compose: + +1. **Сборка и запуск**: + ```bash + docker-compose up -d --build + ``` + +2. **Проверка статуса**: + ```bash + docker-compose ps + ``` + +3. **Просмотр логов**: + ```bash + docker-compose logs -f + ``` + +4. **Остановка**: + ```bash + docker-compose down + ``` + +## Управление контейнером + +### Перезапуск бота: +```bash +docker-compose restart +``` + +### Обновление: +```bash +# Остановка +docker-compose down + +# Обновление (если используете Git) +git pull + +# Пересборка и запуск +docker-compose up -d --build +``` + +### Резервное копирование данных: +Важные данные хранятся в томе `logs`, который можно скопировать: +```bash +# Создание бэкапа логов +tar -czvf synology_bot_logs_backup.tar.gz ./logs +``` + +## Проверка работоспособности + +После развертывания можно проверить состояние бота с помощью следующих команд: + +1. **Проверка статуса контейнера**: + ```bash + docker-compose ps + ``` + +2. **Проверка health-check**: + ```bash + curl http://localhost:8080/health + ``` + Должен вернуть `OK`. + +3. **Проверка логов**: + ```bash + docker-compose logs -f + ``` + Ищите строки с успешной инициализацией бота. + +## Решение проблем + +### Контейнер не запускается или сразу завершает работу +- Проверьте логи: `docker-compose logs -f` +- Проверьте файл `.env` на наличие всех необходимых параметров +- Убедитесь, что порт 8080 не занят другим приложением + +### Проблемы с подключением к Synology NAS +- Проверьте доступность NAS из контейнера: + ```bash + docker-compose exec synology-bot ping $SYNOLOGY_HOST + ``` +- Проверьте правильность учетных данных +- Убедитесь, что API DSM включено в настройках NAS + +### Telegram-бот не отвечает +- Проверьте корректность TELEGRAM_TOKEN +- Убедитесь, что бот запущен: `/start` в чате с ботом +- Проверьте, что ваш Telegram ID указан в ADMIN_USER_IDS + +## Автоматический запуск при перезагрузке сервера + +Docker и Docker Compose по умолчанию настроены на автоматический запуск контейнеров при перезагрузке системы благодаря параметру `restart: unless-stopped` в docker-compose.yml. + +Если эта опция не работает, вы можете настроить systemd: + +1. **Создайте файл сервиса**: + ```bash + sudo nano /etc/systemd/system/synology-bot.service + ``` + +2. **Добавьте следующее содержимое**: + ``` + [Unit] + Description=Synology Power Control Bot + Requires=docker.service + After=docker.service + + [Service] + Type=oneshot + RemainAfterExit=yes + WorkingDirectory=/path/to/synology_power_control_bot + ExecStart=/usr/local/bin/docker-compose up -d + ExecStop=/usr/local/bin/docker-compose down + TimeoutStartSec=0 + + [Install] + WantedBy=multi-user.target + ``` + +3. **Активируйте и запустите сервис**: + ```bash + sudo systemctl enable synology-bot.service + sudo systemctl start synology-bot.service + ``` + +## Безопасность + +- Не передавайте файл `.env` с учетными данными третьим лицам +- Регулярно меняйте пароль от DSM +- Ограничьте доступ к боту только доверенным пользователям +- Рассмотрите возможность запуска на выделенной сети или с дополнительными ограничениями доступа diff --git a/.history/Dockerfile_20250830102631 b/.history/Dockerfile_20250830102631 new file mode 100644 index 0000000..c29324d --- /dev/null +++ b/.history/Dockerfile_20250830102631 @@ -0,0 +1,20 @@ +FROM python:3.11-slim + +# Устанавливаем рабочую директорию +WORKDIR /app + +# Копируем файлы зависимостей +COPY requirements.txt . + +# Устанавливаем зависимости +RUN pip install --no-cache-dir -r requirements.txt + +# Копируем исходный код +COPY . . + +# Указываем переменные окружения +ENV PYTHONPATH=/app +ENV PYTHONUNBUFFERED=1 + +# Запускаем приложение +CMD ["python", "run.py"] diff --git a/.history/Dockerfile_20250830102802 b/.history/Dockerfile_20250830102802 new file mode 100644 index 0000000..e08df4b --- /dev/null +++ b/.history/Dockerfile_20250830102802 @@ -0,0 +1,23 @@ +FROM python:3.11-slim + +# Устанавливаем рабочую директорию +WORKDIR /app + +# Копируем файлы зависимостей +COPY requirements.txt . + +# Устанавливаем зависимости +RUN pip install --no-cache-dir -r requirements.txt + +# Копируем исходный код +COPY . . + +# Делаем entrypoint исполняемым +RUN chmod +x /app/entrypoint.sh + +# Указываем переменные окружения +ENV PYTHONPATH=/app +ENV PYTHONUNBUFFERED=1 + +# Используем entrypoint.sh +ENTRYPOINT ["/app/entrypoint.sh"] diff --git a/.history/Dockerfile_20250830103154 b/.history/Dockerfile_20250830103154 new file mode 100644 index 0000000..e08df4b --- /dev/null +++ b/.history/Dockerfile_20250830103154 @@ -0,0 +1,23 @@ +FROM python:3.11-slim + +# Устанавливаем рабочую директорию +WORKDIR /app + +# Копируем файлы зависимостей +COPY requirements.txt . + +# Устанавливаем зависимости +RUN pip install --no-cache-dir -r requirements.txt + +# Копируем исходный код +COPY . . + +# Делаем entrypoint исполняемым +RUN chmod +x /app/entrypoint.sh + +# Указываем переменные окружения +ENV PYTHONPATH=/app +ENV PYTHONUNBUFFERED=1 + +# Используем entrypoint.sh +ENTRYPOINT ["/app/entrypoint.sh"] diff --git a/.history/README_20250830063733.md b/.history/README_20250830063733.md new file mode 100644 index 0000000..4145dc8 --- /dev/null +++ b/.history/README_20250830063733.md @@ -0,0 +1,101 @@ +# Synology Power Control Bot + +Telegram-бот для удаленного управления питанием сетевого хранилища Synology NAS (DS223j и другие модели). + +## Возможности + +- ✅ Включение питания через Wake-on-LAN +- ✅ Выключение питания через API DSM +- ✅ Перезагрузка системы +- ✅ Проверка статуса и получение информации о системе +- ✅ Ограничение доступа по ID пользователей + +## Требования + +- Python 3.8+ +- Synology NAS с включенным WoL +- Учетная запись администратора DSM +- Telegram Bot API Token + +## Установка и настройка + +1. Клонируйте репозиторий: + ```bash + git clone https://github.com/yourusername/synology_power_control_bot.git + cd synology_power_control_bot + ``` + +2. Установите зависимости: + ```bash + pip install -r requirements.txt + ``` + +3. Настройте параметры в файле `.env`: + ``` + # Telegram Bot Configuration + TELEGRAM_TOKEN=your_telegram_bot_token + ADMIN_USER_IDS=123456789,987654321 + + # Synology NAS Configuration + SYNOLOGY_HOST=192.168.1.100 + SYNOLOGY_PORT=5000 + SYNOLOGY_USERNAME=admin + SYNOLOGY_PASSWORD=your_password + SYNOLOGY_SECURE=False + SYNOLOGY_TIMEOUT=10 + + # Wake-on-LAN Configuration + SYNOLOGY_MAC=00:11:22:33:44:55 + WOL_PORT=9 + ``` + +4. Запустите бота: + ```bash + python -m src.bot + ``` + +## Подготовка Synology NAS + +1. Включите Wake-on-LAN в настройках DSM: + - Панель управления > Сеть > Общие > Wake-on-LAN + +2. Убедитесь, что API DSM включено: + - Панель управления > Службы терминала и SNMP > Включить DSM API + +3. Узнайте MAC-адрес вашего NAS: + - Панель управления > Сеть > Сетевой интерфейс + +## Команды бота + +- `/start` - Начало работы с ботом +- `/status` - Проверка текущего статуса NAS +- `/power` - Управление питанием NAS +- `/help` - Вывод справочной информации + +## Структура проекта + +``` +synology_power_control_bot/ +├── logs/ # Директория для логов +├── src/ # Исходный код +│ ├── api/ # Модули для работы с API +│ │ └── synology.py # API для работы с Synology NAS +│ ├── config/ # Модули конфигурации +│ │ └── config.py # Основная конфигурация +│ ├── handlers/ # Обработчики команд бота +│ │ └── command_handlers.py +│ ├── utils/ # Вспомогательные утилиты +│ │ └── logger.py # Настройка логирования +│ └── bot.py # Основной файл запуска бота +├── .env # Файл с переменными окружения +├── .gitignore # Файл игнорирования Git +└── requirements.txt # Зависимости проекта +``` + +## Безопасность + +Бот имеет систему авторизации на основе ID пользователей Telegram. Убедитесь, что указали правильные ID в переменной `ADMIN_USER_IDS` в файле `.env`. + +## Лицензия + +MIT diff --git a/.history/README_20250830063839.md b/.history/README_20250830063839.md new file mode 100644 index 0000000..4145dc8 --- /dev/null +++ b/.history/README_20250830063839.md @@ -0,0 +1,101 @@ +# Synology Power Control Bot + +Telegram-бот для удаленного управления питанием сетевого хранилища Synology NAS (DS223j и другие модели). + +## Возможности + +- ✅ Включение питания через Wake-on-LAN +- ✅ Выключение питания через API DSM +- ✅ Перезагрузка системы +- ✅ Проверка статуса и получение информации о системе +- ✅ Ограничение доступа по ID пользователей + +## Требования + +- Python 3.8+ +- Synology NAS с включенным WoL +- Учетная запись администратора DSM +- Telegram Bot API Token + +## Установка и настройка + +1. Клонируйте репозиторий: + ```bash + git clone https://github.com/yourusername/synology_power_control_bot.git + cd synology_power_control_bot + ``` + +2. Установите зависимости: + ```bash + pip install -r requirements.txt + ``` + +3. Настройте параметры в файле `.env`: + ``` + # Telegram Bot Configuration + TELEGRAM_TOKEN=your_telegram_bot_token + ADMIN_USER_IDS=123456789,987654321 + + # Synology NAS Configuration + SYNOLOGY_HOST=192.168.1.100 + SYNOLOGY_PORT=5000 + SYNOLOGY_USERNAME=admin + SYNOLOGY_PASSWORD=your_password + SYNOLOGY_SECURE=False + SYNOLOGY_TIMEOUT=10 + + # Wake-on-LAN Configuration + SYNOLOGY_MAC=00:11:22:33:44:55 + WOL_PORT=9 + ``` + +4. Запустите бота: + ```bash + python -m src.bot + ``` + +## Подготовка Synology NAS + +1. Включите Wake-on-LAN в настройках DSM: + - Панель управления > Сеть > Общие > Wake-on-LAN + +2. Убедитесь, что API DSM включено: + - Панель управления > Службы терминала и SNMP > Включить DSM API + +3. Узнайте MAC-адрес вашего NAS: + - Панель управления > Сеть > Сетевой интерфейс + +## Команды бота + +- `/start` - Начало работы с ботом +- `/status` - Проверка текущего статуса NAS +- `/power` - Управление питанием NAS +- `/help` - Вывод справочной информации + +## Структура проекта + +``` +synology_power_control_bot/ +├── logs/ # Директория для логов +├── src/ # Исходный код +│ ├── api/ # Модули для работы с API +│ │ └── synology.py # API для работы с Synology NAS +│ ├── config/ # Модули конфигурации +│ │ └── config.py # Основная конфигурация +│ ├── handlers/ # Обработчики команд бота +│ │ └── command_handlers.py +│ ├── utils/ # Вспомогательные утилиты +│ │ └── logger.py # Настройка логирования +│ └── bot.py # Основной файл запуска бота +├── .env # Файл с переменными окружения +├── .gitignore # Файл игнорирования Git +└── requirements.txt # Зависимости проекта +``` + +## Безопасность + +Бот имеет систему авторизации на основе ID пользователей Telegram. Убедитесь, что указали правильные ID в переменной `ADMIN_USER_IDS` в файле `.env`. + +## Лицензия + +MIT diff --git a/.history/README_20250830065402.md b/.history/README_20250830065402.md new file mode 100644 index 0000000..6733e92 --- /dev/null +++ b/.history/README_20250830065402.md @@ -0,0 +1,117 @@ +# Synology Power Control Bot + +Telegram-бот для удаленного управления питанием сетевого хранилища Synology NAS (DS223j и другие модели). + +## Возможности + +### Управление питанием +- ✅ Включение питания через Wake-on-LAN +- ✅ Выключение питания через API DSM +- ✅ Перезагрузка системы с отслеживанием статуса + +### Мониторинг системы +- ✅ Проверка онлайн статуса NAS +- ✅ Информация о системе (модель, версия DSM, время работы) +- ✅ Мониторинг загрузки CPU и памяти +- ✅ Данные о температуре и сетевой активности +- ✅ Статус хранилища и дисков +- ✅ Информация о безопасности системы + +### Управление данными +- ✅ Просмотр списка общих папок +- ✅ Информация о томах и дисках +- ✅ Статистика использования дисков + +### Безопасность +- ✅ Ограничение доступа по ID пользователей Telegram +- ✅ Безопасное хранение учетных данных + +## Требования + +- Python 3.8+ +- Synology NAS с включенным WoL +- Учетная запись администратора DSM +- Telegram Bot API Token + +## Установка и настройка + +1. Клонируйте репозиторий: + ```bash + git clone https://github.com/yourusername/synology_power_control_bot.git + cd synology_power_control_bot + ``` + +2. Установите зависимости: + ```bash + pip install -r requirements.txt + ``` + +3. Настройте параметры в файле `.env`: + ``` + # Telegram Bot Configuration + TELEGRAM_TOKEN=your_telegram_bot_token + ADMIN_USER_IDS=123456789,987654321 + + # Synology NAS Configuration + SYNOLOGY_HOST=192.168.1.100 + SYNOLOGY_PORT=5000 + SYNOLOGY_USERNAME=admin + SYNOLOGY_PASSWORD=your_password + SYNOLOGY_SECURE=False + SYNOLOGY_TIMEOUT=10 + + # Wake-on-LAN Configuration + SYNOLOGY_MAC=00:11:22:33:44:55 + WOL_PORT=9 + ``` + +4. Запустите бота: + ```bash + python -m src.bot + ``` + +## Подготовка Synology NAS + +1. Включите Wake-on-LAN в настройках DSM: + - Панель управления > Сеть > Общие > Wake-on-LAN + +2. Убедитесь, что API DSM включено: + - Панель управления > Службы терминала и SNMP > Включить DSM API + +3. Узнайте MAC-адрес вашего NAS: + - Панель управления > Сеть > Сетевой интерфейс + +## Команды бота + +- `/start` - Начало работы с ботом +- `/status` - Проверка текущего статуса NAS +- `/power` - Управление питанием NAS +- `/help` - Вывод справочной информации + +## Структура проекта + +``` +synology_power_control_bot/ +├── logs/ # Директория для логов +├── src/ # Исходный код +│ ├── api/ # Модули для работы с API +│ │ └── synology.py # API для работы с Synology NAS +│ ├── config/ # Модули конфигурации +│ │ └── config.py # Основная конфигурация +│ ├── handlers/ # Обработчики команд бота +│ │ └── command_handlers.py +│ ├── utils/ # Вспомогательные утилиты +│ │ └── logger.py # Настройка логирования +│ └── bot.py # Основной файл запуска бота +├── .env # Файл с переменными окружения +├── .gitignore # Файл игнорирования Git +└── requirements.txt # Зависимости проекта +``` + +## Безопасность + +Бот имеет систему авторизации на основе ID пользователей Telegram. Убедитесь, что указали правильные ID в переменной `ADMIN_USER_IDS` в файле `.env`. + +## Лицензия + +MIT diff --git a/.history/README_20250830065412.md b/.history/README_20250830065412.md new file mode 100644 index 0000000..669dbaa --- /dev/null +++ b/.history/README_20250830065412.md @@ -0,0 +1,125 @@ +# Synology Power Control Bot + +Telegram-бот для удаленного управления питанием сетевого хранилища Synology NAS (DS223j и другие модели). + +## Возможности + +### Управление питанием +- ✅ Включение питания через Wake-on-LAN +- ✅ Выключение питания через API DSM +- ✅ Перезагрузка системы с отслеживанием статуса + +### Мониторинг системы +- ✅ Проверка онлайн статуса NAS +- ✅ Информация о системе (модель, версия DSM, время работы) +- ✅ Мониторинг загрузки CPU и памяти +- ✅ Данные о температуре и сетевой активности +- ✅ Статус хранилища и дисков +- ✅ Информация о безопасности системы + +### Управление данными +- ✅ Просмотр списка общих папок +- ✅ Информация о томах и дисках +- ✅ Статистика использования дисков + +### Безопасность +- ✅ Ограничение доступа по ID пользователей Telegram +- ✅ Безопасное хранение учетных данных + +## Требования + +- Python 3.8+ +- Synology NAS с включенным WoL +- Учетная запись администратора DSM +- Telegram Bot API Token + +## Установка и настройка + +1. Клонируйте репозиторий: + ```bash + git clone https://github.com/yourusername/synology_power_control_bot.git + cd synology_power_control_bot + ``` + +2. Установите зависимости: + ```bash + pip install -r requirements.txt + ``` + +3. Настройте параметры в файле `.env`: + ``` + # Telegram Bot Configuration + TELEGRAM_TOKEN=your_telegram_bot_token + ADMIN_USER_IDS=123456789,987654321 + + # Synology NAS Configuration + SYNOLOGY_HOST=192.168.1.100 + SYNOLOGY_PORT=5000 + SYNOLOGY_USERNAME=admin + SYNOLOGY_PASSWORD=your_password + SYNOLOGY_SECURE=False + SYNOLOGY_TIMEOUT=10 + + # Wake-on-LAN Configuration + SYNOLOGY_MAC=00:11:22:33:44:55 + WOL_PORT=9 + ``` + +4. Запустите бота: + ```bash + python -m src.bot + ``` + +## Подготовка Synology NAS + +1. Включите Wake-on-LAN в настройках DSM: + - Панель управления > Сеть > Общие > Wake-on-LAN + +2. Убедитесь, что API DSM включено: + - Панель управления > Службы терминала и SNMP > Включить DSM API + +3. Узнайте MAC-адрес вашего NAS: + - Панель управления > Сеть > Сетевой интерфейс + +## Команды бота + +### Основные команды +- `/start` - Начало работы с ботом +- `/status` - Проверка текущего статуса NAS +- `/power` - Управление питанием NAS (включение, выключение, перезагрузка) +- `/help` - Вывод справочной информации + +### Расширенные команды +- `/system` - Подробная информация о системе +- `/storage` - Информация о хранилище и дисках +- `/shares` - Список общих папок +- `/load` - Текущая нагрузка на систему +- `/security` - Статус безопасности системы + +## Структура проекта + +``` +synology_power_control_bot/ +├── logs/ # Директория для логов +├── src/ # Исходный код +│ ├── api/ # Модули для работы с API +│ │ └── synology.py # API для работы с Synology NAS +│ ├── config/ # Модули конфигурации +│ │ └── config.py # Основная конфигурация +│ ├── handlers/ # Обработчики команд бота +│ │ └── command_handlers.py +│ ├── utils/ # Вспомогательные утилиты +│ │ └── logger.py # Настройка логирования +│ └── bot.py # Основной файл запуска бота +├── .env # Файл с переменными окружения +├── .gitignore # Файл игнорирования Git +└── requirements.txt # Зависимости проекта +``` + +## Безопасность + +Бот имеет систему авторизации на основе ID пользователей Telegram. Убедитесь, что указали правильные ID в переменной `ADMIN_USER_IDS` в файле `.env`. + +## Лицензия + +MIT diff --git a/.history/README_20250830065454.md b/.history/README_20250830065454.md new file mode 100644 index 0000000..669dbaa --- /dev/null +++ b/.history/README_20250830065454.md @@ -0,0 +1,125 @@ +# Synology Power Control Bot + +Telegram-бот для удаленного управления питанием сетевого хранилища Synology NAS (DS223j и другие модели). + +## Возможности + +### Управление питанием +- ✅ Включение питания через Wake-on-LAN +- ✅ Выключение питания через API DSM +- ✅ Перезагрузка системы с отслеживанием статуса + +### Мониторинг системы +- ✅ Проверка онлайн статуса NAS +- ✅ Информация о системе (модель, версия DSM, время работы) +- ✅ Мониторинг загрузки CPU и памяти +- ✅ Данные о температуре и сетевой активности +- ✅ Статус хранилища и дисков +- ✅ Информация о безопасности системы + +### Управление данными +- ✅ Просмотр списка общих папок +- ✅ Информация о томах и дисках +- ✅ Статистика использования дисков + +### Безопасность +- ✅ Ограничение доступа по ID пользователей Telegram +- ✅ Безопасное хранение учетных данных + +## Требования + +- Python 3.8+ +- Synology NAS с включенным WoL +- Учетная запись администратора DSM +- Telegram Bot API Token + +## Установка и настройка + +1. Клонируйте репозиторий: + ```bash + git clone https://github.com/yourusername/synology_power_control_bot.git + cd synology_power_control_bot + ``` + +2. Установите зависимости: + ```bash + pip install -r requirements.txt + ``` + +3. Настройте параметры в файле `.env`: + ``` + # Telegram Bot Configuration + TELEGRAM_TOKEN=your_telegram_bot_token + ADMIN_USER_IDS=123456789,987654321 + + # Synology NAS Configuration + SYNOLOGY_HOST=192.168.1.100 + SYNOLOGY_PORT=5000 + SYNOLOGY_USERNAME=admin + SYNOLOGY_PASSWORD=your_password + SYNOLOGY_SECURE=False + SYNOLOGY_TIMEOUT=10 + + # Wake-on-LAN Configuration + SYNOLOGY_MAC=00:11:22:33:44:55 + WOL_PORT=9 + ``` + +4. Запустите бота: + ```bash + python -m src.bot + ``` + +## Подготовка Synology NAS + +1. Включите Wake-on-LAN в настройках DSM: + - Панель управления > Сеть > Общие > Wake-on-LAN + +2. Убедитесь, что API DSM включено: + - Панель управления > Службы терминала и SNMP > Включить DSM API + +3. Узнайте MAC-адрес вашего NAS: + - Панель управления > Сеть > Сетевой интерфейс + +## Команды бота + +### Основные команды +- `/start` - Начало работы с ботом +- `/status` - Проверка текущего статуса NAS +- `/power` - Управление питанием NAS (включение, выключение, перезагрузка) +- `/help` - Вывод справочной информации + +### Расширенные команды +- `/system` - Подробная информация о системе +- `/storage` - Информация о хранилище и дисках +- `/shares` - Список общих папок +- `/load` - Текущая нагрузка на систему +- `/security` - Статус безопасности системы + +## Структура проекта + +``` +synology_power_control_bot/ +├── logs/ # Директория для логов +├── src/ # Исходный код +│ ├── api/ # Модули для работы с API +│ │ └── synology.py # API для работы с Synology NAS +│ ├── config/ # Модули конфигурации +│ │ └── config.py # Основная конфигурация +│ ├── handlers/ # Обработчики команд бота +│ │ └── command_handlers.py +│ ├── utils/ # Вспомогательные утилиты +│ │ └── logger.py # Настройка логирования +│ └── bot.py # Основной файл запуска бота +├── .env # Файл с переменными окружения +├── .gitignore # Файл игнорирования Git +└── requirements.txt # Зависимости проекта +``` + +## Безопасность + +Бот имеет систему авторизации на основе ID пользователей Telegram. Убедитесь, что указали правильные ID в переменной `ADMIN_USER_IDS` в файле `.env`. + +## Лицензия + +MIT diff --git a/.history/README_20250830072408.md b/.history/README_20250830072408.md new file mode 100644 index 0000000..c7b18ca --- /dev/null +++ b/.history/README_20250830072408.md @@ -0,0 +1,126 @@ +# Synology Power Control Bot + +Telegram-бот для удаленного управления питанием сетевого хранилища Synology NAS (DS223j и другие модели). + +## Возможности + +### Управление питанием +- ✅ Включение питания через Wake-on-LAN +- ✅ Выключение питания через API DSM +- ✅ Перезагрузка системы с отслеживанием статуса + +### Мониторинг системы +- ✅ Проверка онлайн статуса NAS +- ✅ Информация о системе (модель, версия DSM, время работы) +- ✅ Мониторинг загрузки CPU и памяти +- ✅ Данные о температуре и сетевой активности +- ✅ Статус хранилища и дисков +- ✅ Информация о безопасности системы + +### Управление данными +- ✅ Просмотр списка общих папок +- ✅ Информация о томах и дисках +- ✅ Статистика использования дисков + +### Безопасность +- ✅ Ограничение доступа по ID пользователей Telegram +- ✅ Безопасное хранение учетных данных + +## Требования + +- Python 3.8+ +- Synology NAS с включенным WoL +- Учетная запись администратора DSM +- Telegram Bot API Token +- Доступ к порту API Synology DSM (обычно 5000 или 5001) + +## Установка и настройка + +1. Клонируйте репозиторий: + ```bash + git clone https://github.com/yourusername/synology_power_control_bot.git + cd synology_power_control_bot + ``` + +2. Установите зависимости: + ```bash + pip install -r requirements.txt + ``` + +3. Настройте параметры в файле `.env`: + ``` + # Telegram Bot Configuration + TELEGRAM_TOKEN=your_telegram_bot_token + ADMIN_USER_IDS=123456789,987654321 + + # Synology NAS Configuration + SYNOLOGY_HOST=192.168.1.100 + SYNOLOGY_PORT=5000 + SYNOLOGY_USERNAME=admin + SYNOLOGY_PASSWORD=your_password + SYNOLOGY_SECURE=False + SYNOLOGY_TIMEOUT=10 + + # Wake-on-LAN Configuration + SYNOLOGY_MAC=00:11:22:33:44:55 + WOL_PORT=9 + ``` + +4. Запустите бота: + ```bash + python -m src.bot + ``` + +## Подготовка Synology NAS + +1. Включите Wake-on-LAN в настройках DSM: + - Панель управления > Сеть > Общие > Wake-on-LAN + +2. Убедитесь, что API DSM включено: + - Панель управления > Службы терминала и SNMP > Включить DSM API + +3. Узнайте MAC-адрес вашего NAS: + - Панель управления > Сеть > Сетевой интерфейс + +## Команды бота + +### Основные команды +- `/start` - Начало работы с ботом +- `/status` - Проверка текущего статуса NAS +- `/power` - Управление питанием NAS (включение, выключение, перезагрузка) +- `/help` - Вывод справочной информации + +### Расширенные команды +- `/system` - Подробная информация о системе +- `/storage` - Информация о хранилище и дисках +- `/shares` - Список общих папок +- `/load` - Текущая нагрузка на систему +- `/security` - Статус безопасности системы + +## Структура проекта + +``` +synology_power_control_bot/ +├── logs/ # Директория для логов +├── src/ # Исходный код +│ ├── api/ # Модули для работы с API +│ │ └── synology.py # API для работы с Synology NAS +│ ├── config/ # Модули конфигурации +│ │ └── config.py # Основная конфигурация +│ ├── handlers/ # Обработчики команд бота +│ │ └── command_handlers.py +│ ├── utils/ # Вспомогательные утилиты +│ │ └── logger.py # Настройка логирования +│ └── bot.py # Основной файл запуска бота +├── .env # Файл с переменными окружения +├── .gitignore # Файл игнорирования Git +└── requirements.txt # Зависимости проекта +``` + +## Безопасность + +Бот имеет систему авторизации на основе ID пользователей Telegram. Убедитесь, что указали правильные ID в переменной `ADMIN_USER_IDS` в файле `.env`. + +## Лицензия + +MIT diff --git a/.history/README_20250830072817.md b/.history/README_20250830072817.md new file mode 100644 index 0000000..c7b18ca --- /dev/null +++ b/.history/README_20250830072817.md @@ -0,0 +1,126 @@ +# Synology Power Control Bot + +Telegram-бот для удаленного управления питанием сетевого хранилища Synology NAS (DS223j и другие модели). + +## Возможности + +### Управление питанием +- ✅ Включение питания через Wake-on-LAN +- ✅ Выключение питания через API DSM +- ✅ Перезагрузка системы с отслеживанием статуса + +### Мониторинг системы +- ✅ Проверка онлайн статуса NAS +- ✅ Информация о системе (модель, версия DSM, время работы) +- ✅ Мониторинг загрузки CPU и памяти +- ✅ Данные о температуре и сетевой активности +- ✅ Статус хранилища и дисков +- ✅ Информация о безопасности системы + +### Управление данными +- ✅ Просмотр списка общих папок +- ✅ Информация о томах и дисках +- ✅ Статистика использования дисков + +### Безопасность +- ✅ Ограничение доступа по ID пользователей Telegram +- ✅ Безопасное хранение учетных данных + +## Требования + +- Python 3.8+ +- Synology NAS с включенным WoL +- Учетная запись администратора DSM +- Telegram Bot API Token +- Доступ к порту API Synology DSM (обычно 5000 или 5001) + +## Установка и настройка + +1. Клонируйте репозиторий: + ```bash + git clone https://github.com/yourusername/synology_power_control_bot.git + cd synology_power_control_bot + ``` + +2. Установите зависимости: + ```bash + pip install -r requirements.txt + ``` + +3. Настройте параметры в файле `.env`: + ``` + # Telegram Bot Configuration + TELEGRAM_TOKEN=your_telegram_bot_token + ADMIN_USER_IDS=123456789,987654321 + + # Synology NAS Configuration + SYNOLOGY_HOST=192.168.1.100 + SYNOLOGY_PORT=5000 + SYNOLOGY_USERNAME=admin + SYNOLOGY_PASSWORD=your_password + SYNOLOGY_SECURE=False + SYNOLOGY_TIMEOUT=10 + + # Wake-on-LAN Configuration + SYNOLOGY_MAC=00:11:22:33:44:55 + WOL_PORT=9 + ``` + +4. Запустите бота: + ```bash + python -m src.bot + ``` + +## Подготовка Synology NAS + +1. Включите Wake-on-LAN в настройках DSM: + - Панель управления > Сеть > Общие > Wake-on-LAN + +2. Убедитесь, что API DSM включено: + - Панель управления > Службы терминала и SNMP > Включить DSM API + +3. Узнайте MAC-адрес вашего NAS: + - Панель управления > Сеть > Сетевой интерфейс + +## Команды бота + +### Основные команды +- `/start` - Начало работы с ботом +- `/status` - Проверка текущего статуса NAS +- `/power` - Управление питанием NAS (включение, выключение, перезагрузка) +- `/help` - Вывод справочной информации + +### Расширенные команды +- `/system` - Подробная информация о системе +- `/storage` - Информация о хранилище и дисках +- `/shares` - Список общих папок +- `/load` - Текущая нагрузка на систему +- `/security` - Статус безопасности системы + +## Структура проекта + +``` +synology_power_control_bot/ +├── logs/ # Директория для логов +├── src/ # Исходный код +│ ├── api/ # Модули для работы с API +│ │ └── synology.py # API для работы с Synology NAS +│ ├── config/ # Модули конфигурации +│ │ └── config.py # Основная конфигурация +│ ├── handlers/ # Обработчики команд бота +│ │ └── command_handlers.py +│ ├── utils/ # Вспомогательные утилиты +│ │ └── logger.py # Настройка логирования +│ └── bot.py # Основной файл запуска бота +├── .env # Файл с переменными окружения +├── .gitignore # Файл игнорирования Git +└── requirements.txt # Зависимости проекта +``` + +## Безопасность + +Бот имеет систему авторизации на основе ID пользователей Telegram. Убедитесь, что указали правильные ID в переменной `ADMIN_USER_IDS` в файле `.env`. + +## Лицензия + +MIT diff --git a/.history/README_20250830092253.md b/.history/README_20250830092253.md new file mode 100644 index 0000000..6d82398 --- /dev/null +++ b/.history/README_20250830092253.md @@ -0,0 +1,128 @@ +# Synology Power Control Bot + +Telegram-бот для удаленного управления питанием сетевого хранилища Synology NAS (DS223j и другие модели). + +## Возможности + +### Управление питанием +- ✅ Включение питания через Wake-on-LAN +- ✅ Выключение питания через API DSM +- ✅ Перезагрузка системы с отслеживанием статуса + +### Мониторинг системы +- ✅ Проверка онлайн статуса NAS +- ✅ Информация о системе (модель, версия DSM, время работы) +- ✅ Мониторинг загрузки CPU и памяти +- ✅ Данные о температуре и сетевой активности +- ✅ Статус хранилища и дисков +- ✅ Информация о безопасности системы +- ✅ Список активных процессов +- ✅ Мониторинг сетевых подключений + +### Управление данными +- ✅ Просмотр списка общих папок +- ✅ Информация о томах и дисках +- ✅ Статистика использования дисков + +### Безопасность +- ✅ Ограничение доступа по ID пользователей Telegram +- ✅ Безопасное хранение учетных данных + +## Требования + +- Python 3.8+ +- Synology NAS с включенным WoL +- Учетная запись администратора DSM +- Telegram Bot API Token +- Доступ к порту API Synology DSM (обычно 5000 или 5001) + +## Установка и настройка + +1. Клонируйте репозиторий: + ```bash + git clone https://github.com/yourusername/synology_power_control_bot.git + cd synology_power_control_bot + ``` + +2. Установите зависимости: + ```bash + pip install -r requirements.txt + ``` + +3. Настройте параметры в файле `.env`: + ``` + # Telegram Bot Configuration + TELEGRAM_TOKEN=your_telegram_bot_token + ADMIN_USER_IDS=123456789,987654321 + + # Synology NAS Configuration + SYNOLOGY_HOST=192.168.1.100 + SYNOLOGY_PORT=5000 + SYNOLOGY_USERNAME=admin + SYNOLOGY_PASSWORD=your_password + SYNOLOGY_SECURE=False + SYNOLOGY_TIMEOUT=10 + + # Wake-on-LAN Configuration + SYNOLOGY_MAC=00:11:22:33:44:55 + WOL_PORT=9 + ``` + +4. Запустите бота: + ```bash + python -m src.bot + ``` + +## Подготовка Synology NAS + +1. Включите Wake-on-LAN в настройках DSM: + - Панель управления > Сеть > Общие > Wake-on-LAN + +2. Убедитесь, что API DSM включено: + - Панель управления > Службы терминала и SNMP > Включить DSM API + +3. Узнайте MAC-адрес вашего NAS: + - Панель управления > Сеть > Сетевой интерфейс + +## Команды бота + +### Основные команды +- `/start` - Начало работы с ботом +- `/status` - Проверка текущего статуса NAS +- `/power` - Управление питанием NAS (включение, выключение, перезагрузка) +- `/help` - Вывод справочной информации + +### Расширенные команды +- `/system` - Подробная информация о системе +- `/storage` - Информация о хранилище и дисках +- `/shares` - Список общих папок +- `/load` - Текущая нагрузка на систему +- `/security` - Статус безопасности системы + +## Структура проекта + +``` +synology_power_control_bot/ +├── logs/ # Директория для логов +├── src/ # Исходный код +│ ├── api/ # Модули для работы с API +│ │ └── synology.py # API для работы с Synology NAS +│ ├── config/ # Модули конфигурации +│ │ └── config.py # Основная конфигурация +│ ├── handlers/ # Обработчики команд бота +│ │ └── command_handlers.py +│ ├── utils/ # Вспомогательные утилиты +│ │ └── logger.py # Настройка логирования +│ └── bot.py # Основной файл запуска бота +├── .env # Файл с переменными окружения +├── .gitignore # Файл игнорирования Git +└── requirements.txt # Зависимости проекта +``` + +## Безопасность + +Бот имеет систему авторизации на основе ID пользователей Telegram. Убедитесь, что указали правильные ID в переменной `ADMIN_USER_IDS` в файле `.env`. + +## Лицензия + +MIT diff --git a/.history/README_20250830092310.md b/.history/README_20250830092310.md new file mode 100644 index 0000000..d68a352 --- /dev/null +++ b/.history/README_20250830092310.md @@ -0,0 +1,131 @@ +# Synology Power Control Bot + +Telegram-бот для удаленного управления питанием сетевого хранилища Synology NAS (DS223j и другие модели). + +## Возможности + +### Управление питанием +- ✅ Включение питания через Wake-on-LAN +- ✅ Выключение питания через API DSM +- ✅ Перезагрузка системы с отслеживанием статуса + +### Мониторинг системы +- ✅ Проверка онлайн статуса NAS +- ✅ Информация о системе (модель, версия DSM, время работы) +- ✅ Мониторинг загрузки CPU и памяти +- ✅ Данные о температуре и сетевой активности +- ✅ Статус хранилища и дисков +- ✅ Информация о безопасности системы +- ✅ Список активных процессов +- ✅ Мониторинг сетевых подключений + +### Управление данными +- ✅ Просмотр списка общих папок +- ✅ Информация о томах и дисках +- ✅ Статистика использования дисков +- ✅ Просмотр файлов и папок +- ✅ Поиск файлов +- ✅ Мониторинг квот пользователей + +### Безопасность +- ✅ Ограничение доступа по ID пользователей Telegram +- ✅ Безопасное хранение учетных данных + +## Требования + +- Python 3.8+ +- Synology NAS с включенным WoL +- Учетная запись администратора DSM +- Telegram Bot API Token +- Доступ к порту API Synology DSM (обычно 5000 или 5001) + +## Установка и настройка + +1. Клонируйте репозиторий: + ```bash + git clone https://github.com/yourusername/synology_power_control_bot.git + cd synology_power_control_bot + ``` + +2. Установите зависимости: + ```bash + pip install -r requirements.txt + ``` + +3. Настройте параметры в файле `.env`: + ``` + # Telegram Bot Configuration + TELEGRAM_TOKEN=your_telegram_bot_token + ADMIN_USER_IDS=123456789,987654321 + + # Synology NAS Configuration + SYNOLOGY_HOST=192.168.1.100 + SYNOLOGY_PORT=5000 + SYNOLOGY_USERNAME=admin + SYNOLOGY_PASSWORD=your_password + SYNOLOGY_SECURE=False + SYNOLOGY_TIMEOUT=10 + + # Wake-on-LAN Configuration + SYNOLOGY_MAC=00:11:22:33:44:55 + WOL_PORT=9 + ``` + +4. Запустите бота: + ```bash + python -m src.bot + ``` + +## Подготовка Synology NAS + +1. Включите Wake-on-LAN в настройках DSM: + - Панель управления > Сеть > Общие > Wake-on-LAN + +2. Убедитесь, что API DSM включено: + - Панель управления > Службы терминала и SNMP > Включить DSM API + +3. Узнайте MAC-адрес вашего NAS: + - Панель управления > Сеть > Сетевой интерфейс + +## Команды бота + +### Основные команды +- `/start` - Начало работы с ботом +- `/status` - Проверка текущего статуса NAS +- `/power` - Управление питанием NAS (включение, выключение, перезагрузка) +- `/help` - Вывод справочной информации + +### Расширенные команды +- `/system` - Подробная информация о системе +- `/storage` - Информация о хранилище и дисках +- `/shares` - Список общих папок +- `/load` - Текущая нагрузка на систему +- `/security` - Статус безопасности системы + +## Структура проекта + +``` +synology_power_control_bot/ +├── logs/ # Директория для логов +├── src/ # Исходный код +│ ├── api/ # Модули для работы с API +│ │ └── synology.py # API для работы с Synology NAS +│ ├── config/ # Модули конфигурации +│ │ └── config.py # Основная конфигурация +│ ├── handlers/ # Обработчики команд бота +│ │ └── command_handlers.py +│ ├── utils/ # Вспомогательные утилиты +│ │ └── logger.py # Настройка логирования +│ └── bot.py # Основной файл запуска бота +├── .env # Файл с переменными окружения +├── .gitignore # Файл игнорирования Git +└── requirements.txt # Зависимости проекта +``` + +## Безопасность + +Бот имеет систему авторизации на основе ID пользователей Telegram. Убедитесь, что указали правильные ID в переменной `ADMIN_USER_IDS` в файле `.env`. + +## Лицензия + +MIT diff --git a/.history/README_20250830092330.md b/.history/README_20250830092330.md new file mode 100644 index 0000000..487bc81 --- /dev/null +++ b/.history/README_20250830092330.md @@ -0,0 +1,146 @@ +# Synology Power Control Bot + +Telegram-бот для удаленного управления питанием сетевого хранилища Synology NAS (DS223j и другие модели). + +## Возможности + +### Управление питанием +- ✅ Включение питания через Wake-on-LAN +- ✅ Выключение питания через API DSM +- ✅ Перезагрузка системы с отслеживанием статуса + +### Мониторинг системы +- ✅ Проверка онлайн статуса NAS +- ✅ Информация о системе (модель, версия DSM, время работы) +- ✅ Мониторинг загрузки CPU и памяти +- ✅ Данные о температуре и сетевой активности +- ✅ Статус хранилища и дисков +- ✅ Информация о безопасности системы +- ✅ Список активных процессов +- ✅ Мониторинг сетевых подключений + +### Управление данными +- ✅ Просмотр списка общих папок +- ✅ Информация о томах и дисках +- ✅ Статистика использования дисков +- ✅ Просмотр файлов и папок +- ✅ Поиск файлов +- ✅ Мониторинг квот пользователей + +### Безопасность +- ✅ Ограничение доступа по ID пользователей Telegram +- ✅ Безопасное хранение учетных данных + +## Требования + +- Python 3.8+ +- Synology NAS с включенным WoL +- Учетная запись администратора DSM +- Telegram Bot API Token +- Доступ к порту API Synology DSM (обычно 5000 или 5001) + +## Установка и настройка + +1. Клонируйте репозиторий: + ```bash + git clone https://github.com/yourusername/synology_power_control_bot.git + cd synology_power_control_bot + ``` + +2. Установите зависимости: + ```bash + pip install -r requirements.txt + ``` + +3. Настройте параметры в файле `.env`: + ``` + # Telegram Bot Configuration + TELEGRAM_TOKEN=your_telegram_bot_token + ADMIN_USER_IDS=123456789,987654321 + + # Synology NAS Configuration + SYNOLOGY_HOST=192.168.1.100 + SYNOLOGY_PORT=5000 + SYNOLOGY_USERNAME=admin + SYNOLOGY_PASSWORD=your_password + SYNOLOGY_SECURE=False + SYNOLOGY_TIMEOUT=10 + + # Wake-on-LAN Configuration + SYNOLOGY_MAC=00:11:22:33:44:55 + WOL_PORT=9 + ``` + +4. Запустите бота: + ```bash + python -m src.bot + ``` + +## Подготовка Synology NAS + +1. Включите Wake-on-LAN в настройках DSM: + - Панель управления > Сеть > Общие > Wake-on-LAN + +2. Убедитесь, что API DSM включено: + - Панель управления > Службы терминала и SNMP > Включить DSM API + +3. Узнайте MAC-адрес вашего NAS: + - Панель управления > Сеть > Сетевой интерфейс + +## Команды бота + +### Основные команды +- `/start` - Начало работы с ботом +- `/status` - Проверка текущего статуса NAS +- `/power` - Управление питанием NAS (включение, выключение, перезагрузка) +- `/help` - Вывод справочной информации + +### Информационные команды +- `/system` - Подробная информация о системе +- `/storage` - Информация о хранилище и дисках +- `/shares` - Список общих папок +- `/load` - Текущая нагрузка на систему +- `/security` - Статус безопасности системы +- `/temperature` - Температура устройства +- `/processes` - Список активных процессов +- `/network` - Сетевая информация + +### Расширенные команды +- `/schedule` - Расписание питания +- `/browse` - Просмотр файлов +- `/search <запрос>` - Поиск файлов +- `/updates` - Проверка обновлений +- `/backup` - Статус резервного копирования +- `/quota` - Квоты пользователей + +### Быстрые команды +- `/quickreboot` - Быстрая перезагрузка +- `/wakeup` - Пробуждение NAS (WOL) + +## Структура проекта + +``` +synology_power_control_bot/ +├── logs/ # Директория для логов +├── src/ # Исходный код +│ ├── api/ # Модули для работы с API +│ │ └── synology.py # API для работы с Synology NAS +│ ├── config/ # Модули конфигурации +│ │ └── config.py # Основная конфигурация +│ ├── handlers/ # Обработчики команд бота +│ │ └── command_handlers.py +│ ├── utils/ # Вспомогательные утилиты +│ │ └── logger.py # Настройка логирования +│ └── bot.py # Основной файл запуска бота +├── .env # Файл с переменными окружения +├── .gitignore # Файл игнорирования Git +└── requirements.txt # Зависимости проекта +``` + +## Безопасность + +Бот имеет систему авторизации на основе ID пользователей Telegram. Убедитесь, что указали правильные ID в переменной `ADMIN_USER_IDS` в файле `.env`. + +## Лицензия + +MIT diff --git a/.history/README_20250830092350.md b/.history/README_20250830092350.md new file mode 100644 index 0000000..5e2e7d0 --- /dev/null +++ b/.history/README_20250830092350.md @@ -0,0 +1,151 @@ +# Synology Power Control Bot + +Telegram-бот для удаленного управления питанием сетевого хранилища Synology NAS (DS223j и другие модели). + +## Возможности + +### Управление питанием +- ✅ Включение питания через Wake-on-LAN +- ✅ Выключение питания через API DSM +- ✅ Перезагрузка системы с отслеживанием статуса + +### Мониторинг системы +- ✅ Проверка онлайн статуса NAS +- ✅ Информация о системе (модель, версия DSM, время работы) +- ✅ Мониторинг загрузки CPU и памяти +- ✅ Данные о температуре и сетевой активности +- ✅ Статус хранилища и дисков +- ✅ Информация о безопасности системы +- ✅ Список активных процессов +- ✅ Мониторинг сетевых подключений + +### Управление данными +- ✅ Просмотр списка общих папок +- ✅ Информация о томах и дисках +- ✅ Статистика использования дисков +- ✅ Просмотр файлов и папок +- ✅ Поиск файлов +- ✅ Мониторинг квот пользователей + +### Безопасность +- ✅ Ограничение доступа по ID пользователей Telegram +- ✅ Безопасное хранение учетных данных + +### Дополнительные функции +- ✅ Мониторинг обновлений DSM и пакетов +- ✅ Управление расписанием питания +- ✅ Проверка статуса резервного копирования + +## Требования + +- Python 3.8+ +- Synology NAS с включенным WoL +- Учетная запись администратора DSM +- Telegram Bot API Token +- Доступ к порту API Synology DSM (обычно 5000 или 5001) + +## Установка и настройка + +1. Клонируйте репозиторий: + ```bash + git clone https://github.com/yourusername/synology_power_control_bot.git + cd synology_power_control_bot + ``` + +2. Установите зависимости: + ```bash + pip install -r requirements.txt + ``` + +3. Настройте параметры в файле `.env`: + ``` + # Telegram Bot Configuration + TELEGRAM_TOKEN=your_telegram_bot_token + ADMIN_USER_IDS=123456789,987654321 + + # Synology NAS Configuration + SYNOLOGY_HOST=192.168.1.100 + SYNOLOGY_PORT=5000 + SYNOLOGY_USERNAME=admin + SYNOLOGY_PASSWORD=your_password + SYNOLOGY_SECURE=False + SYNOLOGY_TIMEOUT=10 + + # Wake-on-LAN Configuration + SYNOLOGY_MAC=00:11:22:33:44:55 + WOL_PORT=9 + ``` + +4. Запустите бота: + ```bash + python -m src.bot + ``` + +## Подготовка Synology NAS + +1. Включите Wake-on-LAN в настройках DSM: + - Панель управления > Сеть > Общие > Wake-on-LAN + +2. Убедитесь, что API DSM включено: + - Панель управления > Службы терминала и SNMP > Включить DSM API + +3. Узнайте MAC-адрес вашего NAS: + - Панель управления > Сеть > Сетевой интерфейс + +## Команды бота + +### Основные команды +- `/start` - Начало работы с ботом +- `/status` - Проверка текущего статуса NAS +- `/power` - Управление питанием NAS (включение, выключение, перезагрузка) +- `/help` - Вывод справочной информации + +### Информационные команды +- `/system` - Подробная информация о системе +- `/storage` - Информация о хранилище и дисках +- `/shares` - Список общих папок +- `/load` - Текущая нагрузка на систему +- `/security` - Статус безопасности системы +- `/temperature` - Температура устройства +- `/processes` - Список активных процессов +- `/network` - Сетевая информация + +### Расширенные команды +- `/schedule` - Расписание питания +- `/browse` - Просмотр файлов +- `/search <запрос>` - Поиск файлов +- `/updates` - Проверка обновлений +- `/backup` - Статус резервного копирования +- `/quota` - Квоты пользователей + +### Быстрые команды +- `/quickreboot` - Быстрая перезагрузка +- `/wakeup` - Пробуждение NAS (WOL) + +## Структура проекта + +``` +synology_power_control_bot/ +├── logs/ # Директория для логов +├── src/ # Исходный код +│ ├── api/ # Модули для работы с API +│ │ └── synology.py # API для работы с Synology NAS +│ ├── config/ # Модули конфигурации +│ │ └── config.py # Основная конфигурация +│ ├── handlers/ # Обработчики команд бота +│ │ └── command_handlers.py +│ ├── utils/ # Вспомогательные утилиты +│ │ └── logger.py # Настройка логирования +│ └── bot.py # Основной файл запуска бота +├── .env # Файл с переменными окружения +├── .gitignore # Файл игнорирования Git +└── requirements.txt # Зависимости проекта +``` + +## Безопасность + +Бот имеет систему авторизации на основе ID пользователей Telegram. Убедитесь, что указали правильные ID в переменной `ADMIN_USER_IDS` в файле `.env`. + +## Лицензия + +MIT diff --git a/.history/README_20250830092440.md b/.history/README_20250830092440.md new file mode 100644 index 0000000..5e2e7d0 --- /dev/null +++ b/.history/README_20250830092440.md @@ -0,0 +1,151 @@ +# Synology Power Control Bot + +Telegram-бот для удаленного управления питанием сетевого хранилища Synology NAS (DS223j и другие модели). + +## Возможности + +### Управление питанием +- ✅ Включение питания через Wake-on-LAN +- ✅ Выключение питания через API DSM +- ✅ Перезагрузка системы с отслеживанием статуса + +### Мониторинг системы +- ✅ Проверка онлайн статуса NAS +- ✅ Информация о системе (модель, версия DSM, время работы) +- ✅ Мониторинг загрузки CPU и памяти +- ✅ Данные о температуре и сетевой активности +- ✅ Статус хранилища и дисков +- ✅ Информация о безопасности системы +- ✅ Список активных процессов +- ✅ Мониторинг сетевых подключений + +### Управление данными +- ✅ Просмотр списка общих папок +- ✅ Информация о томах и дисках +- ✅ Статистика использования дисков +- ✅ Просмотр файлов и папок +- ✅ Поиск файлов +- ✅ Мониторинг квот пользователей + +### Безопасность +- ✅ Ограничение доступа по ID пользователей Telegram +- ✅ Безопасное хранение учетных данных + +### Дополнительные функции +- ✅ Мониторинг обновлений DSM и пакетов +- ✅ Управление расписанием питания +- ✅ Проверка статуса резервного копирования + +## Требования + +- Python 3.8+ +- Synology NAS с включенным WoL +- Учетная запись администратора DSM +- Telegram Bot API Token +- Доступ к порту API Synology DSM (обычно 5000 или 5001) + +## Установка и настройка + +1. Клонируйте репозиторий: + ```bash + git clone https://github.com/yourusername/synology_power_control_bot.git + cd synology_power_control_bot + ``` + +2. Установите зависимости: + ```bash + pip install -r requirements.txt + ``` + +3. Настройте параметры в файле `.env`: + ``` + # Telegram Bot Configuration + TELEGRAM_TOKEN=your_telegram_bot_token + ADMIN_USER_IDS=123456789,987654321 + + # Synology NAS Configuration + SYNOLOGY_HOST=192.168.1.100 + SYNOLOGY_PORT=5000 + SYNOLOGY_USERNAME=admin + SYNOLOGY_PASSWORD=your_password + SYNOLOGY_SECURE=False + SYNOLOGY_TIMEOUT=10 + + # Wake-on-LAN Configuration + SYNOLOGY_MAC=00:11:22:33:44:55 + WOL_PORT=9 + ``` + +4. Запустите бота: + ```bash + python -m src.bot + ``` + +## Подготовка Synology NAS + +1. Включите Wake-on-LAN в настройках DSM: + - Панель управления > Сеть > Общие > Wake-on-LAN + +2. Убедитесь, что API DSM включено: + - Панель управления > Службы терминала и SNMP > Включить DSM API + +3. Узнайте MAC-адрес вашего NAS: + - Панель управления > Сеть > Сетевой интерфейс + +## Команды бота + +### Основные команды +- `/start` - Начало работы с ботом +- `/status` - Проверка текущего статуса NAS +- `/power` - Управление питанием NAS (включение, выключение, перезагрузка) +- `/help` - Вывод справочной информации + +### Информационные команды +- `/system` - Подробная информация о системе +- `/storage` - Информация о хранилище и дисках +- `/shares` - Список общих папок +- `/load` - Текущая нагрузка на систему +- `/security` - Статус безопасности системы +- `/temperature` - Температура устройства +- `/processes` - Список активных процессов +- `/network` - Сетевая информация + +### Расширенные команды +- `/schedule` - Расписание питания +- `/browse` - Просмотр файлов +- `/search <запрос>` - Поиск файлов +- `/updates` - Проверка обновлений +- `/backup` - Статус резервного копирования +- `/quota` - Квоты пользователей + +### Быстрые команды +- `/quickreboot` - Быстрая перезагрузка +- `/wakeup` - Пробуждение NAS (WOL) + +## Структура проекта + +``` +synology_power_control_bot/ +├── logs/ # Директория для логов +├── src/ # Исходный код +│ ├── api/ # Модули для работы с API +│ │ └── synology.py # API для работы с Synology NAS +│ ├── config/ # Модули конфигурации +│ │ └── config.py # Основная конфигурация +│ ├── handlers/ # Обработчики команд бота +│ │ └── command_handlers.py +│ ├── utils/ # Вспомогательные утилиты +│ │ └── logger.py # Настройка логирования +│ └── bot.py # Основной файл запуска бота +├── .env # Файл с переменными окружения +├── .gitignore # Файл игнорирования Git +└── requirements.txt # Зависимости проекта +``` + +## Безопасность + +Бот имеет систему авторизации на основе ID пользователей Telegram. Убедитесь, что указали правильные ID в переменной `ADMIN_USER_IDS` в файле `.env`. + +## Лицензия + +MIT diff --git a/.history/README_20250830103059.md b/.history/README_20250830103059.md new file mode 100644 index 0000000..bfc3201 --- /dev/null +++ b/.history/README_20250830103059.md @@ -0,0 +1,192 @@ +# Synology Power Control Bot + +Telegram-бот для удаленного управления питанием сетевого хранилища Synology NAS (DS223j и другие модели). + +## Возможности + +### Управление питанием +- ✅ Включение питания через Wake-on-LAN +- ✅ Выключение питания через API DSM +- ✅ Перезагрузка системы с отслеживанием статуса + +### Мониторинг системы +- ✅ Проверка онлайн статуса NAS +- ✅ Информация о системе (модель, версия DSM, время работы) +- ✅ Мониторинг загрузки CPU и памяти +- ✅ Данные о температуре и сетевой активности +- ✅ Статус хранилища и дисков +- ✅ Информация о безопасности системы +- ✅ Список активных процессов +- ✅ Мониторинг сетевых подключений + +### Управление данными +- ✅ Просмотр списка общих папок +- ✅ Информация о томах и дисках +- ✅ Статистика использования дисков +- ✅ Просмотр файлов и папок +- ✅ Поиск файлов +- ✅ Мониторинг квот пользователей + +### Безопасность +- ✅ Ограничение доступа по ID пользователей Telegram +- ✅ Безопасное хранение учетных данных + +### Дополнительные функции +- ✅ Мониторинг обновлений DSM и пакетов +- ✅ Управление расписанием питания +- ✅ Проверка статуса резервного копирования + +## Требования + +- Python 3.8+ +- Synology NAS с включенным WoL +- Учетная запись администратора DSM +- Telegram Bot API Token +- Доступ к порту API Synology DSM (обычно 5000 или 5001) + +## Установка и настройка + +### Метод 1: Локальный запуск + +1. Клонируйте репозиторий: + ```bash + git clone https://github.com/yourusername/synology_power_control_bot.git + cd synology_power_control_bot + ``` + +2. Установите зависимости: + ```bash + pip install -r requirements.txt + ``` + +3. Настройте параметры в файле `.env`: + ``` + # Telegram Bot Configuration + TELEGRAM_TOKEN=your_telegram_bot_token + ADMIN_USER_IDS=123456789,987654321 + + # Synology NAS Configuration + SYNOLOGY_HOST=192.168.1.100 + SYNOLOGY_PORT=5000 + SYNOLOGY_USERNAME=admin + SYNOLOGY_PASSWORD=your_password + SYNOLOGY_SECURE=False + SYNOLOGY_TIMEOUT=10 + + # Wake-on-LAN Configuration + SYNOLOGY_MAC=00:11:22:33:44:55 + WOL_PORT=9 + ``` + +4. Запустите бота: + ```bash + python run.py + ``` + +### Метод 2: Docker + +1. Убедитесь, что Docker и Docker Compose установлены в вашей системе. + +2. Клонируйте репозиторий: + ```bash + git clone https://github.com/yourusername/synology_power_control_bot.git + cd synology_power_control_bot + ``` + +3. Создайте файл `.env` на основе `.env-example` и заполните необходимыми значениями. + +4. Запустите скрипт развертывания: + ```bash + # Linux/macOS + chmod +x deploy.sh + ./deploy.sh + + # Windows + deploy.cmd + ``` + + Или запустите вручную: + ```bash + docker-compose up -d --build + ``` + +5. Проверьте статус: + ```bash + docker-compose ps + ``` + +6. Просмотр логов: + ```bash + docker-compose logs -f + ``` + +Дополнительная информация о Docker-развертывании доступна в файле [README_DOCKER.md](README_DOCKER.md). + +## Подготовка Synology NAS + +1. Включите Wake-on-LAN в настройках DSM: + - Панель управления > Сеть > Общие > Wake-on-LAN + +2. Убедитесь, что API DSM включено: + - Панель управления > Службы терминала и SNMP > Включить DSM API + +3. Узнайте MAC-адрес вашего NAS: + - Панель управления > Сеть > Сетевой интерфейс + +## Команды бота + +### Основные команды +- `/start` - Начало работы с ботом +- `/status` - Проверка текущего статуса NAS +- `/power` - Управление питанием NAS (включение, выключение, перезагрузка) +- `/help` - Вывод справочной информации + +### Информационные команды +- `/system` - Подробная информация о системе +- `/storage` - Информация о хранилище и дисках +- `/shares` - Список общих папок +- `/load` - Текущая нагрузка на систему +- `/security` - Статус безопасности системы +- `/temperature` - Температура устройства +- `/processes` - Список активных процессов +- `/network` - Сетевая информация + +### Расширенные команды +- `/schedule` - Расписание питания +- `/browse` - Просмотр файлов +- `/search <запрос>` - Поиск файлов +- `/updates` - Проверка обновлений +- `/backup` - Статус резервного копирования +- `/quota` - Квоты пользователей + +### Быстрые команды +- `/quickreboot` - Быстрая перезагрузка +- `/wakeup` - Пробуждение NAS (WOL) + +## Структура проекта + +``` +synology_power_control_bot/ +├── logs/ # Директория для логов +├── src/ # Исходный код +│ ├── api/ # Модули для работы с API +│ │ └── synology.py # API для работы с Synology NAS +│ ├── config/ # Модули конфигурации +│ │ └── config.py # Основная конфигурация +│ ├── handlers/ # Обработчики команд бота +│ │ └── command_handlers.py +│ ├── utils/ # Вспомогательные утилиты +│ │ └── logger.py # Настройка логирования +│ └── bot.py # Основной файл запуска бота +├── .env # Файл с переменными окружения +├── .gitignore # Файл игнорирования Git +└── requirements.txt # Зависимости проекта +``` + +## Безопасность + +Бот имеет систему авторизации на основе ID пользователей Telegram. Убедитесь, что указали правильные ID в переменной `ADMIN_USER_IDS` в файле `.env`. + +## Лицензия + +MIT diff --git a/.history/README_20250830103119.md b/.history/README_20250830103119.md new file mode 100644 index 0000000..5e890a6 --- /dev/null +++ b/.history/README_20250830103119.md @@ -0,0 +1,201 @@ +# Synology Power Control Bot + +Telegram-бот для удаленного управления питанием сетевого хранилища Synology NAS (DS223j и другие модели). + +## Возможности + +### Управление питанием +- ✅ Включение питания через Wake-on-LAN +- ✅ Выключение питания через API DSM +- ✅ Перезагрузка системы с отслеживанием статуса + +### Мониторинг системы +- ✅ Проверка онлайн статуса NAS +- ✅ Информация о системе (модель, версия DSM, время работы) +- ✅ Мониторинг загрузки CPU и памяти +- ✅ Данные о температуре и сетевой активности +- ✅ Статус хранилища и дисков +- ✅ Информация о безопасности системы +- ✅ Список активных процессов +- ✅ Мониторинг сетевых подключений + +### Управление данными +- ✅ Просмотр списка общих папок +- ✅ Информация о томах и дисках +- ✅ Статистика использования дисков +- ✅ Просмотр файлов и папок +- ✅ Поиск файлов +- ✅ Мониторинг квот пользователей + +### Безопасность +- ✅ Ограничение доступа по ID пользователей Telegram +- ✅ Безопасное хранение учетных данных + +### Дополнительные функции +- ✅ Мониторинг обновлений DSM и пакетов +- ✅ Управление расписанием питания +- ✅ Проверка статуса резервного копирования + +## Требования + +- Python 3.8+ +- Synology NAS с включенным WoL +- Учетная запись администратора DSM +- Telegram Bot API Token +- Доступ к порту API Synology DSM (обычно 5000 или 5001) + +## Установка и настройка + +### Метод 1: Локальный запуск + +1. Клонируйте репозиторий: + ```bash + git clone https://github.com/yourusername/synology_power_control_bot.git + cd synology_power_control_bot + ``` + +2. Установите зависимости: + ```bash + pip install -r requirements.txt + ``` + +3. Настройте параметры в файле `.env`: + ``` + # Telegram Bot Configuration + TELEGRAM_TOKEN=your_telegram_bot_token + ADMIN_USER_IDS=123456789,987654321 + + # Synology NAS Configuration + SYNOLOGY_HOST=192.168.1.100 + SYNOLOGY_PORT=5000 + SYNOLOGY_USERNAME=admin + SYNOLOGY_PASSWORD=your_password + SYNOLOGY_SECURE=False + SYNOLOGY_TIMEOUT=10 + + # Wake-on-LAN Configuration + SYNOLOGY_MAC=00:11:22:33:44:55 + WOL_PORT=9 + ``` + +4. Запустите бота: + ```bash + python run.py + ``` + +### Метод 2: Docker + +1. Убедитесь, что Docker и Docker Compose установлены в вашей системе. + +2. Клонируйте репозиторий: + ```bash + git clone https://github.com/yourusername/synology_power_control_bot.git + cd synology_power_control_bot + ``` + +3. Создайте файл `.env` на основе `.env-example` и заполните необходимыми значениями. + +4. Запустите скрипт развертывания: + ```bash + # Linux/macOS + chmod +x deploy.sh + ./deploy.sh + + # Windows + deploy.cmd + ``` + + Или запустите вручную: + ```bash + docker-compose up -d --build + ``` + +5. Проверьте статус: + ```bash + docker-compose ps + ``` + +6. Просмотр логов: + ```bash + docker-compose logs -f + ``` + +Дополнительная информация о Docker-развертывании доступна в файле [README_DOCKER.md](README_DOCKER.md). + +## Подготовка Synology NAS + +1. Включите Wake-on-LAN в настройках DSM: + - Панель управления > Сеть > Общие > Wake-on-LAN + +2. Убедитесь, что API DSM включено: + - Панель управления > Службы терминала и SNMP > Включить DSM API + +3. Узнайте MAC-адрес вашего NAS: + - Панель управления > Сеть > Сетевой интерфейс + +## Команды бота + +### Основные команды +- `/start` - Начало работы с ботом +- `/status` - Проверка текущего статуса NAS +- `/power` - Управление питанием NAS (включение, выключение, перезагрузка) +- `/help` - Вывод справочной информации + +### Информационные команды +- `/system` - Подробная информация о системе +- `/storage` - Информация о хранилище и дисках +- `/shares` - Список общих папок +- `/load` - Текущая нагрузка на систему +- `/security` - Статус безопасности системы +- `/temperature` - Температура устройства +- `/processes` - Список активных процессов +- `/network` - Сетевая информация + +### Расширенные команды +- `/schedule` - Расписание питания +- `/browse` - Просмотр файлов +- `/search <запрос>` - Поиск файлов +- `/updates` - Проверка обновлений +- `/backup` - Статус резервного копирования +- `/quota` - Квоты пользователей + +### Быстрые команды +- `/quickreboot` - Быстрая перезагрузка +- `/wakeup` - Пробуждение NAS (WOL) + +## Структура проекта + +``` +synology_power_control_bot/ +├── logs/ # Директория для логов +├── src/ # Исходный код +│ ├── api/ # Модули для работы с API +│ │ └── synology.py # API для работы с Synology NAS +│ ├── config/ # Модули конфигурации +│ │ └── config.py # Основная конфигурация +│ ├── handlers/ # Обработчики команд бота +│ │ └── command_handlers.py +│ ├── utils/ # Вспомогательные утилиты +│ │ └── logger.py # Настройка логирования +│ └── bot.py # Основной файл запуска бота +├── .env # Файл с переменными окружения +├── .env-example # Пример файла переменных окружения +├── .gitignore # Файл игнорирования Git +├── .dockerignore # Файлы, игнорируемые при сборке Docker-образа +├── Dockerfile # Инструкции для сборки Docker-образа +├── docker-compose.yml # Конфигурация Docker Compose +├── deploy.sh # Скрипт развёртывания для Linux +├── deploy.cmd # Скрипт развёртывания для Windows +├── entrypoint.sh # Скрипт для запуска в Docker +├── README.md # Основная документация +├── README_DOCKER.md # Документация по Docker-развёртыванию +└── requirements.txt # Зависимости проекта +``` + +## Безопасность + +Бот имеет систему авторизации на основе ID пользователей Telegram. Убедитесь, что указали правильные ID в переменной `ADMIN_USER_IDS` в файле `.env`. + +## Лицензия + +MIT diff --git a/.history/README_20250830103139.md b/.history/README_20250830103139.md new file mode 100644 index 0000000..473468e --- /dev/null +++ b/.history/README_20250830103139.md @@ -0,0 +1,236 @@ +# Synology Power Control Bot + +Telegram-бот для удаленного управления питанием сетевого хранилища Synology NAS (DS223j и другие модели). + +## Возможности + +### Управление питанием +- ✅ Включение питания через Wake-on-LAN +- ✅ Выключение питания через API DSM +- ✅ Перезагрузка системы с отслеживанием статуса + +### Мониторинг системы +- ✅ Проверка онлайн статуса NAS +- ✅ Информация о системе (модель, версия DSM, время работы) +- ✅ Мониторинг загрузки CPU и памяти +- ✅ Данные о температуре и сетевой активности +- ✅ Статус хранилища и дисков +- ✅ Информация о безопасности системы +- ✅ Список активных процессов +- ✅ Мониторинг сетевых подключений + +### Управление данными +- ✅ Просмотр списка общих папок +- ✅ Информация о томах и дисках +- ✅ Статистика использования дисков +- ✅ Просмотр файлов и папок +- ✅ Поиск файлов +- ✅ Мониторинг квот пользователей + +### Безопасность +- ✅ Ограничение доступа по ID пользователей Telegram +- ✅ Безопасное хранение учетных данных + +### Дополнительные функции +- ✅ Мониторинг обновлений DSM и пакетов +- ✅ Управление расписанием питания +- ✅ Проверка статуса резервного копирования + +## Требования + +- Python 3.8+ +- Synology NAS с включенным WoL +- Учетная запись администратора DSM +- Telegram Bot API Token +- Доступ к порту API Synology DSM (обычно 5000 или 5001) + +## Установка и настройка + +### Метод 1: Локальный запуск + +1. Клонируйте репозиторий: + ```bash + git clone https://github.com/yourusername/synology_power_control_bot.git + cd synology_power_control_bot + ``` + +2. Установите зависимости: + ```bash + pip install -r requirements.txt + ``` + +3. Настройте параметры в файле `.env`: + ``` + # Telegram Bot Configuration + TELEGRAM_TOKEN=your_telegram_bot_token + ADMIN_USER_IDS=123456789,987654321 + + # Synology NAS Configuration + SYNOLOGY_HOST=192.168.1.100 + SYNOLOGY_PORT=5000 + SYNOLOGY_USERNAME=admin + SYNOLOGY_PASSWORD=your_password + SYNOLOGY_SECURE=False + SYNOLOGY_TIMEOUT=10 + + # Wake-on-LAN Configuration + SYNOLOGY_MAC=00:11:22:33:44:55 + WOL_PORT=9 + ``` + +4. Запустите бота: + ```bash + python run.py + ``` + +### Метод 2: Docker + +1. Убедитесь, что Docker и Docker Compose установлены в вашей системе. + +2. Клонируйте репозиторий: + ```bash + git clone https://github.com/yourusername/synology_power_control_bot.git + cd synology_power_control_bot + ``` + +3. Создайте файл `.env` на основе `.env-example` и заполните необходимыми значениями. + +4. Запустите скрипт развертывания: + ```bash + # Linux/macOS + chmod +x deploy.sh + ./deploy.sh + + # Windows + deploy.cmd + ``` + + Или запустите вручную: + ```bash + docker-compose up -d --build + ``` + +5. Проверьте статус: + ```bash + docker-compose ps + ``` + +6. Просмотр логов: + ```bash + docker-compose logs -f + ``` + +Дополнительная информация о Docker-развертывании доступна в файле [README_DOCKER.md](README_DOCKER.md). + +## Подготовка Synology NAS + +1. Включите Wake-on-LAN в настройках DSM: + - Панель управления > Сеть > Общие > Wake-on-LAN + +2. Убедитесь, что API DSM включено: + - Панель управления > Службы терминала и SNMP > Включить DSM API + +3. Узнайте MAC-адрес вашего NAS: + - Панель управления > Сеть > Сетевой интерфейс + +## Команды бота + +### Основные команды +- `/start` - Начало работы с ботом +- `/status` - Проверка текущего статуса NAS +- `/power` - Управление питанием NAS (включение, выключение, перезагрузка) +- `/help` - Вывод справочной информации + +### Информационные команды +- `/system` - Подробная информация о системе +- `/storage` - Информация о хранилище и дисках +- `/shares` - Список общих папок +- `/load` - Текущая нагрузка на систему +- `/security` - Статус безопасности системы +- `/temperature` - Температура устройства +- `/processes` - Список активных процессов +- `/network` - Сетевая информация + +### Расширенные команды +- `/schedule` - Расписание питания +- `/browse` - Просмотр файлов +- `/search <запрос>` - Поиск файлов +- `/updates` - Проверка обновлений +- `/backup` - Статус резервного копирования +- `/quota` - Квоты пользователей + +### Быстрые команды +- `/quickreboot` - Быстрая перезагрузка +- `/wakeup` - Пробуждение NAS (WOL) + +## Структура проекта + +``` +synology_power_control_bot/ +├── logs/ # Директория для логов +├── src/ # Исходный код +│ ├── api/ # Модули для работы с API +│ │ └── synology.py # API для работы с Synology NAS +│ ├── config/ # Модули конфигурации +│ │ └── config.py # Основная конфигурация +│ ├── handlers/ # Обработчики команд бота +│ │ └── command_handlers.py +│ ├── utils/ # Вспомогательные утилиты +│ │ └── logger.py # Настройка логирования +│ └── bot.py # Основной файл запуска бота +├── .env # Файл с переменными окружения +├── .env-example # Пример файла переменных окружения +├── .gitignore # Файл игнорирования Git +├── .dockerignore # Файлы, игнорируемые при сборке Docker-образа +├── Dockerfile # Инструкции для сборки Docker-образа +├── docker-compose.yml # Конфигурация Docker Compose +├── deploy.sh # Скрипт развёртывания для Linux +├── deploy.cmd # Скрипт развёртывания для Windows +├── entrypoint.sh # Скрипт для запуска в Docker +├── README.md # Основная документация +├── README_DOCKER.md # Документация по Docker-развёртыванию +└── requirements.txt # Зависимости проекта +``` + +## Безопасность + +Бот имеет систему авторизации на основе ID пользователей Telegram. Убедитесь, что указали правильные ID в переменной `ADMIN_USER_IDS` в файле `.env`. + +## Устранение неисправностей + +### Проблемы с подключением к Synology NAS + +1. Убедитесь, что NAS доступен по сети (можно проверить с помощью команды `ping`). +2. Проверьте правильность логина и пароля в `.env`. +3. Убедитесь, что DSM API включено в настройках NAS. + +### Проблемы с Docker + +1. Проверьте статус контейнера: `docker-compose ps` +2. Просмотрите логи: `docker-compose logs -f` +3. Перезапустите контейнер: `docker-compose restart` +4. Проверьте состояние здоровья: `docker inspect --format="{{json .State.Health}}" synology-power-control-bot` +5. Проверьте, что все переменные окружения корректно переданы в контейнер. + +### Обновление в Docker + +Для обновления бота в Docker: + +1. Остановите контейнеры: + ```bash + docker-compose down + ``` + +2. Загрузите обновления (если используете Git): + ```bash + git pull + ``` + +3. Запустите контейнеры заново: + ```bash + docker-compose up -d --build + ``` + +## Лицензия + +MIT diff --git a/.history/README_20250830103154.md b/.history/README_20250830103154.md new file mode 100644 index 0000000..473468e --- /dev/null +++ b/.history/README_20250830103154.md @@ -0,0 +1,236 @@ +# Synology Power Control Bot + +Telegram-бот для удаленного управления питанием сетевого хранилища Synology NAS (DS223j и другие модели). + +## Возможности + +### Управление питанием +- ✅ Включение питания через Wake-on-LAN +- ✅ Выключение питания через API DSM +- ✅ Перезагрузка системы с отслеживанием статуса + +### Мониторинг системы +- ✅ Проверка онлайн статуса NAS +- ✅ Информация о системе (модель, версия DSM, время работы) +- ✅ Мониторинг загрузки CPU и памяти +- ✅ Данные о температуре и сетевой активности +- ✅ Статус хранилища и дисков +- ✅ Информация о безопасности системы +- ✅ Список активных процессов +- ✅ Мониторинг сетевых подключений + +### Управление данными +- ✅ Просмотр списка общих папок +- ✅ Информация о томах и дисках +- ✅ Статистика использования дисков +- ✅ Просмотр файлов и папок +- ✅ Поиск файлов +- ✅ Мониторинг квот пользователей + +### Безопасность +- ✅ Ограничение доступа по ID пользователей Telegram +- ✅ Безопасное хранение учетных данных + +### Дополнительные функции +- ✅ Мониторинг обновлений DSM и пакетов +- ✅ Управление расписанием питания +- ✅ Проверка статуса резервного копирования + +## Требования + +- Python 3.8+ +- Synology NAS с включенным WoL +- Учетная запись администратора DSM +- Telegram Bot API Token +- Доступ к порту API Synology DSM (обычно 5000 или 5001) + +## Установка и настройка + +### Метод 1: Локальный запуск + +1. Клонируйте репозиторий: + ```bash + git clone https://github.com/yourusername/synology_power_control_bot.git + cd synology_power_control_bot + ``` + +2. Установите зависимости: + ```bash + pip install -r requirements.txt + ``` + +3. Настройте параметры в файле `.env`: + ``` + # Telegram Bot Configuration + TELEGRAM_TOKEN=your_telegram_bot_token + ADMIN_USER_IDS=123456789,987654321 + + # Synology NAS Configuration + SYNOLOGY_HOST=192.168.1.100 + SYNOLOGY_PORT=5000 + SYNOLOGY_USERNAME=admin + SYNOLOGY_PASSWORD=your_password + SYNOLOGY_SECURE=False + SYNOLOGY_TIMEOUT=10 + + # Wake-on-LAN Configuration + SYNOLOGY_MAC=00:11:22:33:44:55 + WOL_PORT=9 + ``` + +4. Запустите бота: + ```bash + python run.py + ``` + +### Метод 2: Docker + +1. Убедитесь, что Docker и Docker Compose установлены в вашей системе. + +2. Клонируйте репозиторий: + ```bash + git clone https://github.com/yourusername/synology_power_control_bot.git + cd synology_power_control_bot + ``` + +3. Создайте файл `.env` на основе `.env-example` и заполните необходимыми значениями. + +4. Запустите скрипт развертывания: + ```bash + # Linux/macOS + chmod +x deploy.sh + ./deploy.sh + + # Windows + deploy.cmd + ``` + + Или запустите вручную: + ```bash + docker-compose up -d --build + ``` + +5. Проверьте статус: + ```bash + docker-compose ps + ``` + +6. Просмотр логов: + ```bash + docker-compose logs -f + ``` + +Дополнительная информация о Docker-развертывании доступна в файле [README_DOCKER.md](README_DOCKER.md). + +## Подготовка Synology NAS + +1. Включите Wake-on-LAN в настройках DSM: + - Панель управления > Сеть > Общие > Wake-on-LAN + +2. Убедитесь, что API DSM включено: + - Панель управления > Службы терминала и SNMP > Включить DSM API + +3. Узнайте MAC-адрес вашего NAS: + - Панель управления > Сеть > Сетевой интерфейс + +## Команды бота + +### Основные команды +- `/start` - Начало работы с ботом +- `/status` - Проверка текущего статуса NAS +- `/power` - Управление питанием NAS (включение, выключение, перезагрузка) +- `/help` - Вывод справочной информации + +### Информационные команды +- `/system` - Подробная информация о системе +- `/storage` - Информация о хранилище и дисках +- `/shares` - Список общих папок +- `/load` - Текущая нагрузка на систему +- `/security` - Статус безопасности системы +- `/temperature` - Температура устройства +- `/processes` - Список активных процессов +- `/network` - Сетевая информация + +### Расширенные команды +- `/schedule` - Расписание питания +- `/browse` - Просмотр файлов +- `/search <запрос>` - Поиск файлов +- `/updates` - Проверка обновлений +- `/backup` - Статус резервного копирования +- `/quota` - Квоты пользователей + +### Быстрые команды +- `/quickreboot` - Быстрая перезагрузка +- `/wakeup` - Пробуждение NAS (WOL) + +## Структура проекта + +``` +synology_power_control_bot/ +├── logs/ # Директория для логов +├── src/ # Исходный код +│ ├── api/ # Модули для работы с API +│ │ └── synology.py # API для работы с Synology NAS +│ ├── config/ # Модули конфигурации +│ │ └── config.py # Основная конфигурация +│ ├── handlers/ # Обработчики команд бота +│ │ └── command_handlers.py +│ ├── utils/ # Вспомогательные утилиты +│ │ └── logger.py # Настройка логирования +│ └── bot.py # Основной файл запуска бота +├── .env # Файл с переменными окружения +├── .env-example # Пример файла переменных окружения +├── .gitignore # Файл игнорирования Git +├── .dockerignore # Файлы, игнорируемые при сборке Docker-образа +├── Dockerfile # Инструкции для сборки Docker-образа +├── docker-compose.yml # Конфигурация Docker Compose +├── deploy.sh # Скрипт развёртывания для Linux +├── deploy.cmd # Скрипт развёртывания для Windows +├── entrypoint.sh # Скрипт для запуска в Docker +├── README.md # Основная документация +├── README_DOCKER.md # Документация по Docker-развёртыванию +└── requirements.txt # Зависимости проекта +``` + +## Безопасность + +Бот имеет систему авторизации на основе ID пользователей Telegram. Убедитесь, что указали правильные ID в переменной `ADMIN_USER_IDS` в файле `.env`. + +## Устранение неисправностей + +### Проблемы с подключением к Synology NAS + +1. Убедитесь, что NAS доступен по сети (можно проверить с помощью команды `ping`). +2. Проверьте правильность логина и пароля в `.env`. +3. Убедитесь, что DSM API включено в настройках NAS. + +### Проблемы с Docker + +1. Проверьте статус контейнера: `docker-compose ps` +2. Просмотрите логи: `docker-compose logs -f` +3. Перезапустите контейнер: `docker-compose restart` +4. Проверьте состояние здоровья: `docker inspect --format="{{json .State.Health}}" synology-power-control-bot` +5. Проверьте, что все переменные окружения корректно переданы в контейнер. + +### Обновление в Docker + +Для обновления бота в Docker: + +1. Остановите контейнеры: + ```bash + docker-compose down + ``` + +2. Загрузите обновления (если используете Git): + ```bash + git pull + ``` + +3. Запустите контейнеры заново: + ```bash + docker-compose up -d --build + ``` + +## Лицензия + +MIT diff --git a/.history/README_DOCKER_20250830102736.md b/.history/README_DOCKER_20250830102736.md new file mode 100644 index 0000000..6bd6f3f --- /dev/null +++ b/.history/README_DOCKER_20250830102736.md @@ -0,0 +1,106 @@ +# Synology Power Control Bot - Docker Deployment + +## Подготовка к развертыванию + +Перед развертыванием в Docker убедитесь, что: + +1. Docker и Docker Compose установлены в вашей системе. +2. Файл `.env` настроен с правильными значениями. + +## Структура проекта для Docker + +``` +synology_power_control_bot/ +├── src/ # Исходный код бота +├── logs/ # Папка для логов (будет смонтирована как том) +├── .env # Файл с переменными окружения +├── requirements.txt # Зависимости Python +├── Dockerfile # Инструкции для сборки образа +├── docker-compose.yml # Конфигурация Docker Compose +└── run.py # Точка входа +``` + +## Настройка переменных окружения + +Убедитесь, что файл `.env` содержит все необходимые переменные: + +``` +# Telegram Bot API +TELEGRAM_TOKEN=your_telegram_bot_token +ADMIN_USER_IDS=123456789,987654321 # ID пользователей-администраторов через запятую + +# Synology NAS +SYNOLOGY_HOST=192.168.1.100 +SYNOLOGY_PORT=5000 # Обычно 5000 для HTTP и 5001 для HTTPS +SYNOLOGY_USERNAME=your_username +SYNOLOGY_PASSWORD=your_password +SYNOLOGY_SECURE=True # Использовать HTTPS +SYNOLOGY_VERIFY_SSL=False # Проверка SSL-сертификата +SYNOLOGY_TIMEOUT=10 # Таймаут для API запросов в секундах +SYNOLOGY_API_VERSION=1 # Версия API +SYNOLOGY_POWER_API=SYNO.Core.System # API для управления питанием + +# WOL (Wake-on-LAN) +MAC_ADDRESS=00:11:22:33:44:55 # MAC-адрес Synology NAS +WOL_BROADCAST=255.255.255.255 # Broadcast-адрес для WOL +WOL_PORT=9 # Порт для WOL (обычно 7 или 9) + +# Logging +LOG_LEVEL=INFO +``` + +## Сборка и запуск + +### Сборка и запуск контейнеров + +```bash +docker-compose up -d --build +``` + +### Просмотр логов + +```bash +docker-compose logs -f +``` + +### Остановка контейнеров + +```bash +docker-compose down +``` + +## Обновление + +Для обновления бота: + +1. Остановите контейнеры: + ```bash + docker-compose down + ``` + +2. Скачайте последние изменения (если используете Git): + ```bash + git pull + ``` + +3. Соберите и запустите контейнеры заново: + ```bash + docker-compose up -d --build + ``` + +## Устранение неполадок + +### Проверка статуса контейнера +```bash +docker-compose ps +``` + +### Проверка логов контейнера +```bash +docker-compose logs -f synology-bot +``` + +### Подключение к контейнеру +```bash +docker-compose exec synology-bot bash +``` diff --git a/.history/README_DOCKER_20250830103154.md b/.history/README_DOCKER_20250830103154.md new file mode 100644 index 0000000..6bd6f3f --- /dev/null +++ b/.history/README_DOCKER_20250830103154.md @@ -0,0 +1,106 @@ +# Synology Power Control Bot - Docker Deployment + +## Подготовка к развертыванию + +Перед развертыванием в Docker убедитесь, что: + +1. Docker и Docker Compose установлены в вашей системе. +2. Файл `.env` настроен с правильными значениями. + +## Структура проекта для Docker + +``` +synology_power_control_bot/ +├── src/ # Исходный код бота +├── logs/ # Папка для логов (будет смонтирована как том) +├── .env # Файл с переменными окружения +├── requirements.txt # Зависимости Python +├── Dockerfile # Инструкции для сборки образа +├── docker-compose.yml # Конфигурация Docker Compose +└── run.py # Точка входа +``` + +## Настройка переменных окружения + +Убедитесь, что файл `.env` содержит все необходимые переменные: + +``` +# Telegram Bot API +TELEGRAM_TOKEN=your_telegram_bot_token +ADMIN_USER_IDS=123456789,987654321 # ID пользователей-администраторов через запятую + +# Synology NAS +SYNOLOGY_HOST=192.168.1.100 +SYNOLOGY_PORT=5000 # Обычно 5000 для HTTP и 5001 для HTTPS +SYNOLOGY_USERNAME=your_username +SYNOLOGY_PASSWORD=your_password +SYNOLOGY_SECURE=True # Использовать HTTPS +SYNOLOGY_VERIFY_SSL=False # Проверка SSL-сертификата +SYNOLOGY_TIMEOUT=10 # Таймаут для API запросов в секундах +SYNOLOGY_API_VERSION=1 # Версия API +SYNOLOGY_POWER_API=SYNO.Core.System # API для управления питанием + +# WOL (Wake-on-LAN) +MAC_ADDRESS=00:11:22:33:44:55 # MAC-адрес Synology NAS +WOL_BROADCAST=255.255.255.255 # Broadcast-адрес для WOL +WOL_PORT=9 # Порт для WOL (обычно 7 или 9) + +# Logging +LOG_LEVEL=INFO +``` + +## Сборка и запуск + +### Сборка и запуск контейнеров + +```bash +docker-compose up -d --build +``` + +### Просмотр логов + +```bash +docker-compose logs -f +``` + +### Остановка контейнеров + +```bash +docker-compose down +``` + +## Обновление + +Для обновления бота: + +1. Остановите контейнеры: + ```bash + docker-compose down + ``` + +2. Скачайте последние изменения (если используете Git): + ```bash + git pull + ``` + +3. Соберите и запустите контейнеры заново: + ```bash + docker-compose up -d --build + ``` + +## Устранение неполадок + +### Проверка статуса контейнера +```bash +docker-compose ps +``` + +### Проверка логов контейнера +```bash +docker-compose logs -f synology-bot +``` + +### Подключение к контейнеру +```bash +docker-compose exec synology-bot bash +``` diff --git a/.history/deploy_20250830102934.sh b/.history/deploy_20250830102934.sh new file mode 100644 index 0000000..fc7e1aa --- /dev/null +++ b/.history/deploy_20250830102934.sh @@ -0,0 +1,49 @@ +#!/bin/bash +# deploy.sh - Скрипт для развертывания Synology Power Control Bot + +# Цвета для вывода +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +RED='\033[0;31m' +NC='\033[0m' # No Color + +# Проверяем наличие Docker и Docker Compose +echo -e "${YELLOW}Проверка наличия Docker...${NC}" +if ! [ -x "$(command -v docker)" ]; then + echo -e "${RED}Ошибка: Docker не установлен.${NC}" >&2 + echo -e "Установите Docker, следуя инструкциям: https://docs.docker.com/get-docker/" + exit 1 +fi + +echo -e "${YELLOW}Проверка наличия Docker Compose...${NC}" +if ! [ -x "$(command -v docker-compose)" ] && ! [ -x "$(command -v docker compose)" ]; then + echo -e "${RED}Ошибка: Docker Compose не установлен.${NC}" >&2 + echo -e "Установите Docker Compose, следуя инструкциям: https://docs.docker.com/compose/install/" + exit 1 +fi + +# Проверяем наличие файла .env +echo -e "${YELLOW}Проверка файла .env...${NC}" +if [ ! -f ".env" ]; then + echo -e "${RED}Ошибка: Файл .env не найден.${NC}" >&2 + echo -e "Создайте файл .env с необходимыми переменными окружения." + exit 1 +fi + +# Создаем директорию для логов +echo -e "${YELLOW}Создание директории для логов...${NC}" +mkdir -p logs +chmod 777 logs + +# Сборка и запуск Docker контейнеров +echo -e "${YELLOW}Сборка и запуск Docker контейнеров...${NC}" +docker-compose down +docker-compose up -d --build + +# Проверка статуса контейнеров +echo -e "${YELLOW}Проверка статуса контейнеров...${NC}" +docker-compose ps + +echo -e "${GREEN}Развертывание завершено успешно!${NC}" +echo -e "Для просмотра логов: ${YELLOW}docker-compose logs -f${NC}" +echo -e "Для остановки: ${YELLOW}docker-compose down${NC}" diff --git a/.history/deploy_20250830102949.cmd b/.history/deploy_20250830102949.cmd new file mode 100644 index 0000000..6ded549 --- /dev/null +++ b/.history/deploy_20250830102949.cmd @@ -0,0 +1,45 @@ +@echo off +REM deploy.cmd - Скрипт для развертывания Synology Power Control Bot на Windows + +echo Проверка наличия Docker... +where docker >nul 2>&1 +if %ERRORLEVEL% NEQ 0 ( + echo Ошибка: Docker не установлен. + echo Установите Docker Desktop, следуя инструкциям: https://docs.docker.com/desktop/windows/install/ + exit /b 1 +) + +echo Проверка наличия Docker Compose... +where docker-compose >nul 2>&1 +if %ERRORLEVEL% NEQ 0 ( + docker compose version >nul 2>&1 + if %ERRORLEVEL% NEQ 0 ( + echo Ошибка: Docker Compose не установлен. + echo Установите Docker Desktop, следуя инструкциям: https://docs.docker.com/desktop/windows/install/ + exit /b 1 + ) +) + +echo Проверка файла .env... +if not exist .env ( + echo Ошибка: Файл .env не найден. + echo Создайте файл .env с необходимыми переменными окружения. + exit /b 1 +) + +echo Создание директории для логов... +if not exist logs mkdir logs + +echo Сборка и запуск Docker контейнеров... +docker-compose down +docker-compose up -d --build + +echo Проверка статуса контейнеров... +docker-compose ps + +echo. +echo Развертывание завершено успешно! +echo Для просмотра логов: docker-compose logs -f +echo Для остановки: docker-compose down + +pause diff --git a/.history/deploy_20250830103154.cmd b/.history/deploy_20250830103154.cmd new file mode 100644 index 0000000..6ded549 --- /dev/null +++ b/.history/deploy_20250830103154.cmd @@ -0,0 +1,45 @@ +@echo off +REM deploy.cmd - Скрипт для развертывания Synology Power Control Bot на Windows + +echo Проверка наличия Docker... +where docker >nul 2>&1 +if %ERRORLEVEL% NEQ 0 ( + echo Ошибка: Docker не установлен. + echo Установите Docker Desktop, следуя инструкциям: https://docs.docker.com/desktop/windows/install/ + exit /b 1 +) + +echo Проверка наличия Docker Compose... +where docker-compose >nul 2>&1 +if %ERRORLEVEL% NEQ 0 ( + docker compose version >nul 2>&1 + if %ERRORLEVEL% NEQ 0 ( + echo Ошибка: Docker Compose не установлен. + echo Установите Docker Desktop, следуя инструкциям: https://docs.docker.com/desktop/windows/install/ + exit /b 1 + ) +) + +echo Проверка файла .env... +if not exist .env ( + echo Ошибка: Файл .env не найден. + echo Создайте файл .env с необходимыми переменными окружения. + exit /b 1 +) + +echo Создание директории для логов... +if not exist logs mkdir logs + +echo Сборка и запуск Docker контейнеров... +docker-compose down +docker-compose up -d --build + +echo Проверка статуса контейнеров... +docker-compose ps + +echo. +echo Развертывание завершено успешно! +echo Для просмотра логов: docker-compose logs -f +echo Для остановки: docker-compose down + +pause diff --git a/.history/deploy_20250830103154.sh b/.history/deploy_20250830103154.sh new file mode 100644 index 0000000..fc7e1aa --- /dev/null +++ b/.history/deploy_20250830103154.sh @@ -0,0 +1,49 @@ +#!/bin/bash +# deploy.sh - Скрипт для развертывания Synology Power Control Bot + +# Цвета для вывода +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +RED='\033[0;31m' +NC='\033[0m' # No Color + +# Проверяем наличие Docker и Docker Compose +echo -e "${YELLOW}Проверка наличия Docker...${NC}" +if ! [ -x "$(command -v docker)" ]; then + echo -e "${RED}Ошибка: Docker не установлен.${NC}" >&2 + echo -e "Установите Docker, следуя инструкциям: https://docs.docker.com/get-docker/" + exit 1 +fi + +echo -e "${YELLOW}Проверка наличия Docker Compose...${NC}" +if ! [ -x "$(command -v docker-compose)" ] && ! [ -x "$(command -v docker compose)" ]; then + echo -e "${RED}Ошибка: Docker Compose не установлен.${NC}" >&2 + echo -e "Установите Docker Compose, следуя инструкциям: https://docs.docker.com/compose/install/" + exit 1 +fi + +# Проверяем наличие файла .env +echo -e "${YELLOW}Проверка файла .env...${NC}" +if [ ! -f ".env" ]; then + echo -e "${RED}Ошибка: Файл .env не найден.${NC}" >&2 + echo -e "Создайте файл .env с необходимыми переменными окружения." + exit 1 +fi + +# Создаем директорию для логов +echo -e "${YELLOW}Создание директории для логов...${NC}" +mkdir -p logs +chmod 777 logs + +# Сборка и запуск Docker контейнеров +echo -e "${YELLOW}Сборка и запуск Docker контейнеров...${NC}" +docker-compose down +docker-compose up -d --build + +# Проверка статуса контейнеров +echo -e "${YELLOW}Проверка статуса контейнеров...${NC}" +docker-compose ps + +echo -e "${GREEN}Развертывание завершено успешно!${NC}" +echo -e "Для просмотра логов: ${YELLOW}docker-compose logs -f${NC}" +echo -e "Для остановки: ${YELLOW}docker-compose down${NC}" diff --git a/.history/diagnose_api_20250830081925.py b/.history/diagnose_api_20250830081925.py new file mode 100644 index 0000000..2ae4b6a --- /dev/null +++ b/.history/diagnose_api_20250830081925.py @@ -0,0 +1,91 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Диагностический скрипт для определения совместимых API +""" + +import os +import sys +import logging +import argparse +from pathlib import Path + +# Добавляем родительскую директорию в sys.path +parent_dir = str(Path(__file__).resolve().parent.parent) +if parent_dir not in sys.path: + sys.path.insert(0, parent_dir) + +from src.api.api_discovery import discover_available_apis +from src.config.config import SYNOLOGY_HOST, SYNOLOGY_PORT, SYNOLOGY_SECURE + +# Настройка логгера +logging.basicConfig( + level=logging.DEBUG, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' +) +logger = logging.getLogger(__name__) + +def main(): + """Точка входа для диагностического скрипта""" + parser = argparse.ArgumentParser(description='Synology API Diagnostic Tool') + parser.add_argument('--host', help='Synology host address', default=SYNOLOGY_HOST) + parser.add_argument('--port', type=int, help='Synology host port', default=SYNOLOGY_PORT) + parser.add_argument('--secure', action='store_true', help='Use HTTPS', default=SYNOLOGY_SECURE) + + args = parser.parse_args() + + protocol = "https" if args.secure else "http" + base_url = f"{protocol}://{args.host}:{args.port}/webapi" + + print(f"Scanning APIs at {base_url}...") + + apis = discover_available_apis(base_url) + + if not apis: + print("No APIs were discovered. Check connection parameters.") + return + + print(f"Discovered {len(apis)} APIs") + + # Анализ результатов + + # 1. Ищем API для управления питанием + print("\nPower Management APIs:") + power_apis = [name for name in apis.keys() if "power" in name.lower()] + for api in power_apis: + info = apis[api] + print(f" - {api} (v{info.get('minVersion', 1)}-{info.get('maxVersion', 1)}), path: {info.get('path', 'entry.cgi')}") + + # 2. Ищем API для информации о системе + print("\nSystem Information APIs:") + system_info_apis = [name for name in apis.keys() if ("system" in name.lower() or "dsm" in name.lower()) and "info" in name.lower()] + for api in system_info_apis: + info = apis[api] + print(f" - {api} (v{info.get('minVersion', 1)}-{info.get('maxVersion', 1)}), path: {info.get('path', 'entry.cgi')}") + + # 3. Ищем API для перезагрузки + print("\nReboot/Restart APIs:") + reboot_apis = [name for name in apis.keys() if any(word in name.lower() for word in ["restart", "reboot", "power"])] + for api in reboot_apis: + info = apis[api] + print(f" - {api} (v{info.get('minVersion', 1)}-{info.get('maxVersion', 1)}), path: {info.get('path', 'entry.cgi')}") + + print("\nRecommended API Settings:") + + if power_apis: + recommended_power_api = max(power_apis, key=lambda x: apis[x].get('maxVersion', 1)) + print(f"Power API: {recommended_power_api}, version: {apis[recommended_power_api].get('maxVersion', 1)}") + else: + print("Power API: Not found, falling back to SYNO.Core.System") + + if system_info_apis: + recommended_info_api = max(system_info_apis, key=lambda x: apis[x].get('maxVersion', 1)) + print(f"System Info API: {recommended_info_api}, version: {apis[recommended_info_api].get('maxVersion', 1)}") + else: + print("System Info API: Not found, falling back to SYNO.DSM.Info") + + print("\nThese settings should be added to your .env file.") + +if __name__ == "__main__": + main() diff --git a/.history/diagnose_api_20250830081957.py b/.history/diagnose_api_20250830081957.py new file mode 100644 index 0000000..2ae4b6a --- /dev/null +++ b/.history/diagnose_api_20250830081957.py @@ -0,0 +1,91 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Диагностический скрипт для определения совместимых API +""" + +import os +import sys +import logging +import argparse +from pathlib import Path + +# Добавляем родительскую директорию в sys.path +parent_dir = str(Path(__file__).resolve().parent.parent) +if parent_dir not in sys.path: + sys.path.insert(0, parent_dir) + +from src.api.api_discovery import discover_available_apis +from src.config.config import SYNOLOGY_HOST, SYNOLOGY_PORT, SYNOLOGY_SECURE + +# Настройка логгера +logging.basicConfig( + level=logging.DEBUG, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' +) +logger = logging.getLogger(__name__) + +def main(): + """Точка входа для диагностического скрипта""" + parser = argparse.ArgumentParser(description='Synology API Diagnostic Tool') + parser.add_argument('--host', help='Synology host address', default=SYNOLOGY_HOST) + parser.add_argument('--port', type=int, help='Synology host port', default=SYNOLOGY_PORT) + parser.add_argument('--secure', action='store_true', help='Use HTTPS', default=SYNOLOGY_SECURE) + + args = parser.parse_args() + + protocol = "https" if args.secure else "http" + base_url = f"{protocol}://{args.host}:{args.port}/webapi" + + print(f"Scanning APIs at {base_url}...") + + apis = discover_available_apis(base_url) + + if not apis: + print("No APIs were discovered. Check connection parameters.") + return + + print(f"Discovered {len(apis)} APIs") + + # Анализ результатов + + # 1. Ищем API для управления питанием + print("\nPower Management APIs:") + power_apis = [name for name in apis.keys() if "power" in name.lower()] + for api in power_apis: + info = apis[api] + print(f" - {api} (v{info.get('minVersion', 1)}-{info.get('maxVersion', 1)}), path: {info.get('path', 'entry.cgi')}") + + # 2. Ищем API для информации о системе + print("\nSystem Information APIs:") + system_info_apis = [name for name in apis.keys() if ("system" in name.lower() or "dsm" in name.lower()) and "info" in name.lower()] + for api in system_info_apis: + info = apis[api] + print(f" - {api} (v{info.get('minVersion', 1)}-{info.get('maxVersion', 1)}), path: {info.get('path', 'entry.cgi')}") + + # 3. Ищем API для перезагрузки + print("\nReboot/Restart APIs:") + reboot_apis = [name for name in apis.keys() if any(word in name.lower() for word in ["restart", "reboot", "power"])] + for api in reboot_apis: + info = apis[api] + print(f" - {api} (v{info.get('minVersion', 1)}-{info.get('maxVersion', 1)}), path: {info.get('path', 'entry.cgi')}") + + print("\nRecommended API Settings:") + + if power_apis: + recommended_power_api = max(power_apis, key=lambda x: apis[x].get('maxVersion', 1)) + print(f"Power API: {recommended_power_api}, version: {apis[recommended_power_api].get('maxVersion', 1)}") + else: + print("Power API: Not found, falling back to SYNO.Core.System") + + if system_info_apis: + recommended_info_api = max(system_info_apis, key=lambda x: apis[x].get('maxVersion', 1)) + print(f"System Info API: {recommended_info_api}, version: {apis[recommended_info_api].get('maxVersion', 1)}") + else: + print("System Info API: Not found, falling back to SYNO.DSM.Info") + + print("\nThese settings should be added to your .env file.") + +if __name__ == "__main__": + main() diff --git a/.history/direct_api_test_20250830084231.py b/.history/direct_api_test_20250830084231.py new file mode 100644 index 0000000..ca9c245 --- /dev/null +++ b/.history/direct_api_test_20250830084231.py @@ -0,0 +1,293 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Тестовый скрипт для прямого доступа к API Synology для получения информации о системе. +Используется для отладки и определения совместимых API. +""" + +import requests +import logging +import json +import sys +import os +import urllib3 +from requests.adapters import HTTPAdapter +from urllib3.util import Retry + +# Добавляем корневой каталог в путь для импорта +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +from src.config.config import ( + SYNOLOGY_HOST, + SYNOLOGY_PORT, + SYNOLOGY_USERNAME, + SYNOLOGY_PASSWORD, + SYNOLOGY_SECURE +) + +# Отключение предупреждений о небезопасных SSL-соединениях +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + +# Настройка логирования +logging.basicConfig( + level=logging.DEBUG, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' +) +logger = logging.getLogger(__name__) + +def direct_api_test(): + """Прямой тест API без использования классов для определения проблемы""" + # Создаем базовую сессию + session = requests.Session() + session.verify = False # Отключаем проверку SSL + + # Добавляем повторные попытки для HTTP-запросов + retry_strategy = Retry( + total=3, + status_forcelist=[429, 500, 502, 503, 504], + allowed_methods=["GET", "POST"], + backoff_factor=1.0 + ) + adapter = HTTPAdapter(max_retries=retry_strategy) + session.mount("http://", adapter) + session.mount("https://", adapter) + + # Формируем базовый URL + protocol = "https" if SYNOLOGY_SECURE else "http" + base_url = f"{protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi" + logger.info(f"Тестирование прямого API доступа к {base_url}") + + # Шаг 1: Авторизация + logger.info("Шаг 1: Попытка авторизации...") + + # Сначала получаем информацию об API авторизации + api_info_url = f"{base_url}/entry.cgi" + api_info_params = { + "api": "SYNO.API.Info", + "version": "1", + "method": "query", + "query": "SYNO.API.Auth" + } + + try: + auth_info_response = session.get(api_info_url, params=api_info_params, timeout=10) + auth_info_data = auth_info_response.json() + + if auth_info_data.get("success"): + auth_info = auth_info_data.get("data", {}).get("SYNO.API.Auth", {}) + auth_path = auth_info.get("path", "auth.cgi") + auth_max_version = auth_info.get("maxVersion", 6) + + logger.info(f"API авторизации: путь={auth_path}, макс. версия={auth_max_version}") + + # Пробуем версию 6 или максимальную доступную + auth_version = min(6, auth_max_version) + + # Выполняем авторизацию + auth_url = f"{base_url}/{auth_path}" + auth_params = { + "api": "SYNO.API.Auth", + "version": str(auth_version), + "method": "login", + "account": SYNOLOGY_USERNAME, + "passwd": SYNOLOGY_PASSWORD, + "session": "DirectApiTest", + "format": "cookie" + } + + # Для версии 6+ используем немного другой формат + if auth_version >= 6: + auth_params["enable_syno_token"] = "yes" + + logger.info(f"Авторизация с использованием SYNO.API.Auth v{auth_version}") + auth_response = session.get(auth_url, params=auth_params, timeout=10) + auth_data = auth_response.json() + + if auth_data.get("success"): + sid = auth_data.get("data", {}).get("sid") + logger.info(f"Авторизация успешна! SID: {sid[:10]}...") + + # Шаг 2: Тестирование различных API для получения информации о системе + logger.info("Шаг 2: Тестирование различных API для получения информации о системе") + + # Создаем список API для тестирования + api_to_test = [ + {"name": "SYNO.DSM.Info", "method": "getinfo", "version": 1}, + {"name": "SYNO.DSM.Info", "method": "getinfo", "version": 2}, + {"name": "SYNO.Core.System", "method": "info", "version": 1}, + {"name": "SYNO.Core.System", "method": "info", "version": 2}, + {"name": "SYNO.Core.System.Status", "method": "get", "version": 1}, + {"name": "SYNO.Core.System.Status", "method": "get", "version": 2}, + {"name": "SYNO.Core.System.Utilization", "method": "get", "version": 1}, + {"name": "SYNO.Core.CurrentConnection", "method": "list", "version": 1} + ] + + # Перебираем все API и тестируем их + for api in api_to_test: + # Сначала получаем информацию о конкретном API + try: + api_info_params = { + "api": "SYNO.API.Info", + "version": "1", + "method": "query", + "query": api["name"] + } + + api_info_resp = session.get(api_info_url, params=api_info_params, timeout=10) + api_info_data = api_info_resp.json() + + if api_info_data.get("success") and api["name"] in api_info_data.get("data", {}): + api_details = api_info_data["data"][api["name"]] + api_path = api_details.get("path", "entry.cgi") + api_min_version = api_details.get("minVersion", 1) + api_max_version = api_details.get("maxVersion", 1) + + # Проверяем, поддерживается ли указанная версия + if api["version"] < api_min_version: + logger.warning(f"{api['name']} v{api['version']} ниже минимальной {api_min_version}, используем {api_min_version}") + test_version = api_min_version + elif api["version"] > api_max_version: + logger.warning(f"{api['name']} v{api['version']} выше максимальной {api_max_version}, используем {api_max_version}") + test_version = api_max_version + else: + test_version = api["version"] + + # Выполняем запрос API + test_url = f"{base_url}/{api_path}" + test_params = { + "api": api["name"], + "version": str(test_version), + "method": api["method"], + "_sid": sid # Используем sid для аутентификации + } + + logger.info(f"Тестирование {api['name']}.{api['method']} v{test_version}") + test_response = session.get(test_url, params=test_params, timeout=10) + test_data = test_response.json() + + if test_data.get("success"): + logger.info(f"API {api['name']}.{api['method']} v{test_version} РАБОТАЕТ!") + logger.info(f"Результат: {json.dumps(test_data.get('data', {}), indent=2)[:200]}...") + else: + error_code = test_data.get("error", {}).get("code", -1) + logger.error(f"API {api['name']}.{api['method']} v{test_version} ОШИБКА: {error_code}") + + # Если ошибка связана с сессией, попробуем еще раз авторизоваться + if error_code == 119: # Session timeout + logger.info("Повторная авторизация из-за ошибки 119...") + + # Создаем новую сессию + new_session = requests.Session() + new_session.verify = False + + auth_response = new_session.get(auth_url, params=auth_params, timeout=10) + auth_data = auth_response.json() + + if auth_data.get("success"): + new_sid = auth_data.get("data", {}).get("sid") + logger.info(f"Повторная авторизация успешна! Новый SID: {new_sid[:10]}...") + + # Пробуем запрос с новым SID + test_params["_sid"] = new_sid + logger.info(f"Повторное тестирование {api['name']}.{api['method']} v{test_version}") + test_response = new_session.get(test_url, params=test_params, timeout=10) + test_data = test_response.json() + + if test_data.get("success"): + logger.info(f"API {api['name']}.{api['method']} v{test_version} теперь РАБОТАЕТ!") + logger.info(f"Результат с новой сессией: {json.dumps(test_data.get('data', {}), indent=2)[:200]}...") + else: + error_code = test_data.get("error", {}).get("code", -1) + logger.error(f"API {api['name']}.{api['method']} v{test_version} ВСЕ ЕЩЕ С ОШИБКОЙ: {error_code}") + else: + logger.warning(f"API {api['name']} не найден в информации API") + + except Exception as e: + logger.error(f"Ошибка при тестировании {api['name']}.{api['method']} v{api['version']}: {str(e)}") + + # Шаг 3: Тестирование комбинации запросов для решения проблемы + logger.info("Шаг 3: Тестирование комбинации запросов для решения проблемы") + + # Создаем новую сессию для каждого запроса + for api in [{"name": "SYNO.DSM.Info", "method": "getinfo", "version": 1}]: + try: + fresh_session = requests.Session() + fresh_session.verify = False + + # Авторизуемся + auth_response = fresh_session.get(auth_url, params=auth_params, timeout=10) + auth_data = auth_response.json() + + if auth_data.get("success"): + fresh_sid = auth_data.get("data", {}).get("sid") + logger.info(f"Авторизация в новой сессии успешна! SID: {fresh_sid[:10]}...") + + # Сразу же делаем запрос для получения информации в той же сессии + test_params = { + "api": api["name"], + "version": str(api["version"]), + "method": api["method"], + "_sid": fresh_sid + } + + test_url = f"{base_url}/entry.cgi" # Используем entry.cgi по умолчанию + logger.info(f"Тест в свежей сессии: {api['name']}.{api['method']} v{api['version']}") + test_response = fresh_session.get(test_url, params=test_params, timeout=10) + test_data = test_response.json() + + if test_data.get("success"): + logger.info(f"API в свежей сессии РАБОТАЕТ!") + logger.info(f"Результат: {json.dumps(test_data.get('data', {}), indent=2)[:200]}...") + else: + error_code = test_data.get("error", {}).get("code", -1) + logger.error(f"API в свежей сессии ОШИБКА: {error_code}") + except Exception as e: + logger.error(f"Ошибка при тестировании свежей сессии: {str(e)}") + + # Шаг 4: Получаем информацию об остальных API + logger.info("Шаг 4: Получаем информацию о доступных API для уточнения проблемы") + + # Запрашиваем все API из SYNO.API.Info + try: + all_api_params = { + "api": "SYNO.API.Info", + "version": "1", + "method": "query", + "query": "all" + } + + all_api_response = session.get(api_info_url, params=all_api_params, timeout=15) # Больший таймаут для большого ответа + all_api_data = all_api_response.json() + + if all_api_data.get("success"): + api_list = all_api_data.get("data", {}) + logger.info(f"Получен список всех API. Найдено {len(api_list)} API.") + + # Ищем интересующие нас API для отладки + interested_in = ["SYNO.DSM.Info", "SYNO.Core.System", "SYNO.Core.Hardware", + "SYNO.Core.System.Status", "SYNO.API.Auth"] + + logger.info("Информация о важных API:") + for api_name in interested_in: + if api_name in api_list: + logger.info(f"{api_name}: {api_list[api_name]}") + else: + logger.warning(f"API {api_name} не найден") + else: + logger.error("Не удалось получить список всех API") + except Exception as e: + logger.error(f"Ошибка при получении списка API: {str(e)}") + + else: + error_code = auth_data.get("error", {}).get("code", -1) + logger.error(f"Авторизация не удалась! Код ошибки: {error_code}") + else: + logger.error("Не удалось получить информацию об API авторизации") + + except Exception as e: + logger.error(f"Произошла ошибка при выполнении теста: {str(e)}") + +if __name__ == "__main__": + logger.info("Запуск прямого теста API Synology") + direct_api_test() diff --git a/.history/direct_api_test_20250830084257.py b/.history/direct_api_test_20250830084257.py new file mode 100644 index 0000000..ca9c245 --- /dev/null +++ b/.history/direct_api_test_20250830084257.py @@ -0,0 +1,293 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Тестовый скрипт для прямого доступа к API Synology для получения информации о системе. +Используется для отладки и определения совместимых API. +""" + +import requests +import logging +import json +import sys +import os +import urllib3 +from requests.adapters import HTTPAdapter +from urllib3.util import Retry + +# Добавляем корневой каталог в путь для импорта +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +from src.config.config import ( + SYNOLOGY_HOST, + SYNOLOGY_PORT, + SYNOLOGY_USERNAME, + SYNOLOGY_PASSWORD, + SYNOLOGY_SECURE +) + +# Отключение предупреждений о небезопасных SSL-соединениях +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + +# Настройка логирования +logging.basicConfig( + level=logging.DEBUG, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' +) +logger = logging.getLogger(__name__) + +def direct_api_test(): + """Прямой тест API без использования классов для определения проблемы""" + # Создаем базовую сессию + session = requests.Session() + session.verify = False # Отключаем проверку SSL + + # Добавляем повторные попытки для HTTP-запросов + retry_strategy = Retry( + total=3, + status_forcelist=[429, 500, 502, 503, 504], + allowed_methods=["GET", "POST"], + backoff_factor=1.0 + ) + adapter = HTTPAdapter(max_retries=retry_strategy) + session.mount("http://", adapter) + session.mount("https://", adapter) + + # Формируем базовый URL + protocol = "https" if SYNOLOGY_SECURE else "http" + base_url = f"{protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi" + logger.info(f"Тестирование прямого API доступа к {base_url}") + + # Шаг 1: Авторизация + logger.info("Шаг 1: Попытка авторизации...") + + # Сначала получаем информацию об API авторизации + api_info_url = f"{base_url}/entry.cgi" + api_info_params = { + "api": "SYNO.API.Info", + "version": "1", + "method": "query", + "query": "SYNO.API.Auth" + } + + try: + auth_info_response = session.get(api_info_url, params=api_info_params, timeout=10) + auth_info_data = auth_info_response.json() + + if auth_info_data.get("success"): + auth_info = auth_info_data.get("data", {}).get("SYNO.API.Auth", {}) + auth_path = auth_info.get("path", "auth.cgi") + auth_max_version = auth_info.get("maxVersion", 6) + + logger.info(f"API авторизации: путь={auth_path}, макс. версия={auth_max_version}") + + # Пробуем версию 6 или максимальную доступную + auth_version = min(6, auth_max_version) + + # Выполняем авторизацию + auth_url = f"{base_url}/{auth_path}" + auth_params = { + "api": "SYNO.API.Auth", + "version": str(auth_version), + "method": "login", + "account": SYNOLOGY_USERNAME, + "passwd": SYNOLOGY_PASSWORD, + "session": "DirectApiTest", + "format": "cookie" + } + + # Для версии 6+ используем немного другой формат + if auth_version >= 6: + auth_params["enable_syno_token"] = "yes" + + logger.info(f"Авторизация с использованием SYNO.API.Auth v{auth_version}") + auth_response = session.get(auth_url, params=auth_params, timeout=10) + auth_data = auth_response.json() + + if auth_data.get("success"): + sid = auth_data.get("data", {}).get("sid") + logger.info(f"Авторизация успешна! SID: {sid[:10]}...") + + # Шаг 2: Тестирование различных API для получения информации о системе + logger.info("Шаг 2: Тестирование различных API для получения информации о системе") + + # Создаем список API для тестирования + api_to_test = [ + {"name": "SYNO.DSM.Info", "method": "getinfo", "version": 1}, + {"name": "SYNO.DSM.Info", "method": "getinfo", "version": 2}, + {"name": "SYNO.Core.System", "method": "info", "version": 1}, + {"name": "SYNO.Core.System", "method": "info", "version": 2}, + {"name": "SYNO.Core.System.Status", "method": "get", "version": 1}, + {"name": "SYNO.Core.System.Status", "method": "get", "version": 2}, + {"name": "SYNO.Core.System.Utilization", "method": "get", "version": 1}, + {"name": "SYNO.Core.CurrentConnection", "method": "list", "version": 1} + ] + + # Перебираем все API и тестируем их + for api in api_to_test: + # Сначала получаем информацию о конкретном API + try: + api_info_params = { + "api": "SYNO.API.Info", + "version": "1", + "method": "query", + "query": api["name"] + } + + api_info_resp = session.get(api_info_url, params=api_info_params, timeout=10) + api_info_data = api_info_resp.json() + + if api_info_data.get("success") and api["name"] in api_info_data.get("data", {}): + api_details = api_info_data["data"][api["name"]] + api_path = api_details.get("path", "entry.cgi") + api_min_version = api_details.get("minVersion", 1) + api_max_version = api_details.get("maxVersion", 1) + + # Проверяем, поддерживается ли указанная версия + if api["version"] < api_min_version: + logger.warning(f"{api['name']} v{api['version']} ниже минимальной {api_min_version}, используем {api_min_version}") + test_version = api_min_version + elif api["version"] > api_max_version: + logger.warning(f"{api['name']} v{api['version']} выше максимальной {api_max_version}, используем {api_max_version}") + test_version = api_max_version + else: + test_version = api["version"] + + # Выполняем запрос API + test_url = f"{base_url}/{api_path}" + test_params = { + "api": api["name"], + "version": str(test_version), + "method": api["method"], + "_sid": sid # Используем sid для аутентификации + } + + logger.info(f"Тестирование {api['name']}.{api['method']} v{test_version}") + test_response = session.get(test_url, params=test_params, timeout=10) + test_data = test_response.json() + + if test_data.get("success"): + logger.info(f"API {api['name']}.{api['method']} v{test_version} РАБОТАЕТ!") + logger.info(f"Результат: {json.dumps(test_data.get('data', {}), indent=2)[:200]}...") + else: + error_code = test_data.get("error", {}).get("code", -1) + logger.error(f"API {api['name']}.{api['method']} v{test_version} ОШИБКА: {error_code}") + + # Если ошибка связана с сессией, попробуем еще раз авторизоваться + if error_code == 119: # Session timeout + logger.info("Повторная авторизация из-за ошибки 119...") + + # Создаем новую сессию + new_session = requests.Session() + new_session.verify = False + + auth_response = new_session.get(auth_url, params=auth_params, timeout=10) + auth_data = auth_response.json() + + if auth_data.get("success"): + new_sid = auth_data.get("data", {}).get("sid") + logger.info(f"Повторная авторизация успешна! Новый SID: {new_sid[:10]}...") + + # Пробуем запрос с новым SID + test_params["_sid"] = new_sid + logger.info(f"Повторное тестирование {api['name']}.{api['method']} v{test_version}") + test_response = new_session.get(test_url, params=test_params, timeout=10) + test_data = test_response.json() + + if test_data.get("success"): + logger.info(f"API {api['name']}.{api['method']} v{test_version} теперь РАБОТАЕТ!") + logger.info(f"Результат с новой сессией: {json.dumps(test_data.get('data', {}), indent=2)[:200]}...") + else: + error_code = test_data.get("error", {}).get("code", -1) + logger.error(f"API {api['name']}.{api['method']} v{test_version} ВСЕ ЕЩЕ С ОШИБКОЙ: {error_code}") + else: + logger.warning(f"API {api['name']} не найден в информации API") + + except Exception as e: + logger.error(f"Ошибка при тестировании {api['name']}.{api['method']} v{api['version']}: {str(e)}") + + # Шаг 3: Тестирование комбинации запросов для решения проблемы + logger.info("Шаг 3: Тестирование комбинации запросов для решения проблемы") + + # Создаем новую сессию для каждого запроса + for api in [{"name": "SYNO.DSM.Info", "method": "getinfo", "version": 1}]: + try: + fresh_session = requests.Session() + fresh_session.verify = False + + # Авторизуемся + auth_response = fresh_session.get(auth_url, params=auth_params, timeout=10) + auth_data = auth_response.json() + + if auth_data.get("success"): + fresh_sid = auth_data.get("data", {}).get("sid") + logger.info(f"Авторизация в новой сессии успешна! SID: {fresh_sid[:10]}...") + + # Сразу же делаем запрос для получения информации в той же сессии + test_params = { + "api": api["name"], + "version": str(api["version"]), + "method": api["method"], + "_sid": fresh_sid + } + + test_url = f"{base_url}/entry.cgi" # Используем entry.cgi по умолчанию + logger.info(f"Тест в свежей сессии: {api['name']}.{api['method']} v{api['version']}") + test_response = fresh_session.get(test_url, params=test_params, timeout=10) + test_data = test_response.json() + + if test_data.get("success"): + logger.info(f"API в свежей сессии РАБОТАЕТ!") + logger.info(f"Результат: {json.dumps(test_data.get('data', {}), indent=2)[:200]}...") + else: + error_code = test_data.get("error", {}).get("code", -1) + logger.error(f"API в свежей сессии ОШИБКА: {error_code}") + except Exception as e: + logger.error(f"Ошибка при тестировании свежей сессии: {str(e)}") + + # Шаг 4: Получаем информацию об остальных API + logger.info("Шаг 4: Получаем информацию о доступных API для уточнения проблемы") + + # Запрашиваем все API из SYNO.API.Info + try: + all_api_params = { + "api": "SYNO.API.Info", + "version": "1", + "method": "query", + "query": "all" + } + + all_api_response = session.get(api_info_url, params=all_api_params, timeout=15) # Больший таймаут для большого ответа + all_api_data = all_api_response.json() + + if all_api_data.get("success"): + api_list = all_api_data.get("data", {}) + logger.info(f"Получен список всех API. Найдено {len(api_list)} API.") + + # Ищем интересующие нас API для отладки + interested_in = ["SYNO.DSM.Info", "SYNO.Core.System", "SYNO.Core.Hardware", + "SYNO.Core.System.Status", "SYNO.API.Auth"] + + logger.info("Информация о важных API:") + for api_name in interested_in: + if api_name in api_list: + logger.info(f"{api_name}: {api_list[api_name]}") + else: + logger.warning(f"API {api_name} не найден") + else: + logger.error("Не удалось получить список всех API") + except Exception as e: + logger.error(f"Ошибка при получении списка API: {str(e)}") + + else: + error_code = auth_data.get("error", {}).get("code", -1) + logger.error(f"Авторизация не удалась! Код ошибки: {error_code}") + else: + logger.error("Не удалось получить информацию об API авторизации") + + except Exception as e: + logger.error(f"Произошла ошибка при выполнении теста: {str(e)}") + +if __name__ == "__main__": + logger.info("Запуск прямого теста API Synology") + direct_api_test() diff --git a/.history/docker-compose_20250830102643.yml b/.history/docker-compose_20250830102643.yml new file mode 100644 index 0000000..a12f9b3 --- /dev/null +++ b/.history/docker-compose_20250830102643.yml @@ -0,0 +1,21 @@ +version: '3.8' + +services: + synology-bot: + build: + context: . + dockerfile: Dockerfile + container_name: synology-power-control-bot + restart: unless-stopped + env_file: + - .env + volumes: + - ./logs:/app/logs + # Если у вас есть файлы конфигурации или данные, которые нужно сохранять: + # - ./data:/app/data + networks: + - bot-network + +networks: + bot-network: + driver: bridge diff --git a/.history/docker-compose_20250830102820.yml b/.history/docker-compose_20250830102820.yml new file mode 100644 index 0000000..a74cbdf --- /dev/null +++ b/.history/docker-compose_20250830102820.yml @@ -0,0 +1,36 @@ +version: '3.8' + +services: + synology-bot: + build: + context: . + dockerfile: Dockerfile + container_name: synology-power-control-bot + restart: unless-stopped + env_file: + - .env + volumes: + - ./logs:/app/logs + # Если у вас есть файлы конфигурации или данные, которые нужно сохранять: + # - ./data:/app/data + healthcheck: + test: ["CMD", "python", "-c", "import requests; requests.get('http://localhost:8080/health', timeout=5)"] + interval: 60s + timeout: 10s + retries: 3 + start_period: 20s + networks: + - bot-network + # Для ограничения ресурсов (раскомментируйте и настройте при необходимости): + # deploy: + # resources: + # limits: + # cpus: '0.50' + # memory: 512M + # reservations: + # cpus: '0.25' + # memory: 256M + +networks: + bot-network: + driver: bridge diff --git a/.history/docker-compose_20250830102916.yml b/.history/docker-compose_20250830102916.yml new file mode 100644 index 0000000..c35284c --- /dev/null +++ b/.history/docker-compose_20250830102916.yml @@ -0,0 +1,38 @@ +version: '3.8' + +services: + synology-bot: + build: + context: . + dockerfile: Dockerfile + container_name: synology-power-control-bot + restart: unless-stopped + env_file: + - .env + environment: + - DOCKER_ENV=true + volumes: + - ./logs:/app/logs + # Если у вас есть файлы конфигурации или данные, которые нужно сохранять: + # - ./data:/app/data + healthcheck: + test: ["CMD", "python", "-c", "import requests; requests.get('http://localhost:8080/health', timeout=5)"] + interval: 60s + timeout: 10s + retries: 3 + start_period: 20s + networks: + - bot-network + # Для ограничения ресурсов (раскомментируйте и настройте при необходимости): + # deploy: + # resources: + # limits: + # cpus: '0.50' + # memory: 512M + # reservations: + # cpus: '0.25' + # memory: 256M + +networks: + bot-network: + driver: bridge diff --git a/.history/docker-compose_20250830103154.yml b/.history/docker-compose_20250830103154.yml new file mode 100644 index 0000000..c35284c --- /dev/null +++ b/.history/docker-compose_20250830103154.yml @@ -0,0 +1,38 @@ +version: '3.8' + +services: + synology-bot: + build: + context: . + dockerfile: Dockerfile + container_name: synology-power-control-bot + restart: unless-stopped + env_file: + - .env + environment: + - DOCKER_ENV=true + volumes: + - ./logs:/app/logs + # Если у вас есть файлы конфигурации или данные, которые нужно сохранять: + # - ./data:/app/data + healthcheck: + test: ["CMD", "python", "-c", "import requests; requests.get('http://localhost:8080/health', timeout=5)"] + interval: 60s + timeout: 10s + retries: 3 + start_period: 20s + networks: + - bot-network + # Для ограничения ресурсов (раскомментируйте и настройте при необходимости): + # deploy: + # resources: + # limits: + # cpus: '0.50' + # memory: 512M + # reservations: + # cpus: '0.25' + # memory: 256M + +networks: + bot-network: + driver: bridge diff --git a/.history/entrypoint_20250830102747.sh b/.history/entrypoint_20250830102747.sh new file mode 100644 index 0000000..b4de286 --- /dev/null +++ b/.history/entrypoint_20250830102747.sh @@ -0,0 +1,7 @@ +#!/bin/sh + +# Создаем директорию для логов, если она не существует +mkdir -p /app/logs + +# Запускаем бота +exec python /app/run.py diff --git a/.history/entrypoint_20250830103154.sh b/.history/entrypoint_20250830103154.sh new file mode 100644 index 0000000..b4de286 --- /dev/null +++ b/.history/entrypoint_20250830103154.sh @@ -0,0 +1,7 @@ +#!/bin/sh + +# Создаем директорию для логов, если она не существует +mkdir -p /app/logs + +# Запускаем бота +exec python /app/run.py diff --git a/.history/requirements_20250830063740.txt b/.history/requirements_20250830063740.txt new file mode 100644 index 0000000..d08038f --- /dev/null +++ b/.history/requirements_20250830063740.txt @@ -0,0 +1,4 @@ +python-telegram-bot>=20.0 +requests>=2.28.0 +python-dotenv>=1.0.0 +urllib3>=2.0.0 diff --git a/.history/requirements_20250830063839.txt b/.history/requirements_20250830063839.txt new file mode 100644 index 0000000..d08038f --- /dev/null +++ b/.history/requirements_20250830063839.txt @@ -0,0 +1,4 @@ +python-telegram-bot>=20.0 +requests>=2.28.0 +python-dotenv>=1.0.0 +urllib3>=2.0.0 diff --git a/.history/requirements_20250830065002.txt b/.history/requirements_20250830065002.txt new file mode 100644 index 0000000..155425c --- /dev/null +++ b/.history/requirements_20250830065002.txt @@ -0,0 +1,5 @@ +python-telegram-bot>=20.0 +requests>=2.28.0 +python-dotenv>=1.0.0 +urllib3>=2.0.0 +python-synology>=0.4.0 diff --git a/.history/requirements_20250830065455.txt b/.history/requirements_20250830065455.txt new file mode 100644 index 0000000..155425c --- /dev/null +++ b/.history/requirements_20250830065455.txt @@ -0,0 +1,5 @@ +python-telegram-bot>=20.0 +requests>=2.28.0 +python-dotenv>=1.0.0 +urllib3>=2.0.0 +python-synology>=0.4.0 diff --git a/.history/requirements_20250830072350.txt b/.history/requirements_20250830072350.txt new file mode 100644 index 0000000..d08038f --- /dev/null +++ b/.history/requirements_20250830072350.txt @@ -0,0 +1,4 @@ +python-telegram-bot>=20.0 +requests>=2.28.0 +python-dotenv>=1.0.0 +urllib3>=2.0.0 diff --git a/.history/requirements_20250830072817.txt b/.history/requirements_20250830072817.txt new file mode 100644 index 0000000..d08038f --- /dev/null +++ b/.history/requirements_20250830072817.txt @@ -0,0 +1,4 @@ +python-telegram-bot>=20.0 +requests>=2.28.0 +python-dotenv>=1.0.0 +urllib3>=2.0.0 diff --git a/.history/requirements_20250830092418.txt b/.history/requirements_20250830092418.txt new file mode 100644 index 0000000..4703994 --- /dev/null +++ b/.history/requirements_20250830092418.txt @@ -0,0 +1,6 @@ +python-telegram-bot>=20.0 +requests>=2.28.0 +python-dotenv>=1.0.0 +urllib3>=2.0.0 +aiohttp>=3.8.4 +async-timeout>=4.0.2 diff --git a/.history/requirements_20250830092441.txt b/.history/requirements_20250830092441.txt new file mode 100644 index 0000000..4703994 --- /dev/null +++ b/.history/requirements_20250830092441.txt @@ -0,0 +1,6 @@ +python-telegram-bot>=20.0 +requests>=2.28.0 +python-dotenv>=1.0.0 +urllib3>=2.0.0 +aiohttp>=3.8.4 +async-timeout>=4.0.2 diff --git a/.history/run_20250830063754.py b/.history/run_20250830063754.py new file mode 100644 index 0000000..bf8b3e5 --- /dev/null +++ b/.history/run_20250830063754.py @@ -0,0 +1,11 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Точка входа для запуска телеграм-бота +""" + +from src.bot import main + +if __name__ == "__main__": + main() diff --git a/.history/run_20250830063839.py b/.history/run_20250830063839.py new file mode 100644 index 0000000..bf8b3e5 --- /dev/null +++ b/.history/run_20250830063839.py @@ -0,0 +1,11 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Точка входа для запуска телеграм-бота +""" + +from src.bot import main + +if __name__ == "__main__": + main() diff --git a/.history/run_20250830102904.py b/.history/run_20250830102904.py new file mode 100644 index 0000000..b695a8d --- /dev/null +++ b/.history/run_20250830102904.py @@ -0,0 +1,18 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Точка входа для запуска телеграм-бота +""" + +import os +from src.bot import main +from src.healthcheck import start_health_server + +if __name__ == "__main__": + # Запускаем healthcheck сервер в Docker-окружении + if os.environ.get("DOCKER_ENV", "False").lower() == "true": + start_health_server() + + # Запускаем основной бот + main() diff --git a/.history/run_20250830103154.py b/.history/run_20250830103154.py new file mode 100644 index 0000000..b695a8d --- /dev/null +++ b/.history/run_20250830103154.py @@ -0,0 +1,18 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Точка входа для запуска телеграм-бота +""" + +import os +from src.bot import main +from src.healthcheck import start_health_server + +if __name__ == "__main__": + # Запускаем healthcheck сервер в Docker-окружении + if os.environ.get("DOCKER_ENV", "False").lower() == "true": + start_health_server() + + # Запускаем основной бот + main() diff --git a/.history/run_bot_20250830082521.py b/.history/run_bot_20250830082521.py new file mode 100644 index 0000000..737f58d --- /dev/null +++ b/.history/run_bot_20250830082521.py @@ -0,0 +1,11 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Файл-обёртка для запуска бота из корневой директории +""" + +from src.bot import main + +if __name__ == "__main__": + main() diff --git a/.history/run_bot_20250830082536.py b/.history/run_bot_20250830082536.py new file mode 100644 index 0000000..737f58d --- /dev/null +++ b/.history/run_bot_20250830082536.py @@ -0,0 +1,11 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Файл-обёртка для запуска бота из корневой директории +""" + +from src.bot import main + +if __name__ == "__main__": + main() diff --git a/.history/src/api/api_discovery_20250830081819.py b/.history/src/api/api_discovery_20250830081819.py new file mode 100644 index 0000000..641dc8c --- /dev/null +++ b/.history/src/api/api_discovery_20250830081819.py @@ -0,0 +1,113 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Модуль для обнаружения доступных API Synology NAS +""" + +import logging +import requests +import urllib3 +from typing import Dict, Any, List, Optional + +# Отключение предупреждений о небезопасных SSL-соединениях +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + +logger = logging.getLogger(__name__) + +def discover_available_apis(base_url: str, timeout=(10, 20), verify=False) -> Dict[str, Any]: + """ + Получение списка доступных API на Synology NAS + + Args: + base_url: базовый URL для API (например, 'http://192.168.0.100:5000/webapi') + timeout: таймаут для запроса + verify: проверять ли SSL-сертификат + + Returns: + Словарь с информацией о доступных API + """ + logger.info("Discovering available Synology APIs") + + try: + # Делаем базовый запрос для получения всех доступных API + api_info_url = f"{base_url}/entry.cgi" + api_info_params = { + "api": "SYNO.API.Info", + "version": "1", + "method": "query", + "query": "all" + } + + response = requests.get( + api_info_url, + params=api_info_params, + timeout=timeout, + verify=verify + ) + + if response.status_code == 200: + data = response.json() + if data.get("success"): + apis = data.get("data", {}) + logger.info(f"Discovered {len(apis)} APIs") + + # Выводим список найденных API + api_list = list(apis.keys()) + logger.debug(f"Available APIs: {', '.join(api_list[:10])}... and {len(api_list) - 10} more") + + # Группируем API по категориям + power_apis = [api for api in api_list if "power" in api.lower()] + system_apis = [api for api in api_list if "system" in api.lower()] + info_apis = [api for api in api_list if "info" in api.lower()] + + logger.info(f"Power related APIs: {power_apis}") + logger.info(f"System related APIs: {system_apis[:10]}") + logger.info(f"Info related APIs: {info_apis[:10]}") + + return apis + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to discover APIs. Error code: {error_code}") + return {} + else: + logger.error(f"API discovery request failed with HTTP status: {response.status_code}") + return {} + except Exception as e: + logger.error(f"Error during API discovery: {str(e)}") + return {} + +def find_compatible_api(apis: Dict[str, Any], api_category: str, method: str) -> List[Dict[str, Any]]: + """ + Поиск совместимых API заданной категории + + Args: + apis: словарь с доступными API + api_category: категория API (например, 'system', 'power', 'info') + method: искомый метод API + + Returns: + Список подходящих API с версиями + """ + compatible_apis = [] + + for api_name, api_info in apis.items(): + if api_category.lower() in api_name.lower(): + compatible_apis.append({ + "name": api_name, + "path": api_info.get("path", "entry.cgi"), + "min_version": api_info.get("minVersion", 1), + "max_version": api_info.get("maxVersion", 1), + "method": method, + "version": api_info.get("maxVersion", 1) # Используем максимальную версию по умолчанию + }) + + # Сортируем по приоритету + compatible_apis.sort(key=lambda x: ( + # Приоритет по точности совпадения категории + 0 if api_category.upper() in x["name"] else 1, + # Приоритет по версии (от большей к меньшей) + -x["max_version"] + )) + + return compatible_apis diff --git a/.history/src/api/api_discovery_20250830081957.py b/.history/src/api/api_discovery_20250830081957.py new file mode 100644 index 0000000..641dc8c --- /dev/null +++ b/.history/src/api/api_discovery_20250830081957.py @@ -0,0 +1,113 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Модуль для обнаружения доступных API Synology NAS +""" + +import logging +import requests +import urllib3 +from typing import Dict, Any, List, Optional + +# Отключение предупреждений о небезопасных SSL-соединениях +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + +logger = logging.getLogger(__name__) + +def discover_available_apis(base_url: str, timeout=(10, 20), verify=False) -> Dict[str, Any]: + """ + Получение списка доступных API на Synology NAS + + Args: + base_url: базовый URL для API (например, 'http://192.168.0.100:5000/webapi') + timeout: таймаут для запроса + verify: проверять ли SSL-сертификат + + Returns: + Словарь с информацией о доступных API + """ + logger.info("Discovering available Synology APIs") + + try: + # Делаем базовый запрос для получения всех доступных API + api_info_url = f"{base_url}/entry.cgi" + api_info_params = { + "api": "SYNO.API.Info", + "version": "1", + "method": "query", + "query": "all" + } + + response = requests.get( + api_info_url, + params=api_info_params, + timeout=timeout, + verify=verify + ) + + if response.status_code == 200: + data = response.json() + if data.get("success"): + apis = data.get("data", {}) + logger.info(f"Discovered {len(apis)} APIs") + + # Выводим список найденных API + api_list = list(apis.keys()) + logger.debug(f"Available APIs: {', '.join(api_list[:10])}... and {len(api_list) - 10} more") + + # Группируем API по категориям + power_apis = [api for api in api_list if "power" in api.lower()] + system_apis = [api for api in api_list if "system" in api.lower()] + info_apis = [api for api in api_list if "info" in api.lower()] + + logger.info(f"Power related APIs: {power_apis}") + logger.info(f"System related APIs: {system_apis[:10]}") + logger.info(f"Info related APIs: {info_apis[:10]}") + + return apis + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to discover APIs. Error code: {error_code}") + return {} + else: + logger.error(f"API discovery request failed with HTTP status: {response.status_code}") + return {} + except Exception as e: + logger.error(f"Error during API discovery: {str(e)}") + return {} + +def find_compatible_api(apis: Dict[str, Any], api_category: str, method: str) -> List[Dict[str, Any]]: + """ + Поиск совместимых API заданной категории + + Args: + apis: словарь с доступными API + api_category: категория API (например, 'system', 'power', 'info') + method: искомый метод API + + Returns: + Список подходящих API с версиями + """ + compatible_apis = [] + + for api_name, api_info in apis.items(): + if api_category.lower() in api_name.lower(): + compatible_apis.append({ + "name": api_name, + "path": api_info.get("path", "entry.cgi"), + "min_version": api_info.get("minVersion", 1), + "max_version": api_info.get("maxVersion", 1), + "method": method, + "version": api_info.get("maxVersion", 1) # Используем максимальную версию по умолчанию + }) + + # Сортируем по приоритету + compatible_apis.sort(key=lambda x: ( + # Приоритет по точности совпадения категории + 0 if api_category.upper() in x["name"] else 1, + # Приоритет по версии (от большей к меньшей) + -x["max_version"] + )) + + return compatible_apis diff --git a/.history/src/api/api_version_resolver_20250830084129.py b/.history/src/api/api_version_resolver_20250830084129.py new file mode 100644 index 0000000..64a4003 --- /dev/null +++ b/.history/src/api/api_version_resolver_20250830084129.py @@ -0,0 +1,234 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Модуль для разрешения проблем с API Synology и автоматического выбора совместимых версий +""" + +import logging +import requests +from typing import Dict, Any, Optional, List, Tuple + +logger = logging.getLogger(__name__) + +class ApiVersionResolver: + """Класс для определения совместимых версий API и правильных методов""" + + def __init__(self, base_url: str, session: requests.Session, timeout: tuple = (10, 20)): + """Инициализация класса ApiVersionResolver + + Args: + base_url: Базовый URL API Synology NAS (например, http://192.168.0.102:5000/webapi) + session: Сессия requests для повторного использования соединений + timeout: Таймауты для запросов (connect_timeout, read_timeout) + """ + self.base_url = base_url + self.session = session + self.timeout = timeout + self.api_info_cache = {} + + def get_api_info(self, api_name: str) -> Dict[str, Any]: + """Получает информацию об API из SYNO.API.Info + + Args: + api_name: Имя API для запроса (например, SYNO.DSM.Info) + + Returns: + Dict с информацией об API или пустой словарь в случае ошибки + """ + # Проверяем наличие данных в кэше + if api_name in self.api_info_cache: + return self.api_info_cache[api_name] + + try: + # Запрос информации об API + api_info_url = f"{self.base_url}/entry.cgi" + api_info_params = { + "api": "SYNO.API.Info", + "version": "1", + "method": "query", + "query": api_name + } + + logger.debug(f"Querying API info for {api_name}") + response = self.session.get( + api_info_url, + params=api_info_params, + timeout=self.timeout, + verify=False + ) + + if response.status_code != 200: + logger.warning(f"API info request failed with status {response.status_code}") + return {} + + data = response.json() + if not data.get("success"): + logger.warning(f"API info request unsuccessful for {api_name}") + return {} + + # Извлекаем информацию о запрошенном API + api_info = data.get("data", {}).get(api_name, {}) + if not api_info: + logger.warning(f"API {api_name} not found in API info response") + return {} + + # Кэшируем результат + self.api_info_cache[api_name] = api_info + logger.debug(f"API info for {api_name}: {api_info}") + return api_info + + except Exception as e: + logger.error(f"Error querying API info for {api_name}: {str(e)}") + return {} + + def resolve_api_path(self, api_name: str) -> str: + """Определяет путь для API + + Args: + api_name: Имя API + + Returns: + Путь к API или 'entry.cgi' по умолчанию + """ + api_info = self.get_api_info(api_name) + return api_info.get("path", "entry.cgi") + + def resolve_api_version(self, api_name: str, requested_version: int) -> int: + """Определяет совместимую версию API + + Args: + api_name: Имя API + requested_version: Запрошенная версия API + + Returns: + Совместимая версия API, которая будет работать + """ + api_info = self.get_api_info(api_name) + if not api_info: + # Если нет информации, возвращаем запрошенную версию + return requested_version + + min_version = api_info.get("minVersion", 1) + max_version = api_info.get("maxVersion", requested_version) + + # Проверка, поддерживается ли запрошенная версия + if requested_version < min_version: + logger.warning(f"API version {requested_version} for {api_name} is below minimum {min_version}, using {min_version}") + return min_version + elif requested_version > max_version: + logger.warning(f"API version {requested_version} for {api_name} exceeds maximum {max_version}, using {max_version}") + return max_version + + return requested_version + + def resolve_api_method(self, api_name: str) -> Dict[str, str]: + """Определяет доступные методы для API + + Args: + api_name: Имя API + + Returns: + Словарь с типами методов и их правильными именами для данного API + """ + # Возможные методы для разных типов API + api_methods = { + # Методы для информации о системе + "SYNO.DSM.Info": {"info": "getinfo", "get": "getinfo"}, + "SYNO.Core.System": {"info": "info", "get": "info"}, + "SYNO.Core.System.Status": {"info": "get", "get": "get"}, + "SYNO.Core.System.Info": {"info": "get", "get": "get"}, + + # Методы для управления питанием + "SYNO.Core.Hardware.PowerRecovery": { + "restart": "setPowerOnState", + "reboot": "setPowerOnState", + "shutdown": "setPowerOnState", + "poweroff": "setPowerOnState" + }, + "SYNO.Core.System.Power": { + "restart": "restart", + "reboot": "restart", + "shutdown": "shutdown", + "poweroff": "shutdown" + }, + "SYNO.DSM.Power": { + "restart": "reboot", + "reboot": "reboot", + "shutdown": "shutdown", + "poweroff": "shutdown" + }, + "SYNO.Core.Hardware.NeedReboot": { + "restart": "reboot", + "reboot": "reboot" + } + } + + return api_methods.get(api_name, {}) + + def get_api_special_params(self, api_name: str, method: str) -> Dict[str, Any]: + """Возвращает специальные параметры, которые требуются для определенного API + + Args: + api_name: Имя API + method: Метод API + + Returns: + Словарь с параметрами для метода или пустой словарь + """ + # Специфические параметры для определенных API + special_params = { + # Параметры для управления питанием + "SYNO.Core.Hardware.PowerRecovery": { + "setPowerOnState": { + "restart": {"reboot": "true"}, + "reboot": {"reboot": "true"}, + "shutdown": {"state": "powerbtn"}, + "poweroff": {"state": "powerbtn"} + } + }, + # Другие специальные параметры для других API + } + + api_params = special_params.get(api_name, {}) + method_params = api_params.get(method, {}) + + # Если это метод управления питанием, возвращаем соответствующие параметры + if isinstance(method_params, dict) and method in method_params: + return method_params[method] + + return method_params + + def find_compatible_api_for_function(self, function_type: str) -> List[Tuple[str, str, int]]: + """Находит совместимые API для определенного типа функций + + Args: + function_type: Тип функции ('info', 'power', 'status', etc.) + + Returns: + Список кортежей (api_name, method, version) в порядке приоритета + """ + # Определяем API для каждого типа функции + function_apis = { + "info": [ + ("SYNO.DSM.Info", "getinfo", 2), + ("SYNO.Core.System", "info", 1), + ("SYNO.Core.System.Status", "get", 1), + ("SYNO.Core.System.Info", "get", 1) + ], + "power_restart": [ + ("SYNO.Core.Hardware.PowerRecovery", "setPowerOnState", 1), + ("SYNO.Core.Hardware.NeedReboot", "reboot", 1), + ("SYNO.Core.System.Power", "restart", 1), + ("SYNO.DSM.Power", "reboot", 1), + ("SYNO.Core.System", "reboot", 3) + ], + "power_shutdown": [ + ("SYNO.Core.Hardware.PowerRecovery", "setPowerOnState", 1), + ("SYNO.Core.System.Power", "shutdown", 1), + ("SYNO.DSM.Power", "shutdown", 1), + ("SYNO.Core.System", "shutdown", 3) + ] + } + + return function_apis.get(function_type, []) diff --git a/.history/src/api/api_version_resolver_20250830084257.py b/.history/src/api/api_version_resolver_20250830084257.py new file mode 100644 index 0000000..64a4003 --- /dev/null +++ b/.history/src/api/api_version_resolver_20250830084257.py @@ -0,0 +1,234 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Модуль для разрешения проблем с API Synology и автоматического выбора совместимых версий +""" + +import logging +import requests +from typing import Dict, Any, Optional, List, Tuple + +logger = logging.getLogger(__name__) + +class ApiVersionResolver: + """Класс для определения совместимых версий API и правильных методов""" + + def __init__(self, base_url: str, session: requests.Session, timeout: tuple = (10, 20)): + """Инициализация класса ApiVersionResolver + + Args: + base_url: Базовый URL API Synology NAS (например, http://192.168.0.102:5000/webapi) + session: Сессия requests для повторного использования соединений + timeout: Таймауты для запросов (connect_timeout, read_timeout) + """ + self.base_url = base_url + self.session = session + self.timeout = timeout + self.api_info_cache = {} + + def get_api_info(self, api_name: str) -> Dict[str, Any]: + """Получает информацию об API из SYNO.API.Info + + Args: + api_name: Имя API для запроса (например, SYNO.DSM.Info) + + Returns: + Dict с информацией об API или пустой словарь в случае ошибки + """ + # Проверяем наличие данных в кэше + if api_name in self.api_info_cache: + return self.api_info_cache[api_name] + + try: + # Запрос информации об API + api_info_url = f"{self.base_url}/entry.cgi" + api_info_params = { + "api": "SYNO.API.Info", + "version": "1", + "method": "query", + "query": api_name + } + + logger.debug(f"Querying API info for {api_name}") + response = self.session.get( + api_info_url, + params=api_info_params, + timeout=self.timeout, + verify=False + ) + + if response.status_code != 200: + logger.warning(f"API info request failed with status {response.status_code}") + return {} + + data = response.json() + if not data.get("success"): + logger.warning(f"API info request unsuccessful for {api_name}") + return {} + + # Извлекаем информацию о запрошенном API + api_info = data.get("data", {}).get(api_name, {}) + if not api_info: + logger.warning(f"API {api_name} not found in API info response") + return {} + + # Кэшируем результат + self.api_info_cache[api_name] = api_info + logger.debug(f"API info for {api_name}: {api_info}") + return api_info + + except Exception as e: + logger.error(f"Error querying API info for {api_name}: {str(e)}") + return {} + + def resolve_api_path(self, api_name: str) -> str: + """Определяет путь для API + + Args: + api_name: Имя API + + Returns: + Путь к API или 'entry.cgi' по умолчанию + """ + api_info = self.get_api_info(api_name) + return api_info.get("path", "entry.cgi") + + def resolve_api_version(self, api_name: str, requested_version: int) -> int: + """Определяет совместимую версию API + + Args: + api_name: Имя API + requested_version: Запрошенная версия API + + Returns: + Совместимая версия API, которая будет работать + """ + api_info = self.get_api_info(api_name) + if not api_info: + # Если нет информации, возвращаем запрошенную версию + return requested_version + + min_version = api_info.get("minVersion", 1) + max_version = api_info.get("maxVersion", requested_version) + + # Проверка, поддерживается ли запрошенная версия + if requested_version < min_version: + logger.warning(f"API version {requested_version} for {api_name} is below minimum {min_version}, using {min_version}") + return min_version + elif requested_version > max_version: + logger.warning(f"API version {requested_version} for {api_name} exceeds maximum {max_version}, using {max_version}") + return max_version + + return requested_version + + def resolve_api_method(self, api_name: str) -> Dict[str, str]: + """Определяет доступные методы для API + + Args: + api_name: Имя API + + Returns: + Словарь с типами методов и их правильными именами для данного API + """ + # Возможные методы для разных типов API + api_methods = { + # Методы для информации о системе + "SYNO.DSM.Info": {"info": "getinfo", "get": "getinfo"}, + "SYNO.Core.System": {"info": "info", "get": "info"}, + "SYNO.Core.System.Status": {"info": "get", "get": "get"}, + "SYNO.Core.System.Info": {"info": "get", "get": "get"}, + + # Методы для управления питанием + "SYNO.Core.Hardware.PowerRecovery": { + "restart": "setPowerOnState", + "reboot": "setPowerOnState", + "shutdown": "setPowerOnState", + "poweroff": "setPowerOnState" + }, + "SYNO.Core.System.Power": { + "restart": "restart", + "reboot": "restart", + "shutdown": "shutdown", + "poweroff": "shutdown" + }, + "SYNO.DSM.Power": { + "restart": "reboot", + "reboot": "reboot", + "shutdown": "shutdown", + "poweroff": "shutdown" + }, + "SYNO.Core.Hardware.NeedReboot": { + "restart": "reboot", + "reboot": "reboot" + } + } + + return api_methods.get(api_name, {}) + + def get_api_special_params(self, api_name: str, method: str) -> Dict[str, Any]: + """Возвращает специальные параметры, которые требуются для определенного API + + Args: + api_name: Имя API + method: Метод API + + Returns: + Словарь с параметрами для метода или пустой словарь + """ + # Специфические параметры для определенных API + special_params = { + # Параметры для управления питанием + "SYNO.Core.Hardware.PowerRecovery": { + "setPowerOnState": { + "restart": {"reboot": "true"}, + "reboot": {"reboot": "true"}, + "shutdown": {"state": "powerbtn"}, + "poweroff": {"state": "powerbtn"} + } + }, + # Другие специальные параметры для других API + } + + api_params = special_params.get(api_name, {}) + method_params = api_params.get(method, {}) + + # Если это метод управления питанием, возвращаем соответствующие параметры + if isinstance(method_params, dict) and method in method_params: + return method_params[method] + + return method_params + + def find_compatible_api_for_function(self, function_type: str) -> List[Tuple[str, str, int]]: + """Находит совместимые API для определенного типа функций + + Args: + function_type: Тип функции ('info', 'power', 'status', etc.) + + Returns: + Список кортежей (api_name, method, version) в порядке приоритета + """ + # Определяем API для каждого типа функции + function_apis = { + "info": [ + ("SYNO.DSM.Info", "getinfo", 2), + ("SYNO.Core.System", "info", 1), + ("SYNO.Core.System.Status", "get", 1), + ("SYNO.Core.System.Info", "get", 1) + ], + "power_restart": [ + ("SYNO.Core.Hardware.PowerRecovery", "setPowerOnState", 1), + ("SYNO.Core.Hardware.NeedReboot", "reboot", 1), + ("SYNO.Core.System.Power", "restart", 1), + ("SYNO.DSM.Power", "reboot", 1), + ("SYNO.Core.System", "reboot", 3) + ], + "power_shutdown": [ + ("SYNO.Core.Hardware.PowerRecovery", "setPowerOnState", 1), + ("SYNO.Core.System.Power", "shutdown", 1), + ("SYNO.DSM.Power", "shutdown", 1), + ("SYNO.Core.System", "shutdown", 3) + ] + } + + return function_apis.get(function_type, []) diff --git a/.history/src/api/synology_20250830063552.py b/.history/src/api/synology_20250830063552.py new file mode 100644 index 0000000..1a03a0b --- /dev/null +++ b/.history/src/api/synology_20250830063552.py @@ -0,0 +1,262 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Модуль для взаимодействия с API Synology NAS +""" + +import requests +import json +import logging +from typing import Dict, Any, Optional +import socket +import struct +from time import sleep + +from src.config.config import ( + SYNOLOGY_HOST, + SYNOLOGY_PORT, + SYNOLOGY_USERNAME, + SYNOLOGY_PASSWORD, + SYNOLOGY_SECURE, + SYNOLOGY_TIMEOUT, + SYNOLOGY_MAC, + WOL_PORT +) + +logger = logging.getLogger(__name__) + +class SynologyAPI: + """Класс для взаимодействия с API Synology NAS""" + + def __init__(self): + """Инициализация класса SynologyAPI""" + self.protocol = "https" if SYNOLOGY_SECURE else "http" + self.base_url = f"{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi" + self.sid = None + self.session = requests.Session() + + def login(self) -> bool: + """Авторизация в API Synology NAS""" + try: + url = f"{self.base_url}/auth.cgi" + params = { + "api": "SYNO.API.Auth", + "version": "3", + "method": "login", + "account": SYNOLOGY_USERNAME, + "passwd": SYNOLOGY_PASSWORD, + "session": "SynologyPowerControlBot", + "format": "cookie" + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + self.sid = data.get("data", {}).get("sid") + logger.info("Successfully logged in to Synology NAS") + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to log in to Synology NAS: Error code {error_code}") + return False + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + return False + + def logout(self) -> bool: + """Выход из API Synology NAS""" + if not self.sid: + return True + + try: + url = f"{self.base_url}/auth.cgi" + params = { + "api": "SYNO.API.Auth", + "version": "1", + "method": "logout", + "session": "SynologyPowerControlBot", + "_sid": self.sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + self.sid = None + logger.info("Successfully logged out from Synology NAS") + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to log out from Synology NAS: Error code {error_code}") + return False + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + return False + + def get_system_status(self) -> Optional[Dict[str, Any]]: + """Получение статуса системы""" + if not self.sid and not self.login(): + return None + + try: + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.DSM.Info", + "version": "1", + "method": "getinfo", + "_sid": self.sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + logger.info("Successfully fetched system status") + return data.get("data") + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to get system status: Error code {error_code}") + return None + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + return None + + def shutdown_system(self) -> bool: + """Выключение системы""" + if not self.sid and not self.login(): + return False + + try: + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.DSM.System", + "version": "1", + "method": "shutdown", + "_sid": self.sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + logger.info("Successfully initiated system shutdown") + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to shutdown system: Error code {error_code}") + return False + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + return False + + def reboot_system(self) -> bool: + """Перезагрузка системы""" + if not self.sid and not self.login(): + return False + + try: + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.DSM.System", + "version": "1", + "method": "reboot", + "_sid": self.sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + logger.info("Successfully initiated system reboot") + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to reboot system: Error code {error_code}") + return False + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + return False + + def is_online(self) -> bool: + """Проверка онлайн-статуса Synology NAS""" + try: + socket_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + socket_obj.settimeout(SYNOLOGY_TIMEOUT) + result = socket_obj.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT)) + socket_obj.close() + + return result == 0 + except socket.error: + return False + + def wake_on_lan(self) -> bool: + """Отправка Wake-on-LAN пакета для включения Synology NAS""" + if not SYNOLOGY_MAC: + logger.error("MAC address not configured") + return False + + try: + # Преобразование MAC-адреса в байты + mac_address = SYNOLOGY_MAC.replace(':', '').replace('-', '') + mac_bytes = bytes.fromhex(mac_address) + + # Создание Magic Packet + magic_packet = b'\xff' * 6 + mac_bytes * 16 + + # Отправка пакета + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) + sock.sendto(magic_packet, (SYNOLOGY_HOST, WOL_PORT)) + sock.close() + + logger.info(f"Wake-on-LAN packet sent to {SYNOLOGY_MAC}") + return True + + except Exception as e: + logger.error(f"Error sending Wake-on-LAN packet: {str(e)}") + return False + + def wait_for_boot(self, max_attempts: int = 30, delay: int = 10) -> bool: + """Ожидание загрузки Synology NAS""" + logger.info("Waiting for Synology NAS to boot...") + + for attempt in range(max_attempts): + if self.is_online(): + logger.info(f"Synology NAS is online after {attempt + 1} attempts") + # Дадим дополнительное время для полной загрузки всех сервисов + sleep(delay) + return True + + sleep(delay) + logger.info(f"Waiting for boot... Attempt {attempt + 1}/{max_attempts}") + + logger.error(f"Synology NAS didn't come online after {max_attempts} attempts") + return False + + def power_on(self) -> bool: + """Включение Synology NAS""" + if self.is_online(): + logger.info("Synology NAS is already online") + return True + + # Отправка WoL пакета + if not self.wake_on_lan(): + return False + + # Ожидание загрузки + return self.wait_for_boot() + + def power_off(self) -> bool: + """Выключение Synology NAS""" + if not self.is_online(): + logger.info("Synology NAS is already offline") + return True + + return self.shutdown_system() diff --git a/.history/src/api/synology_20250830063839.py b/.history/src/api/synology_20250830063839.py new file mode 100644 index 0000000..1a03a0b --- /dev/null +++ b/.history/src/api/synology_20250830063839.py @@ -0,0 +1,262 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Модуль для взаимодействия с API Synology NAS +""" + +import requests +import json +import logging +from typing import Dict, Any, Optional +import socket +import struct +from time import sleep + +from src.config.config import ( + SYNOLOGY_HOST, + SYNOLOGY_PORT, + SYNOLOGY_USERNAME, + SYNOLOGY_PASSWORD, + SYNOLOGY_SECURE, + SYNOLOGY_TIMEOUT, + SYNOLOGY_MAC, + WOL_PORT +) + +logger = logging.getLogger(__name__) + +class SynologyAPI: + """Класс для взаимодействия с API Synology NAS""" + + def __init__(self): + """Инициализация класса SynologyAPI""" + self.protocol = "https" if SYNOLOGY_SECURE else "http" + self.base_url = f"{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi" + self.sid = None + self.session = requests.Session() + + def login(self) -> bool: + """Авторизация в API Synology NAS""" + try: + url = f"{self.base_url}/auth.cgi" + params = { + "api": "SYNO.API.Auth", + "version": "3", + "method": "login", + "account": SYNOLOGY_USERNAME, + "passwd": SYNOLOGY_PASSWORD, + "session": "SynologyPowerControlBot", + "format": "cookie" + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + self.sid = data.get("data", {}).get("sid") + logger.info("Successfully logged in to Synology NAS") + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to log in to Synology NAS: Error code {error_code}") + return False + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + return False + + def logout(self) -> bool: + """Выход из API Synology NAS""" + if not self.sid: + return True + + try: + url = f"{self.base_url}/auth.cgi" + params = { + "api": "SYNO.API.Auth", + "version": "1", + "method": "logout", + "session": "SynologyPowerControlBot", + "_sid": self.sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + self.sid = None + logger.info("Successfully logged out from Synology NAS") + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to log out from Synology NAS: Error code {error_code}") + return False + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + return False + + def get_system_status(self) -> Optional[Dict[str, Any]]: + """Получение статуса системы""" + if not self.sid and not self.login(): + return None + + try: + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.DSM.Info", + "version": "1", + "method": "getinfo", + "_sid": self.sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + logger.info("Successfully fetched system status") + return data.get("data") + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to get system status: Error code {error_code}") + return None + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + return None + + def shutdown_system(self) -> bool: + """Выключение системы""" + if not self.sid and not self.login(): + return False + + try: + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.DSM.System", + "version": "1", + "method": "shutdown", + "_sid": self.sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + logger.info("Successfully initiated system shutdown") + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to shutdown system: Error code {error_code}") + return False + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + return False + + def reboot_system(self) -> bool: + """Перезагрузка системы""" + if not self.sid and not self.login(): + return False + + try: + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.DSM.System", + "version": "1", + "method": "reboot", + "_sid": self.sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + logger.info("Successfully initiated system reboot") + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to reboot system: Error code {error_code}") + return False + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + return False + + def is_online(self) -> bool: + """Проверка онлайн-статуса Synology NAS""" + try: + socket_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + socket_obj.settimeout(SYNOLOGY_TIMEOUT) + result = socket_obj.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT)) + socket_obj.close() + + return result == 0 + except socket.error: + return False + + def wake_on_lan(self) -> bool: + """Отправка Wake-on-LAN пакета для включения Synology NAS""" + if not SYNOLOGY_MAC: + logger.error("MAC address not configured") + return False + + try: + # Преобразование MAC-адреса в байты + mac_address = SYNOLOGY_MAC.replace(':', '').replace('-', '') + mac_bytes = bytes.fromhex(mac_address) + + # Создание Magic Packet + magic_packet = b'\xff' * 6 + mac_bytes * 16 + + # Отправка пакета + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) + sock.sendto(magic_packet, (SYNOLOGY_HOST, WOL_PORT)) + sock.close() + + logger.info(f"Wake-on-LAN packet sent to {SYNOLOGY_MAC}") + return True + + except Exception as e: + logger.error(f"Error sending Wake-on-LAN packet: {str(e)}") + return False + + def wait_for_boot(self, max_attempts: int = 30, delay: int = 10) -> bool: + """Ожидание загрузки Synology NAS""" + logger.info("Waiting for Synology NAS to boot...") + + for attempt in range(max_attempts): + if self.is_online(): + logger.info(f"Synology NAS is online after {attempt + 1} attempts") + # Дадим дополнительное время для полной загрузки всех сервисов + sleep(delay) + return True + + sleep(delay) + logger.info(f"Waiting for boot... Attempt {attempt + 1}/{max_attempts}") + + logger.error(f"Synology NAS didn't come online after {max_attempts} attempts") + return False + + def power_on(self) -> bool: + """Включение Synology NAS""" + if self.is_online(): + logger.info("Synology NAS is already online") + return True + + # Отправка WoL пакета + if not self.wake_on_lan(): + return False + + # Ожидание загрузки + return self.wait_for_boot() + + def power_off(self) -> bool: + """Выключение Synology NAS""" + if not self.is_online(): + logger.info("Synology NAS is already offline") + return True + + return self.shutdown_system() diff --git a/.history/src/api/synology_20250830065021.py b/.history/src/api/synology_20250830065021.py new file mode 100644 index 0000000..f361511 --- /dev/null +++ b/.history/src/api/synology_20250830065021.py @@ -0,0 +1,267 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Модуль для взаимодействия с API Synology NAS с использованием библиотеки python-synology +""" + +import requests +import json +import logging +from typing import Dict, Any, Optional, List, Tuple +import socket +import struct +from time import sleep +import urllib3 +from synology import SynologyDSM + +from src.config.config import ( + SYNOLOGY_HOST, + SYNOLOGY_PORT, + SYNOLOGY_USERNAME, + SYNOLOGY_PASSWORD, + SYNOLOGY_SECURE, + SYNOLOGY_TIMEOUT, + SYNOLOGY_MAC, + WOL_PORT +) + +# Отключение предупреждений о небезопасных SSL-соединениях +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + +logger = logging.getLogger(__name__) + +class SynologyAPI: + """Класс для взаимодействия с API Synology NAS""" + + def __init__(self): + """Инициализация класса SynologyAPI""" + self.protocol = "https" if SYNOLOGY_SECURE else "http" + self.base_url = f"{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi" + self.sid = None + self.session = requests.Session() + + def login(self) -> bool: + """Авторизация в API Synology NAS""" + try: + url = f"{self.base_url}/auth.cgi" + params = { + "api": "SYNO.API.Auth", + "version": "3", + "method": "login", + "account": SYNOLOGY_USERNAME, + "passwd": SYNOLOGY_PASSWORD, + "session": "SynologyPowerControlBot", + "format": "cookie" + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + self.sid = data.get("data", {}).get("sid") + logger.info("Successfully logged in to Synology NAS") + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to log in to Synology NAS: Error code {error_code}") + return False + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + return False + + def logout(self) -> bool: + """Выход из API Synology NAS""" + if not self.sid: + return True + + try: + url = f"{self.base_url}/auth.cgi" + params = { + "api": "SYNO.API.Auth", + "version": "1", + "method": "logout", + "session": "SynologyPowerControlBot", + "_sid": self.sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + self.sid = None + logger.info("Successfully logged out from Synology NAS") + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to log out from Synology NAS: Error code {error_code}") + return False + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + return False + + def get_system_status(self) -> Optional[Dict[str, Any]]: + """Получение статуса системы""" + if not self.sid and not self.login(): + return None + + try: + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.DSM.Info", + "version": "1", + "method": "getinfo", + "_sid": self.sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + logger.info("Successfully fetched system status") + return data.get("data") + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to get system status: Error code {error_code}") + return None + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + return None + + def shutdown_system(self) -> bool: + """Выключение системы""" + if not self.sid and not self.login(): + return False + + try: + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.DSM.System", + "version": "1", + "method": "shutdown", + "_sid": self.sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + logger.info("Successfully initiated system shutdown") + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to shutdown system: Error code {error_code}") + return False + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + return False + + def reboot_system(self) -> bool: + """Перезагрузка системы""" + if not self.sid and not self.login(): + return False + + try: + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.DSM.System", + "version": "1", + "method": "reboot", + "_sid": self.sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + logger.info("Successfully initiated system reboot") + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to reboot system: Error code {error_code}") + return False + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + return False + + def is_online(self) -> bool: + """Проверка онлайн-статуса Synology NAS""" + try: + socket_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + socket_obj.settimeout(SYNOLOGY_TIMEOUT) + result = socket_obj.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT)) + socket_obj.close() + + return result == 0 + except socket.error: + return False + + def wake_on_lan(self) -> bool: + """Отправка Wake-on-LAN пакета для включения Synology NAS""" + if not SYNOLOGY_MAC: + logger.error("MAC address not configured") + return False + + try: + # Преобразование MAC-адреса в байты + mac_address = SYNOLOGY_MAC.replace(':', '').replace('-', '') + mac_bytes = bytes.fromhex(mac_address) + + # Создание Magic Packet + magic_packet = b'\xff' * 6 + mac_bytes * 16 + + # Отправка пакета + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) + sock.sendto(magic_packet, (SYNOLOGY_HOST, WOL_PORT)) + sock.close() + + logger.info(f"Wake-on-LAN packet sent to {SYNOLOGY_MAC}") + return True + + except Exception as e: + logger.error(f"Error sending Wake-on-LAN packet: {str(e)}") + return False + + def wait_for_boot(self, max_attempts: int = 30, delay: int = 10) -> bool: + """Ожидание загрузки Synology NAS""" + logger.info("Waiting for Synology NAS to boot...") + + for attempt in range(max_attempts): + if self.is_online(): + logger.info(f"Synology NAS is online after {attempt + 1} attempts") + # Дадим дополнительное время для полной загрузки всех сервисов + sleep(delay) + return True + + sleep(delay) + logger.info(f"Waiting for boot... Attempt {attempt + 1}/{max_attempts}") + + logger.error(f"Synology NAS didn't come online after {max_attempts} attempts") + return False + + def power_on(self) -> bool: + """Включение Synology NAS""" + if self.is_online(): + logger.info("Synology NAS is already online") + return True + + # Отправка WoL пакета + if not self.wake_on_lan(): + return False + + # Ожидание загрузки + return self.wait_for_boot() + + def power_off(self) -> bool: + """Выключение Synology NAS""" + if not self.is_online(): + logger.info("Synology NAS is already offline") + return True + + return self.shutdown_system() diff --git a/.history/src/api/synology_20250830065110.py b/.history/src/api/synology_20250830065110.py new file mode 100644 index 0000000..f830e5c --- /dev/null +++ b/.history/src/api/synology_20250830065110.py @@ -0,0 +1,315 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Модуль для взаимодействия с API Synology NAS с использованием библиотеки python-synology +""" + +import requests +import json +import logging +from typing import Dict, Any, Optional, List, Tuple +import socket +import struct +from time import sleep +import urllib3 +from synology import SynologyDSM + +from src.config.config import ( + SYNOLOGY_HOST, + SYNOLOGY_PORT, + SYNOLOGY_USERNAME, + SYNOLOGY_PASSWORD, + SYNOLOGY_SECURE, + SYNOLOGY_TIMEOUT, + SYNOLOGY_MAC, + WOL_PORT +) + +# Отключение предупреждений о небезопасных SSL-соединениях +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + +logger = logging.getLogger(__name__) + +class SynologyAPI: + """Класс для взаимодействия с API Synology NAS с использованием python-synology""" + + def __init__(self): + """Инициализация класса SynologyAPI""" + self.protocol = "https" if SYNOLOGY_SECURE else "http" + self.dsm = None + + def login(self) -> bool: + """Авторизация в API Synology NAS используя python-synology""" + try: + # Создаем экземпляр SynologyDSM + self.dsm = SynologyDSM( + SYNOLOGY_HOST, + port=SYNOLOGY_PORT, + username=SYNOLOGY_USERNAME, + password=SYNOLOGY_PASSWORD, + secure=SYNOLOGY_SECURE, + timeout=SYNOLOGY_TIMEOUT, + verify_ssl=False + ) + + # Авторизация + self.dsm.login() + logger.info("Successfully logged in to Synology NAS") + return True + + except Exception as e: + logger.error(f"Failed to log in to Synology NAS: {str(e)}") + self.dsm = None + return False + + def logout(self) -> bool: + """Выход из API Synology NAS""" + if not self.dsm: + return True + + try: + self.dsm.logout() + self.dsm = None + logger.info("Successfully logged out from Synology NAS") + return True + + except Exception as e: + logger.error(f"Error during logout: {str(e)}") + return False + + def get_system_status(self) -> Optional[Dict[str, Any]]: + """Получение расширенного статуса системы""" + if not self.dsm and not self.login(): + return None + + try: + result = { + "model": self.dsm.information.model, + "version": self.dsm.information.version_string, + "uptime": self.dsm.information.uptime, + "serial": self.dsm.information.serial, + "temperature": self.dsm.information.temperature, + "temperature_unit": "C", + "cpu_usage": self.dsm.utilisation.cpu_total_load, + "memory": { + "total_mb": self.dsm.utilisation.memory_size_mb, + "available_mb": self.dsm.utilisation.memory_available_real_mb, + "usage_percent": self.dsm.utilisation.memory_real_usage + }, + "network": self._get_network_info(), + "volumes": self._get_volumes_info() + } + + logger.info("Successfully fetched extended system status") + return result + + except Exception as e: + logger.error(f"Error getting system status: {str(e)}") + return None + + def _get_network_info(self) -> List[Dict[str, Any]]: + """Получение информации о сетевых интерфейсах""" + try: + result = [] + + # Получение информации о сети + for device in self.dsm.network.interfaces: + net_info = { + "device": device, + "ip": self.dsm.network.get_ip(device), + "mask": self.dsm.network.get_mask(device), + "mac": self.dsm.network.get_mac(device), + "type": self.dsm.network.get_type(device), + "rx_bytes": self.dsm.network.get_rx(device), + "tx_bytes": self.dsm.network.get_tx(device) + } + result.append(net_info) + + return result + + except Exception as e: + logger.error(f"Error getting network info: {str(e)}") + return [] + + def _get_volumes_info(self) -> List[Dict[str, Any]]: + """Получение информации о томах хранения""" + try: + result = [] + + # Получение информации о томах + for volume in self.dsm.storage.volumes: + vol_info = { + "name": volume, + "status": self.dsm.storage.volume_status(volume), + "device_type": self.dsm.storage.volume_device_type(volume), + "total_size": self.dsm.storage.volume_size_total(volume), + "used_size": self.dsm.storage.volume_size_used(volume), + "percent_used": self.dsm.storage.volume_percentage_used(volume) + } + result.append(vol_info) + + return result + + except Exception as e: + logger.error(f"Error getting volumes info: {str(e)}") + return [] + + def get_shared_folders(self) -> List[Dict[str, Any]]: + """Получение списка общих папок""" + if not self.dsm and not self.login(): + return [] + + try: + result = [] + + # Получение информации о общих папках + for folder in self.dsm.share.shares: + share_info = { + "name": folder, + "path": self.dsm.share.get_info(folder).get("path", ""), + "desc": self.dsm.share.get_info(folder).get("desc", "") + } + result.append(share_info) + + logger.info(f"Successfully retrieved {len(result)} shared folders") + return result + + except Exception as e: + logger.error(f"Error getting shared folders: {str(e)}") + return [] + + def get_system_info(self) -> Dict[str, Any]: + """Получение основной информации о системе""" + if not self.dsm and not self.login(): + return {} + + try: + result = { + "model": self.dsm.information.model, + "serial": self.dsm.information.serial, + "version": self.dsm.information.version_string, + "uptime": self.dsm.information.uptime + } + + logger.info("Successfully fetched system info") + return result + + except Exception as e: + logger.error(f"Error getting system info: {str(e)}") + return {} + + def shutdown_system(self) -> bool: + """Выключение системы""" + if not self.dsm and not self.login(): + return False + + try: + # Используем низкоуровневый API для отправки команды выключения + endpoint = "SYNO.DSM.System" + api_path = "entry.cgi" + req_param = {"version": 1, "method": "shutdown"} + + self.dsm.post(endpoint, api_path, req_param) + logger.info("Successfully initiated system shutdown") + return True + + except Exception as e: + logger.error(f"Error shutting down system: {str(e)}") + return False + + def reboot_system(self) -> bool: + """Перезагрузка системы""" + if not self.dsm and not self.login(): + return False + + try: + # Используем низкоуровневый API для отправки команды перезагрузки + endpoint = "SYNO.DSM.System" + api_path = "entry.cgi" + req_param = {"version": 1, "method": "reboot"} + + self.dsm.post(endpoint, api_path, req_param) + logger.info("Successfully initiated system reboot") + return True + + except Exception as e: + logger.error(f"Error rebooting system: {str(e)}") + return False + + def is_online(self) -> bool: + """Проверка онлайн-статуса Synology NAS""" + try: + socket_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + socket_obj.settimeout(SYNOLOGY_TIMEOUT) + result = socket_obj.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT)) + socket_obj.close() + + return result == 0 + except socket.error: + return False + + def wake_on_lan(self) -> bool: + """Отправка Wake-on-LAN пакета для включения Synology NAS""" + if not SYNOLOGY_MAC: + logger.error("MAC address not configured") + return False + + try: + # Преобразование MAC-адреса в байты + mac_address = SYNOLOGY_MAC.replace(':', '').replace('-', '') + mac_bytes = bytes.fromhex(mac_address) + + # Создание Magic Packet + magic_packet = b'\xff' * 6 + mac_bytes * 16 + + # Отправка пакета + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) + sock.sendto(magic_packet, (SYNOLOGY_HOST, WOL_PORT)) + sock.close() + + logger.info(f"Wake-on-LAN packet sent to {SYNOLOGY_MAC}") + return True + + except Exception as e: + logger.error(f"Error sending Wake-on-LAN packet: {str(e)}") + return False + + def wait_for_boot(self, max_attempts: int = 30, delay: int = 10) -> bool: + """Ожидание загрузки Synology NAS""" + logger.info("Waiting for Synology NAS to boot...") + + for attempt in range(max_attempts): + if self.is_online(): + logger.info(f"Synology NAS is online after {attempt + 1} attempts") + # Дадим дополнительное время для полной загрузки всех сервисов + sleep(delay) + return True + + sleep(delay) + logger.info(f"Waiting for boot... Attempt {attempt + 1}/{max_attempts}") + + logger.error(f"Synology NAS didn't come online after {max_attempts} attempts") + return False + + def power_on(self) -> bool: + """Включение Synology NAS""" + if self.is_online(): + logger.info("Synology NAS is already online") + return True + + # Отправка WoL пакета + if not self.wake_on_lan(): + return False + + # Ожидание загрузки + return self.wait_for_boot() + + def power_off(self) -> bool: + """Выключение Synology NAS""" + if not self.is_online(): + logger.info("Synology NAS is already offline") + return True + + return self.shutdown_system() diff --git a/.history/src/api/synology_20250830065154.py b/.history/src/api/synology_20250830065154.py new file mode 100644 index 0000000..9dc9610 --- /dev/null +++ b/.history/src/api/synology_20250830065154.py @@ -0,0 +1,447 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Модуль для взаимодействия с API Synology NAS с использованием библиотеки python-synology +""" + +import requests +import json +import logging +from typing import Dict, Any, Optional, List, Tuple +import socket +import struct +from time import sleep +import urllib3 +from synology import SynologyDSM + +from src.config.config import ( + SYNOLOGY_HOST, + SYNOLOGY_PORT, + SYNOLOGY_USERNAME, + SYNOLOGY_PASSWORD, + SYNOLOGY_SECURE, + SYNOLOGY_TIMEOUT, + SYNOLOGY_MAC, + WOL_PORT +) + +# Отключение предупреждений о небезопасных SSL-соединениях +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + +logger = logging.getLogger(__name__) + +class SynologyAPI: + """Класс для взаимодействия с API Synology NAS с использованием python-synology""" + + def __init__(self): + """Инициализация класса SynologyAPI""" + self.protocol = "https" if SYNOLOGY_SECURE else "http" + self.dsm = None + + def login(self) -> bool: + """Авторизация в API Synology NAS используя python-synology""" + try: + # Создаем экземпляр SynologyDSM + self.dsm = SynologyDSM( + SYNOLOGY_HOST, + port=SYNOLOGY_PORT, + username=SYNOLOGY_USERNAME, + password=SYNOLOGY_PASSWORD, + secure=SYNOLOGY_SECURE, + timeout=SYNOLOGY_TIMEOUT, + verify_ssl=False + ) + + # Авторизация + self.dsm.login() + logger.info("Successfully logged in to Synology NAS") + return True + + except Exception as e: + logger.error(f"Failed to log in to Synology NAS: {str(e)}") + self.dsm = None + return False + + def logout(self) -> bool: + """Выход из API Synology NAS""" + if not self.dsm: + return True + + try: + self.dsm.logout() + self.dsm = None + logger.info("Successfully logged out from Synology NAS") + return True + + except Exception as e: + logger.error(f"Error during logout: {str(e)}") + return False + + def get_system_status(self) -> Optional[Dict[str, Any]]: + """Получение расширенного статуса системы""" + if not self.dsm and not self.login(): + return None + + try: + result = { + "model": self.dsm.information.model, + "version": self.dsm.information.version_string, + "uptime": self.dsm.information.uptime, + "serial": self.dsm.information.serial, + "temperature": self.dsm.information.temperature, + "temperature_unit": "C", + "cpu_usage": self.dsm.utilisation.cpu_total_load, + "memory": { + "total_mb": self.dsm.utilisation.memory_size_mb, + "available_mb": self.dsm.utilisation.memory_available_real_mb, + "usage_percent": self.dsm.utilisation.memory_real_usage + }, + "network": self._get_network_info(), + "volumes": self._get_volumes_info() + } + + logger.info("Successfully fetched extended system status") + return result + + except Exception as e: + logger.error(f"Error getting system status: {str(e)}") + return None + + def _get_network_info(self) -> List[Dict[str, Any]]: + """Получение информации о сетевых интерфейсах""" + try: + result = [] + + # Получение информации о сети + for device in self.dsm.network.interfaces: + net_info = { + "device": device, + "ip": self.dsm.network.get_ip(device), + "mask": self.dsm.network.get_mask(device), + "mac": self.dsm.network.get_mac(device), + "type": self.dsm.network.get_type(device), + "rx_bytes": self.dsm.network.get_rx(device), + "tx_bytes": self.dsm.network.get_tx(device) + } + result.append(net_info) + + return result + + except Exception as e: + logger.error(f"Error getting network info: {str(e)}") + return [] + + def _get_volumes_info(self) -> List[Dict[str, Any]]: + """Получение информации о томах хранения""" + try: + result = [] + + # Получение информации о томах + for volume in self.dsm.storage.volumes: + vol_info = { + "name": volume, + "status": self.dsm.storage.volume_status(volume), + "device_type": self.dsm.storage.volume_device_type(volume), + "total_size": self.dsm.storage.volume_size_total(volume), + "used_size": self.dsm.storage.volume_size_used(volume), + "percent_used": self.dsm.storage.volume_percentage_used(volume) + } + result.append(vol_info) + + return result + + except Exception as e: + logger.error(f"Error getting volumes info: {str(e)}") + return [] + + def get_shared_folders(self) -> List[Dict[str, Any]]: + """Получение списка общих папок""" + if not self.dsm and not self.login(): + return [] + + try: + result = [] + + # Получение информации о общих папках + for folder in self.dsm.share.shares: + share_info = { + "name": folder, + "path": self.dsm.share.get_info(folder).get("path", ""), + "desc": self.dsm.share.get_info(folder).get("desc", "") + } + result.append(share_info) + + logger.info(f"Successfully retrieved {len(result)} shared folders") + return result + + except Exception as e: + logger.error(f"Error getting shared folders: {str(e)}") + return [] + + def get_system_info(self) -> Dict[str, Any]: + """Получение основной информации о системе""" + if not self.dsm and not self.login(): + return {} + + try: + result = { + "model": self.dsm.information.model, + "serial": self.dsm.information.serial, + "version": self.dsm.information.version_string, + "uptime": self.dsm.information.uptime + } + + logger.info("Successfully fetched system info") + return result + + except Exception as e: + logger.error(f"Error getting system info: {str(e)}") + return {} + + def shutdown_system(self) -> bool: + """Выключение системы""" + if not self.dsm and not self.login(): + return False + + try: + # Используем низкоуровневый API для отправки команды выключения + endpoint = "SYNO.DSM.System" + api_path = "entry.cgi" + req_param = {"version": 1, "method": "shutdown"} + + self.dsm.post(endpoint, api_path, req_param) + logger.info("Successfully initiated system shutdown") + return True + + except Exception as e: + logger.error(f"Error shutting down system: {str(e)}") + return False + + def reboot_system(self) -> bool: + """Перезагрузка системы""" + if not self.dsm and not self.login(): + return False + + try: + # Используем низкоуровневый API для отправки команды перезагрузки + endpoint = "SYNO.DSM.System" + api_path = "entry.cgi" + req_param = {"version": 1, "method": "reboot"} + + self.dsm.post(endpoint, api_path, req_param) + logger.info("Successfully initiated system reboot") + return True + + except Exception as e: + logger.error(f"Error rebooting system: {str(e)}") + return False + + def is_online(self) -> bool: + """Проверка онлайн-статуса Synology NAS""" + try: + socket_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + socket_obj.settimeout(SYNOLOGY_TIMEOUT) + result = socket_obj.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT)) + socket_obj.close() + + return result == 0 + except socket.error: + return False + + def wake_on_lan(self) -> bool: + """Отправка Wake-on-LAN пакета для включения Synology NAS""" + if not SYNOLOGY_MAC: + logger.error("MAC address not configured") + return False + + try: + # Преобразование MAC-адреса в байты + mac_address = SYNOLOGY_MAC.replace(':', '').replace('-', '') + mac_bytes = bytes.fromhex(mac_address) + + # Создание Magic Packet + magic_packet = b'\xff' * 6 + mac_bytes * 16 + + # Отправка пакета + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) + sock.sendto(magic_packet, (SYNOLOGY_HOST, WOL_PORT)) + sock.close() + + logger.info(f"Wake-on-LAN packet sent to {SYNOLOGY_MAC}") + return True + + except Exception as e: + logger.error(f"Error sending Wake-on-LAN packet: {str(e)}") + return False + + def wait_for_boot(self, max_attempts: int = 30, delay: int = 10) -> bool: + """Ожидание загрузки Synology NAS""" + logger.info("Waiting for Synology NAS to boot...") + + for attempt in range(max_attempts): + if self.is_online(): + logger.info(f"Synology NAS is online after {attempt + 1} attempts") + # Дадим дополнительное время для полной загрузки всех сервисов + sleep(delay) + return True + + sleep(delay) + logger.info(f"Waiting for boot... Attempt {attempt + 1}/{max_attempts}") + + logger.error(f"Synology NAS didn't come online after {max_attempts} attempts") + return False + + def power_on(self) -> bool: + """Включение Synology NAS""" + if self.is_online(): + logger.info("Synology NAS is already online") + return True + + # Отправка WoL пакета + if not self.wake_on_lan(): + return False + + # Ожидание загрузки + return self.wait_for_boot() + + def power_off(self) -> bool: + """Выключение Synology NAS""" + if not self.is_online(): + logger.info("Synology NAS is already offline") + return True + + return self.shutdown_system() + + def get_system_load(self) -> Dict[str, Any]: + """Получение информации о загрузке системы""" + if not self.dsm and not self.login(): + return {} + + try: + result = { + "cpu_load": self.dsm.utilisation.cpu_total_load, + "memory": { + "total_mb": self.dsm.utilisation.memory_size_mb, + "available_mb": self.dsm.utilisation.memory_available_real_mb, + "cached_mb": self.dsm.utilisation.memory_cached_mb, + "usage_percent": self.dsm.utilisation.memory_real_usage + }, + "network": {} + } + + # Добавляем данные по сети + for device in self.dsm.network.interfaces: + result["network"][device] = { + "rx_bytes": self.dsm.network.get_rx(device), + "tx_bytes": self.dsm.network.get_tx(device) + } + + logger.info("Successfully fetched system load") + return result + + except Exception as e: + logger.error(f"Error getting system load: {str(e)}") + return {} + + def get_storage_status(self) -> Dict[str, Any]: + """Получение подробной информации о хранилище""" + if not self.dsm and not self.login(): + return {} + + try: + result = { + "volumes": self._get_volumes_info(), + "disks": self._get_disks_info(), + "total_size": 0, + "total_used": 0 + } + + # Суммируем общий размер и использование + for volume in result["volumes"]: + result["total_size"] += volume["total_size"] + result["total_used"] += volume["used_size"] + + logger.info("Successfully fetched storage status") + return result + + except Exception as e: + logger.error(f"Error getting storage status: {str(e)}") + return {} + + def _get_disks_info(self) -> List[Dict[str, Any]]: + """Получение информации о дисках""" + try: + result = [] + + # Получение информации о дисках + for disk in self.dsm.storage.disks: + disk_info = { + "name": disk, + "model": self.dsm.storage.disk_model(disk), + "type": self.dsm.storage.disk_type(disk), + "status": self.dsm.storage.disk_status(disk), + "temp": self.dsm.storage.disk_temp(disk) + } + result.append(disk_info) + + return result + + except Exception as e: + logger.error(f"Error getting disks info: {str(e)}") + return [] + + def get_security_status(self) -> Dict[str, bool]: + """Получение информации о состоянии безопасности""" + if not self.dsm and not self.login(): + return {"success": False} + + try: + # Используем низкоуровневый API для получения информации о безопасности + endpoint = "SYNO.Core.Security.DSM" + api_path = "entry.cgi" + req_param = {"version": 1, "method": "status"} + + response = self.dsm.get(endpoint, api_path, req_param) + + if response and "data" in response: + return { + "success": True, + "status": response["data"].get("status", "unknown"), + "last_check": response["data"].get("last_check", None), + "is_secure": response["data"].get("is_secure", False) + } + else: + return {"success": False} + + except Exception as e: + logger.error(f"Error getting security status: {str(e)}") + return {"success": False} + + def get_users(self) -> List[str]: + """Получение списка пользователей""" + if not self.dsm and not self.login(): + return [] + + try: + users = [] + + # Используем низкоуровневый API для получения списка пользователей + endpoint = "SYNO.Core.User" + api_path = "entry.cgi" + req_param = {"version": 1, "method": "list", "additional": ["email"]} + + response = self.dsm.get(endpoint, api_path, req_param) + + if response and "data" in response and "users" in response["data"]: + for user in response["data"]["users"]: + if "name" in user: + users.append(user["name"]) + + logger.info(f"Successfully retrieved {len(users)} users") + return users + + except Exception as e: + logger.error(f"Error getting users: {str(e)}") + return [] diff --git a/.history/src/api/synology_20250830065454.py b/.history/src/api/synology_20250830065454.py new file mode 100644 index 0000000..9dc9610 --- /dev/null +++ b/.history/src/api/synology_20250830065454.py @@ -0,0 +1,447 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Модуль для взаимодействия с API Synology NAS с использованием библиотеки python-synology +""" + +import requests +import json +import logging +from typing import Dict, Any, Optional, List, Tuple +import socket +import struct +from time import sleep +import urllib3 +from synology import SynologyDSM + +from src.config.config import ( + SYNOLOGY_HOST, + SYNOLOGY_PORT, + SYNOLOGY_USERNAME, + SYNOLOGY_PASSWORD, + SYNOLOGY_SECURE, + SYNOLOGY_TIMEOUT, + SYNOLOGY_MAC, + WOL_PORT +) + +# Отключение предупреждений о небезопасных SSL-соединениях +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + +logger = logging.getLogger(__name__) + +class SynologyAPI: + """Класс для взаимодействия с API Synology NAS с использованием python-synology""" + + def __init__(self): + """Инициализация класса SynologyAPI""" + self.protocol = "https" if SYNOLOGY_SECURE else "http" + self.dsm = None + + def login(self) -> bool: + """Авторизация в API Synology NAS используя python-synology""" + try: + # Создаем экземпляр SynologyDSM + self.dsm = SynologyDSM( + SYNOLOGY_HOST, + port=SYNOLOGY_PORT, + username=SYNOLOGY_USERNAME, + password=SYNOLOGY_PASSWORD, + secure=SYNOLOGY_SECURE, + timeout=SYNOLOGY_TIMEOUT, + verify_ssl=False + ) + + # Авторизация + self.dsm.login() + logger.info("Successfully logged in to Synology NAS") + return True + + except Exception as e: + logger.error(f"Failed to log in to Synology NAS: {str(e)}") + self.dsm = None + return False + + def logout(self) -> bool: + """Выход из API Synology NAS""" + if not self.dsm: + return True + + try: + self.dsm.logout() + self.dsm = None + logger.info("Successfully logged out from Synology NAS") + return True + + except Exception as e: + logger.error(f"Error during logout: {str(e)}") + return False + + def get_system_status(self) -> Optional[Dict[str, Any]]: + """Получение расширенного статуса системы""" + if not self.dsm and not self.login(): + return None + + try: + result = { + "model": self.dsm.information.model, + "version": self.dsm.information.version_string, + "uptime": self.dsm.information.uptime, + "serial": self.dsm.information.serial, + "temperature": self.dsm.information.temperature, + "temperature_unit": "C", + "cpu_usage": self.dsm.utilisation.cpu_total_load, + "memory": { + "total_mb": self.dsm.utilisation.memory_size_mb, + "available_mb": self.dsm.utilisation.memory_available_real_mb, + "usage_percent": self.dsm.utilisation.memory_real_usage + }, + "network": self._get_network_info(), + "volumes": self._get_volumes_info() + } + + logger.info("Successfully fetched extended system status") + return result + + except Exception as e: + logger.error(f"Error getting system status: {str(e)}") + return None + + def _get_network_info(self) -> List[Dict[str, Any]]: + """Получение информации о сетевых интерфейсах""" + try: + result = [] + + # Получение информации о сети + for device in self.dsm.network.interfaces: + net_info = { + "device": device, + "ip": self.dsm.network.get_ip(device), + "mask": self.dsm.network.get_mask(device), + "mac": self.dsm.network.get_mac(device), + "type": self.dsm.network.get_type(device), + "rx_bytes": self.dsm.network.get_rx(device), + "tx_bytes": self.dsm.network.get_tx(device) + } + result.append(net_info) + + return result + + except Exception as e: + logger.error(f"Error getting network info: {str(e)}") + return [] + + def _get_volumes_info(self) -> List[Dict[str, Any]]: + """Получение информации о томах хранения""" + try: + result = [] + + # Получение информации о томах + for volume in self.dsm.storage.volumes: + vol_info = { + "name": volume, + "status": self.dsm.storage.volume_status(volume), + "device_type": self.dsm.storage.volume_device_type(volume), + "total_size": self.dsm.storage.volume_size_total(volume), + "used_size": self.dsm.storage.volume_size_used(volume), + "percent_used": self.dsm.storage.volume_percentage_used(volume) + } + result.append(vol_info) + + return result + + except Exception as e: + logger.error(f"Error getting volumes info: {str(e)}") + return [] + + def get_shared_folders(self) -> List[Dict[str, Any]]: + """Получение списка общих папок""" + if not self.dsm and not self.login(): + return [] + + try: + result = [] + + # Получение информации о общих папках + for folder in self.dsm.share.shares: + share_info = { + "name": folder, + "path": self.dsm.share.get_info(folder).get("path", ""), + "desc": self.dsm.share.get_info(folder).get("desc", "") + } + result.append(share_info) + + logger.info(f"Successfully retrieved {len(result)} shared folders") + return result + + except Exception as e: + logger.error(f"Error getting shared folders: {str(e)}") + return [] + + def get_system_info(self) -> Dict[str, Any]: + """Получение основной информации о системе""" + if not self.dsm and not self.login(): + return {} + + try: + result = { + "model": self.dsm.information.model, + "serial": self.dsm.information.serial, + "version": self.dsm.information.version_string, + "uptime": self.dsm.information.uptime + } + + logger.info("Successfully fetched system info") + return result + + except Exception as e: + logger.error(f"Error getting system info: {str(e)}") + return {} + + def shutdown_system(self) -> bool: + """Выключение системы""" + if not self.dsm and not self.login(): + return False + + try: + # Используем низкоуровневый API для отправки команды выключения + endpoint = "SYNO.DSM.System" + api_path = "entry.cgi" + req_param = {"version": 1, "method": "shutdown"} + + self.dsm.post(endpoint, api_path, req_param) + logger.info("Successfully initiated system shutdown") + return True + + except Exception as e: + logger.error(f"Error shutting down system: {str(e)}") + return False + + def reboot_system(self) -> bool: + """Перезагрузка системы""" + if not self.dsm and not self.login(): + return False + + try: + # Используем низкоуровневый API для отправки команды перезагрузки + endpoint = "SYNO.DSM.System" + api_path = "entry.cgi" + req_param = {"version": 1, "method": "reboot"} + + self.dsm.post(endpoint, api_path, req_param) + logger.info("Successfully initiated system reboot") + return True + + except Exception as e: + logger.error(f"Error rebooting system: {str(e)}") + return False + + def is_online(self) -> bool: + """Проверка онлайн-статуса Synology NAS""" + try: + socket_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + socket_obj.settimeout(SYNOLOGY_TIMEOUT) + result = socket_obj.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT)) + socket_obj.close() + + return result == 0 + except socket.error: + return False + + def wake_on_lan(self) -> bool: + """Отправка Wake-on-LAN пакета для включения Synology NAS""" + if not SYNOLOGY_MAC: + logger.error("MAC address not configured") + return False + + try: + # Преобразование MAC-адреса в байты + mac_address = SYNOLOGY_MAC.replace(':', '').replace('-', '') + mac_bytes = bytes.fromhex(mac_address) + + # Создание Magic Packet + magic_packet = b'\xff' * 6 + mac_bytes * 16 + + # Отправка пакета + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) + sock.sendto(magic_packet, (SYNOLOGY_HOST, WOL_PORT)) + sock.close() + + logger.info(f"Wake-on-LAN packet sent to {SYNOLOGY_MAC}") + return True + + except Exception as e: + logger.error(f"Error sending Wake-on-LAN packet: {str(e)}") + return False + + def wait_for_boot(self, max_attempts: int = 30, delay: int = 10) -> bool: + """Ожидание загрузки Synology NAS""" + logger.info("Waiting for Synology NAS to boot...") + + for attempt in range(max_attempts): + if self.is_online(): + logger.info(f"Synology NAS is online after {attempt + 1} attempts") + # Дадим дополнительное время для полной загрузки всех сервисов + sleep(delay) + return True + + sleep(delay) + logger.info(f"Waiting for boot... Attempt {attempt + 1}/{max_attempts}") + + logger.error(f"Synology NAS didn't come online after {max_attempts} attempts") + return False + + def power_on(self) -> bool: + """Включение Synology NAS""" + if self.is_online(): + logger.info("Synology NAS is already online") + return True + + # Отправка WoL пакета + if not self.wake_on_lan(): + return False + + # Ожидание загрузки + return self.wait_for_boot() + + def power_off(self) -> bool: + """Выключение Synology NAS""" + if not self.is_online(): + logger.info("Synology NAS is already offline") + return True + + return self.shutdown_system() + + def get_system_load(self) -> Dict[str, Any]: + """Получение информации о загрузке системы""" + if not self.dsm and not self.login(): + return {} + + try: + result = { + "cpu_load": self.dsm.utilisation.cpu_total_load, + "memory": { + "total_mb": self.dsm.utilisation.memory_size_mb, + "available_mb": self.dsm.utilisation.memory_available_real_mb, + "cached_mb": self.dsm.utilisation.memory_cached_mb, + "usage_percent": self.dsm.utilisation.memory_real_usage + }, + "network": {} + } + + # Добавляем данные по сети + for device in self.dsm.network.interfaces: + result["network"][device] = { + "rx_bytes": self.dsm.network.get_rx(device), + "tx_bytes": self.dsm.network.get_tx(device) + } + + logger.info("Successfully fetched system load") + return result + + except Exception as e: + logger.error(f"Error getting system load: {str(e)}") + return {} + + def get_storage_status(self) -> Dict[str, Any]: + """Получение подробной информации о хранилище""" + if not self.dsm and not self.login(): + return {} + + try: + result = { + "volumes": self._get_volumes_info(), + "disks": self._get_disks_info(), + "total_size": 0, + "total_used": 0 + } + + # Суммируем общий размер и использование + for volume in result["volumes"]: + result["total_size"] += volume["total_size"] + result["total_used"] += volume["used_size"] + + logger.info("Successfully fetched storage status") + return result + + except Exception as e: + logger.error(f"Error getting storage status: {str(e)}") + return {} + + def _get_disks_info(self) -> List[Dict[str, Any]]: + """Получение информации о дисках""" + try: + result = [] + + # Получение информации о дисках + for disk in self.dsm.storage.disks: + disk_info = { + "name": disk, + "model": self.dsm.storage.disk_model(disk), + "type": self.dsm.storage.disk_type(disk), + "status": self.dsm.storage.disk_status(disk), + "temp": self.dsm.storage.disk_temp(disk) + } + result.append(disk_info) + + return result + + except Exception as e: + logger.error(f"Error getting disks info: {str(e)}") + return [] + + def get_security_status(self) -> Dict[str, bool]: + """Получение информации о состоянии безопасности""" + if not self.dsm and not self.login(): + return {"success": False} + + try: + # Используем низкоуровневый API для получения информации о безопасности + endpoint = "SYNO.Core.Security.DSM" + api_path = "entry.cgi" + req_param = {"version": 1, "method": "status"} + + response = self.dsm.get(endpoint, api_path, req_param) + + if response and "data" in response: + return { + "success": True, + "status": response["data"].get("status", "unknown"), + "last_check": response["data"].get("last_check", None), + "is_secure": response["data"].get("is_secure", False) + } + else: + return {"success": False} + + except Exception as e: + logger.error(f"Error getting security status: {str(e)}") + return {"success": False} + + def get_users(self) -> List[str]: + """Получение списка пользователей""" + if not self.dsm and not self.login(): + return [] + + try: + users = [] + + # Используем низкоуровневый API для получения списка пользователей + endpoint = "SYNO.Core.User" + api_path = "entry.cgi" + req_param = {"version": 1, "method": "list", "additional": ["email"]} + + response = self.dsm.get(endpoint, api_path, req_param) + + if response and "data" in response and "users" in response["data"]: + for user in response["data"]["users"]: + if "name" in user: + users.append(user["name"]) + + logger.info(f"Successfully retrieved {len(users)} users") + return users + + except Exception as e: + logger.error(f"Error getting users: {str(e)}") + return [] diff --git a/.history/src/api/synology_20250830071505.py b/.history/src/api/synology_20250830071505.py new file mode 100644 index 0000000..fe5d0a9 --- /dev/null +++ b/.history/src/api/synology_20250830071505.py @@ -0,0 +1,447 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Модуль для взаимодействия с API Synology NAS с использованием библиотеки python-synology +""" + +import requests +import json +import logging +from typing import Dict, Any, Optional, List, Tuple +import socket +import struct +from time import sleep +import urllib3 +from synology_dsm import SynologyDSM + +from src.config.config import ( + SYNOLOGY_HOST, + SYNOLOGY_PORT, + SYNOLOGY_USERNAME, + SYNOLOGY_PASSWORD, + SYNOLOGY_SECURE, + SYNOLOGY_TIMEOUT, + SYNOLOGY_MAC, + WOL_PORT +) + +# Отключение предупреждений о небезопасных SSL-соединениях +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + +logger = logging.getLogger(__name__) + +class SynologyAPI: + """Класс для взаимодействия с API Synology NAS с использованием python-synology""" + + def __init__(self): + """Инициализация класса SynologyAPI""" + self.protocol = "https" if SYNOLOGY_SECURE else "http" + self.dsm = None + + def login(self) -> bool: + """Авторизация в API Synology NAS используя python-synology""" + try: + # Создаем экземпляр SynologyDSM + self.dsm = SynologyDSM( + SYNOLOGY_HOST, + port=SYNOLOGY_PORT, + username=SYNOLOGY_USERNAME, + password=SYNOLOGY_PASSWORD, + secure=SYNOLOGY_SECURE, + timeout=SYNOLOGY_TIMEOUT, + verify_ssl=False + ) + + # Авторизация + self.dsm.login() + logger.info("Successfully logged in to Synology NAS") + return True + + except Exception as e: + logger.error(f"Failed to log in to Synology NAS: {str(e)}") + self.dsm = None + return False + + def logout(self) -> bool: + """Выход из API Synology NAS""" + if not self.dsm: + return True + + try: + self.dsm.logout() + self.dsm = None + logger.info("Successfully logged out from Synology NAS") + return True + + except Exception as e: + logger.error(f"Error during logout: {str(e)}") + return False + + def get_system_status(self) -> Optional[Dict[str, Any]]: + """Получение расширенного статуса системы""" + if not self.dsm and not self.login(): + return None + + try: + result = { + "model": self.dsm.information.model, + "version": self.dsm.information.version_string, + "uptime": self.dsm.information.uptime, + "serial": self.dsm.information.serial, + "temperature": self.dsm.information.temperature, + "temperature_unit": "C", + "cpu_usage": self.dsm.utilisation.cpu_total_load, + "memory": { + "total_mb": self.dsm.utilisation.memory_size_mb, + "available_mb": self.dsm.utilisation.memory_available_real_mb, + "usage_percent": self.dsm.utilisation.memory_real_usage + }, + "network": self._get_network_info(), + "volumes": self._get_volumes_info() + } + + logger.info("Successfully fetched extended system status") + return result + + except Exception as e: + logger.error(f"Error getting system status: {str(e)}") + return None + + def _get_network_info(self) -> List[Dict[str, Any]]: + """Получение информации о сетевых интерфейсах""" + try: + result = [] + + # Получение информации о сети + for device in self.dsm.network.interfaces: + net_info = { + "device": device, + "ip": self.dsm.network.get_ip(device), + "mask": self.dsm.network.get_mask(device), + "mac": self.dsm.network.get_mac(device), + "type": self.dsm.network.get_type(device), + "rx_bytes": self.dsm.network.get_rx(device), + "tx_bytes": self.dsm.network.get_tx(device) + } + result.append(net_info) + + return result + + except Exception as e: + logger.error(f"Error getting network info: {str(e)}") + return [] + + def _get_volumes_info(self) -> List[Dict[str, Any]]: + """Получение информации о томах хранения""" + try: + result = [] + + # Получение информации о томах + for volume in self.dsm.storage.volumes: + vol_info = { + "name": volume, + "status": self.dsm.storage.volume_status(volume), + "device_type": self.dsm.storage.volume_device_type(volume), + "total_size": self.dsm.storage.volume_size_total(volume), + "used_size": self.dsm.storage.volume_size_used(volume), + "percent_used": self.dsm.storage.volume_percentage_used(volume) + } + result.append(vol_info) + + return result + + except Exception as e: + logger.error(f"Error getting volumes info: {str(e)}") + return [] + + def get_shared_folders(self) -> List[Dict[str, Any]]: + """Получение списка общих папок""" + if not self.dsm and not self.login(): + return [] + + try: + result = [] + + # Получение информации о общих папках + for folder in self.dsm.share.shares: + share_info = { + "name": folder, + "path": self.dsm.share.get_info(folder).get("path", ""), + "desc": self.dsm.share.get_info(folder).get("desc", "") + } + result.append(share_info) + + logger.info(f"Successfully retrieved {len(result)} shared folders") + return result + + except Exception as e: + logger.error(f"Error getting shared folders: {str(e)}") + return [] + + def get_system_info(self) -> Dict[str, Any]: + """Получение основной информации о системе""" + if not self.dsm and not self.login(): + return {} + + try: + result = { + "model": self.dsm.information.model, + "serial": self.dsm.information.serial, + "version": self.dsm.information.version_string, + "uptime": self.dsm.information.uptime + } + + logger.info("Successfully fetched system info") + return result + + except Exception as e: + logger.error(f"Error getting system info: {str(e)}") + return {} + + def shutdown_system(self) -> bool: + """Выключение системы""" + if not self.dsm and not self.login(): + return False + + try: + # Используем низкоуровневый API для отправки команды выключения + endpoint = "SYNO.DSM.System" + api_path = "entry.cgi" + req_param = {"version": 1, "method": "shutdown"} + + self.dsm.post(endpoint, api_path, req_param) + logger.info("Successfully initiated system shutdown") + return True + + except Exception as e: + logger.error(f"Error shutting down system: {str(e)}") + return False + + def reboot_system(self) -> bool: + """Перезагрузка системы""" + if not self.dsm and not self.login(): + return False + + try: + # Используем низкоуровневый API для отправки команды перезагрузки + endpoint = "SYNO.DSM.System" + api_path = "entry.cgi" + req_param = {"version": 1, "method": "reboot"} + + self.dsm.post(endpoint, api_path, req_param) + logger.info("Successfully initiated system reboot") + return True + + except Exception as e: + logger.error(f"Error rebooting system: {str(e)}") + return False + + def is_online(self) -> bool: + """Проверка онлайн-статуса Synology NAS""" + try: + socket_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + socket_obj.settimeout(SYNOLOGY_TIMEOUT) + result = socket_obj.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT)) + socket_obj.close() + + return result == 0 + except socket.error: + return False + + def wake_on_lan(self) -> bool: + """Отправка Wake-on-LAN пакета для включения Synology NAS""" + if not SYNOLOGY_MAC: + logger.error("MAC address not configured") + return False + + try: + # Преобразование MAC-адреса в байты + mac_address = SYNOLOGY_MAC.replace(':', '').replace('-', '') + mac_bytes = bytes.fromhex(mac_address) + + # Создание Magic Packet + magic_packet = b'\xff' * 6 + mac_bytes * 16 + + # Отправка пакета + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) + sock.sendto(magic_packet, (SYNOLOGY_HOST, WOL_PORT)) + sock.close() + + logger.info(f"Wake-on-LAN packet sent to {SYNOLOGY_MAC}") + return True + + except Exception as e: + logger.error(f"Error sending Wake-on-LAN packet: {str(e)}") + return False + + def wait_for_boot(self, max_attempts: int = 30, delay: int = 10) -> bool: + """Ожидание загрузки Synology NAS""" + logger.info("Waiting for Synology NAS to boot...") + + for attempt in range(max_attempts): + if self.is_online(): + logger.info(f"Synology NAS is online after {attempt + 1} attempts") + # Дадим дополнительное время для полной загрузки всех сервисов + sleep(delay) + return True + + sleep(delay) + logger.info(f"Waiting for boot... Attempt {attempt + 1}/{max_attempts}") + + logger.error(f"Synology NAS didn't come online after {max_attempts} attempts") + return False + + def power_on(self) -> bool: + """Включение Synology NAS""" + if self.is_online(): + logger.info("Synology NAS is already online") + return True + + # Отправка WoL пакета + if not self.wake_on_lan(): + return False + + # Ожидание загрузки + return self.wait_for_boot() + + def power_off(self) -> bool: + """Выключение Synology NAS""" + if not self.is_online(): + logger.info("Synology NAS is already offline") + return True + + return self.shutdown_system() + + def get_system_load(self) -> Dict[str, Any]: + """Получение информации о загрузке системы""" + if not self.dsm and not self.login(): + return {} + + try: + result = { + "cpu_load": self.dsm.utilisation.cpu_total_load, + "memory": { + "total_mb": self.dsm.utilisation.memory_size_mb, + "available_mb": self.dsm.utilisation.memory_available_real_mb, + "cached_mb": self.dsm.utilisation.memory_cached_mb, + "usage_percent": self.dsm.utilisation.memory_real_usage + }, + "network": {} + } + + # Добавляем данные по сети + for device in self.dsm.network.interfaces: + result["network"][device] = { + "rx_bytes": self.dsm.network.get_rx(device), + "tx_bytes": self.dsm.network.get_tx(device) + } + + logger.info("Successfully fetched system load") + return result + + except Exception as e: + logger.error(f"Error getting system load: {str(e)}") + return {} + + def get_storage_status(self) -> Dict[str, Any]: + """Получение подробной информации о хранилище""" + if not self.dsm and not self.login(): + return {} + + try: + result = { + "volumes": self._get_volumes_info(), + "disks": self._get_disks_info(), + "total_size": 0, + "total_used": 0 + } + + # Суммируем общий размер и использование + for volume in result["volumes"]: + result["total_size"] += volume["total_size"] + result["total_used"] += volume["used_size"] + + logger.info("Successfully fetched storage status") + return result + + except Exception as e: + logger.error(f"Error getting storage status: {str(e)}") + return {} + + def _get_disks_info(self) -> List[Dict[str, Any]]: + """Получение информации о дисках""" + try: + result = [] + + # Получение информации о дисках + for disk in self.dsm.storage.disks: + disk_info = { + "name": disk, + "model": self.dsm.storage.disk_model(disk), + "type": self.dsm.storage.disk_type(disk), + "status": self.dsm.storage.disk_status(disk), + "temp": self.dsm.storage.disk_temp(disk) + } + result.append(disk_info) + + return result + + except Exception as e: + logger.error(f"Error getting disks info: {str(e)}") + return [] + + def get_security_status(self) -> Dict[str, bool]: + """Получение информации о состоянии безопасности""" + if not self.dsm and not self.login(): + return {"success": False} + + try: + # Используем низкоуровневый API для получения информации о безопасности + endpoint = "SYNO.Core.Security.DSM" + api_path = "entry.cgi" + req_param = {"version": 1, "method": "status"} + + response = self.dsm.get(endpoint, api_path, req_param) + + if response and "data" in response: + return { + "success": True, + "status": response["data"].get("status", "unknown"), + "last_check": response["data"].get("last_check", None), + "is_secure": response["data"].get("is_secure", False) + } + else: + return {"success": False} + + except Exception as e: + logger.error(f"Error getting security status: {str(e)}") + return {"success": False} + + def get_users(self) -> List[str]: + """Получение списка пользователей""" + if not self.dsm and not self.login(): + return [] + + try: + users = [] + + # Используем низкоуровневый API для получения списка пользователей + endpoint = "SYNO.Core.User" + api_path = "entry.cgi" + req_param = {"version": 1, "method": "list", "additional": ["email"]} + + response = self.dsm.get(endpoint, api_path, req_param) + + if response and "data" in response and "users" in response["data"]: + for user in response["data"]["users"]: + if "name" in user: + users.append(user["name"]) + + logger.info(f"Successfully retrieved {len(users)} users") + return users + + except Exception as e: + logger.error(f"Error getting users: {str(e)}") + return [] diff --git a/.history/src/api/synology_20250830071525.py b/.history/src/api/synology_20250830071525.py new file mode 100644 index 0000000..fe5d0a9 --- /dev/null +++ b/.history/src/api/synology_20250830071525.py @@ -0,0 +1,447 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Модуль для взаимодействия с API Synology NAS с использованием библиотеки python-synology +""" + +import requests +import json +import logging +from typing import Dict, Any, Optional, List, Tuple +import socket +import struct +from time import sleep +import urllib3 +from synology_dsm import SynologyDSM + +from src.config.config import ( + SYNOLOGY_HOST, + SYNOLOGY_PORT, + SYNOLOGY_USERNAME, + SYNOLOGY_PASSWORD, + SYNOLOGY_SECURE, + SYNOLOGY_TIMEOUT, + SYNOLOGY_MAC, + WOL_PORT +) + +# Отключение предупреждений о небезопасных SSL-соединениях +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + +logger = logging.getLogger(__name__) + +class SynologyAPI: + """Класс для взаимодействия с API Synology NAS с использованием python-synology""" + + def __init__(self): + """Инициализация класса SynologyAPI""" + self.protocol = "https" if SYNOLOGY_SECURE else "http" + self.dsm = None + + def login(self) -> bool: + """Авторизация в API Synology NAS используя python-synology""" + try: + # Создаем экземпляр SynologyDSM + self.dsm = SynologyDSM( + SYNOLOGY_HOST, + port=SYNOLOGY_PORT, + username=SYNOLOGY_USERNAME, + password=SYNOLOGY_PASSWORD, + secure=SYNOLOGY_SECURE, + timeout=SYNOLOGY_TIMEOUT, + verify_ssl=False + ) + + # Авторизация + self.dsm.login() + logger.info("Successfully logged in to Synology NAS") + return True + + except Exception as e: + logger.error(f"Failed to log in to Synology NAS: {str(e)}") + self.dsm = None + return False + + def logout(self) -> bool: + """Выход из API Synology NAS""" + if not self.dsm: + return True + + try: + self.dsm.logout() + self.dsm = None + logger.info("Successfully logged out from Synology NAS") + return True + + except Exception as e: + logger.error(f"Error during logout: {str(e)}") + return False + + def get_system_status(self) -> Optional[Dict[str, Any]]: + """Получение расширенного статуса системы""" + if not self.dsm and not self.login(): + return None + + try: + result = { + "model": self.dsm.information.model, + "version": self.dsm.information.version_string, + "uptime": self.dsm.information.uptime, + "serial": self.dsm.information.serial, + "temperature": self.dsm.information.temperature, + "temperature_unit": "C", + "cpu_usage": self.dsm.utilisation.cpu_total_load, + "memory": { + "total_mb": self.dsm.utilisation.memory_size_mb, + "available_mb": self.dsm.utilisation.memory_available_real_mb, + "usage_percent": self.dsm.utilisation.memory_real_usage + }, + "network": self._get_network_info(), + "volumes": self._get_volumes_info() + } + + logger.info("Successfully fetched extended system status") + return result + + except Exception as e: + logger.error(f"Error getting system status: {str(e)}") + return None + + def _get_network_info(self) -> List[Dict[str, Any]]: + """Получение информации о сетевых интерфейсах""" + try: + result = [] + + # Получение информации о сети + for device in self.dsm.network.interfaces: + net_info = { + "device": device, + "ip": self.dsm.network.get_ip(device), + "mask": self.dsm.network.get_mask(device), + "mac": self.dsm.network.get_mac(device), + "type": self.dsm.network.get_type(device), + "rx_bytes": self.dsm.network.get_rx(device), + "tx_bytes": self.dsm.network.get_tx(device) + } + result.append(net_info) + + return result + + except Exception as e: + logger.error(f"Error getting network info: {str(e)}") + return [] + + def _get_volumes_info(self) -> List[Dict[str, Any]]: + """Получение информации о томах хранения""" + try: + result = [] + + # Получение информации о томах + for volume in self.dsm.storage.volumes: + vol_info = { + "name": volume, + "status": self.dsm.storage.volume_status(volume), + "device_type": self.dsm.storage.volume_device_type(volume), + "total_size": self.dsm.storage.volume_size_total(volume), + "used_size": self.dsm.storage.volume_size_used(volume), + "percent_used": self.dsm.storage.volume_percentage_used(volume) + } + result.append(vol_info) + + return result + + except Exception as e: + logger.error(f"Error getting volumes info: {str(e)}") + return [] + + def get_shared_folders(self) -> List[Dict[str, Any]]: + """Получение списка общих папок""" + if not self.dsm and not self.login(): + return [] + + try: + result = [] + + # Получение информации о общих папках + for folder in self.dsm.share.shares: + share_info = { + "name": folder, + "path": self.dsm.share.get_info(folder).get("path", ""), + "desc": self.dsm.share.get_info(folder).get("desc", "") + } + result.append(share_info) + + logger.info(f"Successfully retrieved {len(result)} shared folders") + return result + + except Exception as e: + logger.error(f"Error getting shared folders: {str(e)}") + return [] + + def get_system_info(self) -> Dict[str, Any]: + """Получение основной информации о системе""" + if not self.dsm and not self.login(): + return {} + + try: + result = { + "model": self.dsm.information.model, + "serial": self.dsm.information.serial, + "version": self.dsm.information.version_string, + "uptime": self.dsm.information.uptime + } + + logger.info("Successfully fetched system info") + return result + + except Exception as e: + logger.error(f"Error getting system info: {str(e)}") + return {} + + def shutdown_system(self) -> bool: + """Выключение системы""" + if not self.dsm and not self.login(): + return False + + try: + # Используем низкоуровневый API для отправки команды выключения + endpoint = "SYNO.DSM.System" + api_path = "entry.cgi" + req_param = {"version": 1, "method": "shutdown"} + + self.dsm.post(endpoint, api_path, req_param) + logger.info("Successfully initiated system shutdown") + return True + + except Exception as e: + logger.error(f"Error shutting down system: {str(e)}") + return False + + def reboot_system(self) -> bool: + """Перезагрузка системы""" + if not self.dsm and not self.login(): + return False + + try: + # Используем низкоуровневый API для отправки команды перезагрузки + endpoint = "SYNO.DSM.System" + api_path = "entry.cgi" + req_param = {"version": 1, "method": "reboot"} + + self.dsm.post(endpoint, api_path, req_param) + logger.info("Successfully initiated system reboot") + return True + + except Exception as e: + logger.error(f"Error rebooting system: {str(e)}") + return False + + def is_online(self) -> bool: + """Проверка онлайн-статуса Synology NAS""" + try: + socket_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + socket_obj.settimeout(SYNOLOGY_TIMEOUT) + result = socket_obj.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT)) + socket_obj.close() + + return result == 0 + except socket.error: + return False + + def wake_on_lan(self) -> bool: + """Отправка Wake-on-LAN пакета для включения Synology NAS""" + if not SYNOLOGY_MAC: + logger.error("MAC address not configured") + return False + + try: + # Преобразование MAC-адреса в байты + mac_address = SYNOLOGY_MAC.replace(':', '').replace('-', '') + mac_bytes = bytes.fromhex(mac_address) + + # Создание Magic Packet + magic_packet = b'\xff' * 6 + mac_bytes * 16 + + # Отправка пакета + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) + sock.sendto(magic_packet, (SYNOLOGY_HOST, WOL_PORT)) + sock.close() + + logger.info(f"Wake-on-LAN packet sent to {SYNOLOGY_MAC}") + return True + + except Exception as e: + logger.error(f"Error sending Wake-on-LAN packet: {str(e)}") + return False + + def wait_for_boot(self, max_attempts: int = 30, delay: int = 10) -> bool: + """Ожидание загрузки Synology NAS""" + logger.info("Waiting for Synology NAS to boot...") + + for attempt in range(max_attempts): + if self.is_online(): + logger.info(f"Synology NAS is online after {attempt + 1} attempts") + # Дадим дополнительное время для полной загрузки всех сервисов + sleep(delay) + return True + + sleep(delay) + logger.info(f"Waiting for boot... Attempt {attempt + 1}/{max_attempts}") + + logger.error(f"Synology NAS didn't come online after {max_attempts} attempts") + return False + + def power_on(self) -> bool: + """Включение Synology NAS""" + if self.is_online(): + logger.info("Synology NAS is already online") + return True + + # Отправка WoL пакета + if not self.wake_on_lan(): + return False + + # Ожидание загрузки + return self.wait_for_boot() + + def power_off(self) -> bool: + """Выключение Synology NAS""" + if not self.is_online(): + logger.info("Synology NAS is already offline") + return True + + return self.shutdown_system() + + def get_system_load(self) -> Dict[str, Any]: + """Получение информации о загрузке системы""" + if not self.dsm and not self.login(): + return {} + + try: + result = { + "cpu_load": self.dsm.utilisation.cpu_total_load, + "memory": { + "total_mb": self.dsm.utilisation.memory_size_mb, + "available_mb": self.dsm.utilisation.memory_available_real_mb, + "cached_mb": self.dsm.utilisation.memory_cached_mb, + "usage_percent": self.dsm.utilisation.memory_real_usage + }, + "network": {} + } + + # Добавляем данные по сети + for device in self.dsm.network.interfaces: + result["network"][device] = { + "rx_bytes": self.dsm.network.get_rx(device), + "tx_bytes": self.dsm.network.get_tx(device) + } + + logger.info("Successfully fetched system load") + return result + + except Exception as e: + logger.error(f"Error getting system load: {str(e)}") + return {} + + def get_storage_status(self) -> Dict[str, Any]: + """Получение подробной информации о хранилище""" + if not self.dsm and not self.login(): + return {} + + try: + result = { + "volumes": self._get_volumes_info(), + "disks": self._get_disks_info(), + "total_size": 0, + "total_used": 0 + } + + # Суммируем общий размер и использование + for volume in result["volumes"]: + result["total_size"] += volume["total_size"] + result["total_used"] += volume["used_size"] + + logger.info("Successfully fetched storage status") + return result + + except Exception as e: + logger.error(f"Error getting storage status: {str(e)}") + return {} + + def _get_disks_info(self) -> List[Dict[str, Any]]: + """Получение информации о дисках""" + try: + result = [] + + # Получение информации о дисках + for disk in self.dsm.storage.disks: + disk_info = { + "name": disk, + "model": self.dsm.storage.disk_model(disk), + "type": self.dsm.storage.disk_type(disk), + "status": self.dsm.storage.disk_status(disk), + "temp": self.dsm.storage.disk_temp(disk) + } + result.append(disk_info) + + return result + + except Exception as e: + logger.error(f"Error getting disks info: {str(e)}") + return [] + + def get_security_status(self) -> Dict[str, bool]: + """Получение информации о состоянии безопасности""" + if not self.dsm and not self.login(): + return {"success": False} + + try: + # Используем низкоуровневый API для получения информации о безопасности + endpoint = "SYNO.Core.Security.DSM" + api_path = "entry.cgi" + req_param = {"version": 1, "method": "status"} + + response = self.dsm.get(endpoint, api_path, req_param) + + if response and "data" in response: + return { + "success": True, + "status": response["data"].get("status", "unknown"), + "last_check": response["data"].get("last_check", None), + "is_secure": response["data"].get("is_secure", False) + } + else: + return {"success": False} + + except Exception as e: + logger.error(f"Error getting security status: {str(e)}") + return {"success": False} + + def get_users(self) -> List[str]: + """Получение списка пользователей""" + if not self.dsm and not self.login(): + return [] + + try: + users = [] + + # Используем низкоуровневый API для получения списка пользователей + endpoint = "SYNO.Core.User" + api_path = "entry.cgi" + req_param = {"version": 1, "method": "list", "additional": ["email"]} + + response = self.dsm.get(endpoint, api_path, req_param) + + if response and "data" in response and "users" in response["data"]: + for user in response["data"]["users"]: + if "name" in user: + users.append(user["name"]) + + logger.info(f"Successfully retrieved {len(users)} users") + return users + + except Exception as e: + logger.error(f"Error getting users: {str(e)}") + return [] diff --git a/.history/src/api/synology_20250830071727.py b/.history/src/api/synology_20250830071727.py new file mode 100644 index 0000000..a351e5f --- /dev/null +++ b/.history/src/api/synology_20250830071727.py @@ -0,0 +1,446 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Модуль для взаимодействия с API Synology NAS с использованием библиотеки python-synology +""" + +import requests +import json +import logging +from typing import Dict, Any, Optional, List, Tuple +import socket +import struct +from time import sleep +import urllib3 +from synology_dsm import SynologyDSM + +from src.config.config import ( + SYNOLOGY_HOST, + SYNOLOGY_PORT, + SYNOLOGY_USERNAME, + SYNOLOGY_PASSWORD, + SYNOLOGY_SECURE, + SYNOLOGY_TIMEOUT, + SYNOLOGY_MAC, + WOL_PORT +) + +# Отключение предупреждений о небезопасных SSL-соединениях +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + +logger = logging.getLogger(__name__) + +class SynologyAPI: + """Класс для взаимодействия с API Synology NAS с использованием python-synology""" + + def __init__(self): + """Инициализация класса SynologyAPI""" + self.dsm = None + + def login(self) -> bool: + """Авторизация в API Synology NAS используя python-synology""" + try: + # Создаем экземпляр SynologyDSM + self.dsm = SynologyDSM( + dsm_ip=SYNOLOGY_HOST, + dsm_port=SYNOLOGY_PORT, + username=SYNOLOGY_USERNAME, + password=SYNOLOGY_PASSWORD, + use_https=SYNOLOGY_SECURE, + timeout=SYNOLOGY_TIMEOUT, + verify_ssl=False + ) + + # Авторизация + self.dsm.login() + logger.info("Successfully logged in to Synology NAS") + return True + + except Exception as e: + logger.error(f"Failed to log in to Synology NAS: {str(e)}") + self.dsm = None + return False + + def logout(self) -> bool: + """Выход из API Synology NAS""" + if not self.dsm: + return True + + try: + self.dsm.logout() + self.dsm = None + logger.info("Successfully logged out from Synology NAS") + return True + + except Exception as e: + logger.error(f"Error during logout: {str(e)}") + return False + + def get_system_status(self) -> Optional[Dict[str, Any]]: + """Получение расширенного статуса системы""" + if not self.dsm and not self.login(): + return None + + try: + result = { + "model": self.dsm.information.model, + "version": self.dsm.information.version_string, + "uptime": self.dsm.information.uptime, + "serial": self.dsm.information.serial, + "temperature": self.dsm.information.temperature, + "temperature_unit": "C", + "cpu_usage": self.dsm.utilisation.cpu_total_load, + "memory": { + "total_mb": self.dsm.utilisation.memory_size_mb, + "available_mb": self.dsm.utilisation.memory_available_real_mb, + "usage_percent": self.dsm.utilisation.memory_real_usage + }, + "network": self._get_network_info(), + "volumes": self._get_volumes_info() + } + + logger.info("Successfully fetched extended system status") + return result + + except Exception as e: + logger.error(f"Error getting system status: {str(e)}") + return None + + def _get_network_info(self) -> List[Dict[str, Any]]: + """Получение информации о сетевых интерфейсах""" + try: + result = [] + + # Получение информации о сети + for device in self.dsm.network.interfaces: + net_info = { + "device": device, + "ip": self.dsm.network.get_ip(device), + "mask": self.dsm.network.get_mask(device), + "mac": self.dsm.network.get_mac(device), + "type": self.dsm.network.get_type(device), + "rx_bytes": self.dsm.network.get_rx(device), + "tx_bytes": self.dsm.network.get_tx(device) + } + result.append(net_info) + + return result + + except Exception as e: + logger.error(f"Error getting network info: {str(e)}") + return [] + + def _get_volumes_info(self) -> List[Dict[str, Any]]: + """Получение информации о томах хранения""" + try: + result = [] + + # Получение информации о томах + for volume in self.dsm.storage.volumes: + vol_info = { + "name": volume, + "status": self.dsm.storage.volume_status(volume), + "device_type": self.dsm.storage.volume_device_type(volume), + "total_size": self.dsm.storage.volume_size_total(volume), + "used_size": self.dsm.storage.volume_size_used(volume), + "percent_used": self.dsm.storage.volume_percentage_used(volume) + } + result.append(vol_info) + + return result + + except Exception as e: + logger.error(f"Error getting volumes info: {str(e)}") + return [] + + def get_shared_folders(self) -> List[Dict[str, Any]]: + """Получение списка общих папок""" + if not self.dsm and not self.login(): + return [] + + try: + result = [] + + # Получение информации о общих папках + for folder in self.dsm.share.shares: + share_info = { + "name": folder, + "path": self.dsm.share.get_info(folder).get("path", ""), + "desc": self.dsm.share.get_info(folder).get("desc", "") + } + result.append(share_info) + + logger.info(f"Successfully retrieved {len(result)} shared folders") + return result + + except Exception as e: + logger.error(f"Error getting shared folders: {str(e)}") + return [] + + def get_system_info(self) -> Dict[str, Any]: + """Получение основной информации о системе""" + if not self.dsm and not self.login(): + return {} + + try: + result = { + "model": self.dsm.information.model, + "serial": self.dsm.information.serial, + "version": self.dsm.information.version_string, + "uptime": self.dsm.information.uptime + } + + logger.info("Successfully fetched system info") + return result + + except Exception as e: + logger.error(f"Error getting system info: {str(e)}") + return {} + + def shutdown_system(self) -> bool: + """Выключение системы""" + if not self.dsm and not self.login(): + return False + + try: + # Используем низкоуровневый API для отправки команды выключения + endpoint = "SYNO.DSM.System" + api_path = "entry.cgi" + req_param = {"version": 1, "method": "shutdown"} + + self.dsm.post(endpoint, api_path, req_param) + logger.info("Successfully initiated system shutdown") + return True + + except Exception as e: + logger.error(f"Error shutting down system: {str(e)}") + return False + + def reboot_system(self) -> bool: + """Перезагрузка системы""" + if not self.dsm and not self.login(): + return False + + try: + # Используем низкоуровневый API для отправки команды перезагрузки + endpoint = "SYNO.DSM.System" + api_path = "entry.cgi" + req_param = {"version": 1, "method": "reboot"} + + self.dsm.post(endpoint, api_path, req_param) + logger.info("Successfully initiated system reboot") + return True + + except Exception as e: + logger.error(f"Error rebooting system: {str(e)}") + return False + + def is_online(self) -> bool: + """Проверка онлайн-статуса Synology NAS""" + try: + socket_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + socket_obj.settimeout(SYNOLOGY_TIMEOUT) + result = socket_obj.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT)) + socket_obj.close() + + return result == 0 + except socket.error: + return False + + def wake_on_lan(self) -> bool: + """Отправка Wake-on-LAN пакета для включения Synology NAS""" + if not SYNOLOGY_MAC: + logger.error("MAC address not configured") + return False + + try: + # Преобразование MAC-адреса в байты + mac_address = SYNOLOGY_MAC.replace(':', '').replace('-', '') + mac_bytes = bytes.fromhex(mac_address) + + # Создание Magic Packet + magic_packet = b'\xff' * 6 + mac_bytes * 16 + + # Отправка пакета + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) + sock.sendto(magic_packet, (SYNOLOGY_HOST, WOL_PORT)) + sock.close() + + logger.info(f"Wake-on-LAN packet sent to {SYNOLOGY_MAC}") + return True + + except Exception as e: + logger.error(f"Error sending Wake-on-LAN packet: {str(e)}") + return False + + def wait_for_boot(self, max_attempts: int = 30, delay: int = 10) -> bool: + """Ожидание загрузки Synology NAS""" + logger.info("Waiting for Synology NAS to boot...") + + for attempt in range(max_attempts): + if self.is_online(): + logger.info(f"Synology NAS is online after {attempt + 1} attempts") + # Дадим дополнительное время для полной загрузки всех сервисов + sleep(delay) + return True + + sleep(delay) + logger.info(f"Waiting for boot... Attempt {attempt + 1}/{max_attempts}") + + logger.error(f"Synology NAS didn't come online after {max_attempts} attempts") + return False + + def power_on(self) -> bool: + """Включение Synology NAS""" + if self.is_online(): + logger.info("Synology NAS is already online") + return True + + # Отправка WoL пакета + if not self.wake_on_lan(): + return False + + # Ожидание загрузки + return self.wait_for_boot() + + def power_off(self) -> bool: + """Выключение Synology NAS""" + if not self.is_online(): + logger.info("Synology NAS is already offline") + return True + + return self.shutdown_system() + + def get_system_load(self) -> Dict[str, Any]: + """Получение информации о загрузке системы""" + if not self.dsm and not self.login(): + return {} + + try: + result = { + "cpu_load": self.dsm.utilisation.cpu_total_load, + "memory": { + "total_mb": self.dsm.utilisation.memory_size_mb, + "available_mb": self.dsm.utilisation.memory_available_real_mb, + "cached_mb": self.dsm.utilisation.memory_cached_mb, + "usage_percent": self.dsm.utilisation.memory_real_usage + }, + "network": {} + } + + # Добавляем данные по сети + for device in self.dsm.network.interfaces: + result["network"][device] = { + "rx_bytes": self.dsm.network.get_rx(device), + "tx_bytes": self.dsm.network.get_tx(device) + } + + logger.info("Successfully fetched system load") + return result + + except Exception as e: + logger.error(f"Error getting system load: {str(e)}") + return {} + + def get_storage_status(self) -> Dict[str, Any]: + """Получение подробной информации о хранилище""" + if not self.dsm and not self.login(): + return {} + + try: + result = { + "volumes": self._get_volumes_info(), + "disks": self._get_disks_info(), + "total_size": 0, + "total_used": 0 + } + + # Суммируем общий размер и использование + for volume in result["volumes"]: + result["total_size"] += volume["total_size"] + result["total_used"] += volume["used_size"] + + logger.info("Successfully fetched storage status") + return result + + except Exception as e: + logger.error(f"Error getting storage status: {str(e)}") + return {} + + def _get_disks_info(self) -> List[Dict[str, Any]]: + """Получение информации о дисках""" + try: + result = [] + + # Получение информации о дисках + for disk in self.dsm.storage.disks: + disk_info = { + "name": disk, + "model": self.dsm.storage.disk_model(disk), + "type": self.dsm.storage.disk_type(disk), + "status": self.dsm.storage.disk_status(disk), + "temp": self.dsm.storage.disk_temp(disk) + } + result.append(disk_info) + + return result + + except Exception as e: + logger.error(f"Error getting disks info: {str(e)}") + return [] + + def get_security_status(self) -> Dict[str, bool]: + """Получение информации о состоянии безопасности""" + if not self.dsm and not self.login(): + return {"success": False} + + try: + # Используем низкоуровневый API для получения информации о безопасности + endpoint = "SYNO.Core.Security.DSM" + api_path = "entry.cgi" + req_param = {"version": 1, "method": "status"} + + response = self.dsm.get(endpoint, api_path, req_param) + + if response and "data" in response: + return { + "success": True, + "status": response["data"].get("status", "unknown"), + "last_check": response["data"].get("last_check", None), + "is_secure": response["data"].get("is_secure", False) + } + else: + return {"success": False} + + except Exception as e: + logger.error(f"Error getting security status: {str(e)}") + return {"success": False} + + def get_users(self) -> List[str]: + """Получение списка пользователей""" + if not self.dsm and not self.login(): + return [] + + try: + users = [] + + # Используем низкоуровневый API для получения списка пользователей + endpoint = "SYNO.Core.User" + api_path = "entry.cgi" + req_param = {"version": 1, "method": "list", "additional": ["email"]} + + response = self.dsm.get(endpoint, api_path, req_param) + + if response and "data" in response and "users" in response["data"]: + for user in response["data"]["users"]: + if "name" in user: + users.append(user["name"]) + + logger.info(f"Successfully retrieved {len(users)} users") + return users + + except Exception as e: + logger.error(f"Error getting users: {str(e)}") + return [] diff --git a/.history/src/api/synology_20250830071755.py b/.history/src/api/synology_20250830071755.py new file mode 100644 index 0000000..a351e5f --- /dev/null +++ b/.history/src/api/synology_20250830071755.py @@ -0,0 +1,446 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Модуль для взаимодействия с API Synology NAS с использованием библиотеки python-synology +""" + +import requests +import json +import logging +from typing import Dict, Any, Optional, List, Tuple +import socket +import struct +from time import sleep +import urllib3 +from synology_dsm import SynologyDSM + +from src.config.config import ( + SYNOLOGY_HOST, + SYNOLOGY_PORT, + SYNOLOGY_USERNAME, + SYNOLOGY_PASSWORD, + SYNOLOGY_SECURE, + SYNOLOGY_TIMEOUT, + SYNOLOGY_MAC, + WOL_PORT +) + +# Отключение предупреждений о небезопасных SSL-соединениях +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + +logger = logging.getLogger(__name__) + +class SynologyAPI: + """Класс для взаимодействия с API Synology NAS с использованием python-synology""" + + def __init__(self): + """Инициализация класса SynologyAPI""" + self.dsm = None + + def login(self) -> bool: + """Авторизация в API Synology NAS используя python-synology""" + try: + # Создаем экземпляр SynologyDSM + self.dsm = SynologyDSM( + dsm_ip=SYNOLOGY_HOST, + dsm_port=SYNOLOGY_PORT, + username=SYNOLOGY_USERNAME, + password=SYNOLOGY_PASSWORD, + use_https=SYNOLOGY_SECURE, + timeout=SYNOLOGY_TIMEOUT, + verify_ssl=False + ) + + # Авторизация + self.dsm.login() + logger.info("Successfully logged in to Synology NAS") + return True + + except Exception as e: + logger.error(f"Failed to log in to Synology NAS: {str(e)}") + self.dsm = None + return False + + def logout(self) -> bool: + """Выход из API Synology NAS""" + if not self.dsm: + return True + + try: + self.dsm.logout() + self.dsm = None + logger.info("Successfully logged out from Synology NAS") + return True + + except Exception as e: + logger.error(f"Error during logout: {str(e)}") + return False + + def get_system_status(self) -> Optional[Dict[str, Any]]: + """Получение расширенного статуса системы""" + if not self.dsm and not self.login(): + return None + + try: + result = { + "model": self.dsm.information.model, + "version": self.dsm.information.version_string, + "uptime": self.dsm.information.uptime, + "serial": self.dsm.information.serial, + "temperature": self.dsm.information.temperature, + "temperature_unit": "C", + "cpu_usage": self.dsm.utilisation.cpu_total_load, + "memory": { + "total_mb": self.dsm.utilisation.memory_size_mb, + "available_mb": self.dsm.utilisation.memory_available_real_mb, + "usage_percent": self.dsm.utilisation.memory_real_usage + }, + "network": self._get_network_info(), + "volumes": self._get_volumes_info() + } + + logger.info("Successfully fetched extended system status") + return result + + except Exception as e: + logger.error(f"Error getting system status: {str(e)}") + return None + + def _get_network_info(self) -> List[Dict[str, Any]]: + """Получение информации о сетевых интерфейсах""" + try: + result = [] + + # Получение информации о сети + for device in self.dsm.network.interfaces: + net_info = { + "device": device, + "ip": self.dsm.network.get_ip(device), + "mask": self.dsm.network.get_mask(device), + "mac": self.dsm.network.get_mac(device), + "type": self.dsm.network.get_type(device), + "rx_bytes": self.dsm.network.get_rx(device), + "tx_bytes": self.dsm.network.get_tx(device) + } + result.append(net_info) + + return result + + except Exception as e: + logger.error(f"Error getting network info: {str(e)}") + return [] + + def _get_volumes_info(self) -> List[Dict[str, Any]]: + """Получение информации о томах хранения""" + try: + result = [] + + # Получение информации о томах + for volume in self.dsm.storage.volumes: + vol_info = { + "name": volume, + "status": self.dsm.storage.volume_status(volume), + "device_type": self.dsm.storage.volume_device_type(volume), + "total_size": self.dsm.storage.volume_size_total(volume), + "used_size": self.dsm.storage.volume_size_used(volume), + "percent_used": self.dsm.storage.volume_percentage_used(volume) + } + result.append(vol_info) + + return result + + except Exception as e: + logger.error(f"Error getting volumes info: {str(e)}") + return [] + + def get_shared_folders(self) -> List[Dict[str, Any]]: + """Получение списка общих папок""" + if not self.dsm and not self.login(): + return [] + + try: + result = [] + + # Получение информации о общих папках + for folder in self.dsm.share.shares: + share_info = { + "name": folder, + "path": self.dsm.share.get_info(folder).get("path", ""), + "desc": self.dsm.share.get_info(folder).get("desc", "") + } + result.append(share_info) + + logger.info(f"Successfully retrieved {len(result)} shared folders") + return result + + except Exception as e: + logger.error(f"Error getting shared folders: {str(e)}") + return [] + + def get_system_info(self) -> Dict[str, Any]: + """Получение основной информации о системе""" + if not self.dsm and not self.login(): + return {} + + try: + result = { + "model": self.dsm.information.model, + "serial": self.dsm.information.serial, + "version": self.dsm.information.version_string, + "uptime": self.dsm.information.uptime + } + + logger.info("Successfully fetched system info") + return result + + except Exception as e: + logger.error(f"Error getting system info: {str(e)}") + return {} + + def shutdown_system(self) -> bool: + """Выключение системы""" + if not self.dsm and not self.login(): + return False + + try: + # Используем низкоуровневый API для отправки команды выключения + endpoint = "SYNO.DSM.System" + api_path = "entry.cgi" + req_param = {"version": 1, "method": "shutdown"} + + self.dsm.post(endpoint, api_path, req_param) + logger.info("Successfully initiated system shutdown") + return True + + except Exception as e: + logger.error(f"Error shutting down system: {str(e)}") + return False + + def reboot_system(self) -> bool: + """Перезагрузка системы""" + if not self.dsm and not self.login(): + return False + + try: + # Используем низкоуровневый API для отправки команды перезагрузки + endpoint = "SYNO.DSM.System" + api_path = "entry.cgi" + req_param = {"version": 1, "method": "reboot"} + + self.dsm.post(endpoint, api_path, req_param) + logger.info("Successfully initiated system reboot") + return True + + except Exception as e: + logger.error(f"Error rebooting system: {str(e)}") + return False + + def is_online(self) -> bool: + """Проверка онлайн-статуса Synology NAS""" + try: + socket_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + socket_obj.settimeout(SYNOLOGY_TIMEOUT) + result = socket_obj.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT)) + socket_obj.close() + + return result == 0 + except socket.error: + return False + + def wake_on_lan(self) -> bool: + """Отправка Wake-on-LAN пакета для включения Synology NAS""" + if not SYNOLOGY_MAC: + logger.error("MAC address not configured") + return False + + try: + # Преобразование MAC-адреса в байты + mac_address = SYNOLOGY_MAC.replace(':', '').replace('-', '') + mac_bytes = bytes.fromhex(mac_address) + + # Создание Magic Packet + magic_packet = b'\xff' * 6 + mac_bytes * 16 + + # Отправка пакета + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) + sock.sendto(magic_packet, (SYNOLOGY_HOST, WOL_PORT)) + sock.close() + + logger.info(f"Wake-on-LAN packet sent to {SYNOLOGY_MAC}") + return True + + except Exception as e: + logger.error(f"Error sending Wake-on-LAN packet: {str(e)}") + return False + + def wait_for_boot(self, max_attempts: int = 30, delay: int = 10) -> bool: + """Ожидание загрузки Synology NAS""" + logger.info("Waiting for Synology NAS to boot...") + + for attempt in range(max_attempts): + if self.is_online(): + logger.info(f"Synology NAS is online after {attempt + 1} attempts") + # Дадим дополнительное время для полной загрузки всех сервисов + sleep(delay) + return True + + sleep(delay) + logger.info(f"Waiting for boot... Attempt {attempt + 1}/{max_attempts}") + + logger.error(f"Synology NAS didn't come online after {max_attempts} attempts") + return False + + def power_on(self) -> bool: + """Включение Synology NAS""" + if self.is_online(): + logger.info("Synology NAS is already online") + return True + + # Отправка WoL пакета + if not self.wake_on_lan(): + return False + + # Ожидание загрузки + return self.wait_for_boot() + + def power_off(self) -> bool: + """Выключение Synology NAS""" + if not self.is_online(): + logger.info("Synology NAS is already offline") + return True + + return self.shutdown_system() + + def get_system_load(self) -> Dict[str, Any]: + """Получение информации о загрузке системы""" + if not self.dsm and not self.login(): + return {} + + try: + result = { + "cpu_load": self.dsm.utilisation.cpu_total_load, + "memory": { + "total_mb": self.dsm.utilisation.memory_size_mb, + "available_mb": self.dsm.utilisation.memory_available_real_mb, + "cached_mb": self.dsm.utilisation.memory_cached_mb, + "usage_percent": self.dsm.utilisation.memory_real_usage + }, + "network": {} + } + + # Добавляем данные по сети + for device in self.dsm.network.interfaces: + result["network"][device] = { + "rx_bytes": self.dsm.network.get_rx(device), + "tx_bytes": self.dsm.network.get_tx(device) + } + + logger.info("Successfully fetched system load") + return result + + except Exception as e: + logger.error(f"Error getting system load: {str(e)}") + return {} + + def get_storage_status(self) -> Dict[str, Any]: + """Получение подробной информации о хранилище""" + if not self.dsm and not self.login(): + return {} + + try: + result = { + "volumes": self._get_volumes_info(), + "disks": self._get_disks_info(), + "total_size": 0, + "total_used": 0 + } + + # Суммируем общий размер и использование + for volume in result["volumes"]: + result["total_size"] += volume["total_size"] + result["total_used"] += volume["used_size"] + + logger.info("Successfully fetched storage status") + return result + + except Exception as e: + logger.error(f"Error getting storage status: {str(e)}") + return {} + + def _get_disks_info(self) -> List[Dict[str, Any]]: + """Получение информации о дисках""" + try: + result = [] + + # Получение информации о дисках + for disk in self.dsm.storage.disks: + disk_info = { + "name": disk, + "model": self.dsm.storage.disk_model(disk), + "type": self.dsm.storage.disk_type(disk), + "status": self.dsm.storage.disk_status(disk), + "temp": self.dsm.storage.disk_temp(disk) + } + result.append(disk_info) + + return result + + except Exception as e: + logger.error(f"Error getting disks info: {str(e)}") + return [] + + def get_security_status(self) -> Dict[str, bool]: + """Получение информации о состоянии безопасности""" + if not self.dsm and not self.login(): + return {"success": False} + + try: + # Используем низкоуровневый API для получения информации о безопасности + endpoint = "SYNO.Core.Security.DSM" + api_path = "entry.cgi" + req_param = {"version": 1, "method": "status"} + + response = self.dsm.get(endpoint, api_path, req_param) + + if response and "data" in response: + return { + "success": True, + "status": response["data"].get("status", "unknown"), + "last_check": response["data"].get("last_check", None), + "is_secure": response["data"].get("is_secure", False) + } + else: + return {"success": False} + + except Exception as e: + logger.error(f"Error getting security status: {str(e)}") + return {"success": False} + + def get_users(self) -> List[str]: + """Получение списка пользователей""" + if not self.dsm and not self.login(): + return [] + + try: + users = [] + + # Используем низкоуровневый API для получения списка пользователей + endpoint = "SYNO.Core.User" + api_path = "entry.cgi" + req_param = {"version": 1, "method": "list", "additional": ["email"]} + + response = self.dsm.get(endpoint, api_path, req_param) + + if response and "data" in response and "users" in response["data"]: + for user in response["data"]["users"]: + if "name" in user: + users.append(user["name"]) + + logger.info(f"Successfully retrieved {len(users)} users") + return users + + except Exception as e: + logger.error(f"Error getting users: {str(e)}") + return [] diff --git a/.history/src/api/synology_20250830071934.py b/.history/src/api/synology_20250830071934.py new file mode 100644 index 0000000..5a89749 --- /dev/null +++ b/.history/src/api/synology_20250830071934.py @@ -0,0 +1,445 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Модуль для взаимодействия с API Synology NAS +""" + +import requests +import json +import logging +from typing import Dict, Any, Optional, List +import socket +import struct +from time import sleep +import urllib3 + +from src.config.config import ( + SYNOLOGY_HOST, + SYNOLOGY_PORT, + SYNOLOGY_USERNAME, + SYNOLOGY_PASSWORD, + SYNOLOGY_SECURE, + SYNOLOGY_TIMEOUT, + SYNOLOGY_MAC, + WOL_PORT +) + +# Отключение предупреждений о небезопасных SSL-соединениях +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + +logger = logging.getLogger(__name__) + +class SynologyAPI: + """Класс для взаимодействия с API Synology NAS с использованием python-synology""" + + def __init__(self): + """Инициализация класса SynologyAPI""" + self.dsm = None + + def login(self) -> bool: + """Авторизация в API Synology NAS используя python-synology""" + try: + # Создаем экземпляр SynologyDSM + self.dsm = SynologyDSM( + dsm_ip=SYNOLOGY_HOST, + dsm_port=SYNOLOGY_PORT, + username=SYNOLOGY_USERNAME, + password=SYNOLOGY_PASSWORD, + use_https=SYNOLOGY_SECURE, + timeout=SYNOLOGY_TIMEOUT, + verify_ssl=False + ) + + # Авторизация + self.dsm.login() + logger.info("Successfully logged in to Synology NAS") + return True + + except Exception as e: + logger.error(f"Failed to log in to Synology NAS: {str(e)}") + self.dsm = None + return False + + def logout(self) -> bool: + """Выход из API Synology NAS""" + if not self.dsm: + return True + + try: + self.dsm.logout() + self.dsm = None + logger.info("Successfully logged out from Synology NAS") + return True + + except Exception as e: + logger.error(f"Error during logout: {str(e)}") + return False + + def get_system_status(self) -> Optional[Dict[str, Any]]: + """Получение расширенного статуса системы""" + if not self.dsm and not self.login(): + return None + + try: + result = { + "model": self.dsm.information.model, + "version": self.dsm.information.version_string, + "uptime": self.dsm.information.uptime, + "serial": self.dsm.information.serial, + "temperature": self.dsm.information.temperature, + "temperature_unit": "C", + "cpu_usage": self.dsm.utilisation.cpu_total_load, + "memory": { + "total_mb": self.dsm.utilisation.memory_size_mb, + "available_mb": self.dsm.utilisation.memory_available_real_mb, + "usage_percent": self.dsm.utilisation.memory_real_usage + }, + "network": self._get_network_info(), + "volumes": self._get_volumes_info() + } + + logger.info("Successfully fetched extended system status") + return result + + except Exception as e: + logger.error(f"Error getting system status: {str(e)}") + return None + + def _get_network_info(self) -> List[Dict[str, Any]]: + """Получение информации о сетевых интерфейсах""" + try: + result = [] + + # Получение информации о сети + for device in self.dsm.network.interfaces: + net_info = { + "device": device, + "ip": self.dsm.network.get_ip(device), + "mask": self.dsm.network.get_mask(device), + "mac": self.dsm.network.get_mac(device), + "type": self.dsm.network.get_type(device), + "rx_bytes": self.dsm.network.get_rx(device), + "tx_bytes": self.dsm.network.get_tx(device) + } + result.append(net_info) + + return result + + except Exception as e: + logger.error(f"Error getting network info: {str(e)}") + return [] + + def _get_volumes_info(self) -> List[Dict[str, Any]]: + """Получение информации о томах хранения""" + try: + result = [] + + # Получение информации о томах + for volume in self.dsm.storage.volumes: + vol_info = { + "name": volume, + "status": self.dsm.storage.volume_status(volume), + "device_type": self.dsm.storage.volume_device_type(volume), + "total_size": self.dsm.storage.volume_size_total(volume), + "used_size": self.dsm.storage.volume_size_used(volume), + "percent_used": self.dsm.storage.volume_percentage_used(volume) + } + result.append(vol_info) + + return result + + except Exception as e: + logger.error(f"Error getting volumes info: {str(e)}") + return [] + + def get_shared_folders(self) -> List[Dict[str, Any]]: + """Получение списка общих папок""" + if not self.dsm and not self.login(): + return [] + + try: + result = [] + + # Получение информации о общих папках + for folder in self.dsm.share.shares: + share_info = { + "name": folder, + "path": self.dsm.share.get_info(folder).get("path", ""), + "desc": self.dsm.share.get_info(folder).get("desc", "") + } + result.append(share_info) + + logger.info(f"Successfully retrieved {len(result)} shared folders") + return result + + except Exception as e: + logger.error(f"Error getting shared folders: {str(e)}") + return [] + + def get_system_info(self) -> Dict[str, Any]: + """Получение основной информации о системе""" + if not self.dsm and not self.login(): + return {} + + try: + result = { + "model": self.dsm.information.model, + "serial": self.dsm.information.serial, + "version": self.dsm.information.version_string, + "uptime": self.dsm.information.uptime + } + + logger.info("Successfully fetched system info") + return result + + except Exception as e: + logger.error(f"Error getting system info: {str(e)}") + return {} + + def shutdown_system(self) -> bool: + """Выключение системы""" + if not self.dsm and not self.login(): + return False + + try: + # Используем низкоуровневый API для отправки команды выключения + endpoint = "SYNO.DSM.System" + api_path = "entry.cgi" + req_param = {"version": 1, "method": "shutdown"} + + self.dsm.post(endpoint, api_path, req_param) + logger.info("Successfully initiated system shutdown") + return True + + except Exception as e: + logger.error(f"Error shutting down system: {str(e)}") + return False + + def reboot_system(self) -> bool: + """Перезагрузка системы""" + if not self.dsm and not self.login(): + return False + + try: + # Используем низкоуровневый API для отправки команды перезагрузки + endpoint = "SYNO.DSM.System" + api_path = "entry.cgi" + req_param = {"version": 1, "method": "reboot"} + + self.dsm.post(endpoint, api_path, req_param) + logger.info("Successfully initiated system reboot") + return True + + except Exception as e: + logger.error(f"Error rebooting system: {str(e)}") + return False + + def is_online(self) -> bool: + """Проверка онлайн-статуса Synology NAS""" + try: + socket_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + socket_obj.settimeout(SYNOLOGY_TIMEOUT) + result = socket_obj.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT)) + socket_obj.close() + + return result == 0 + except socket.error: + return False + + def wake_on_lan(self) -> bool: + """Отправка Wake-on-LAN пакета для включения Synology NAS""" + if not SYNOLOGY_MAC: + logger.error("MAC address not configured") + return False + + try: + # Преобразование MAC-адреса в байты + mac_address = SYNOLOGY_MAC.replace(':', '').replace('-', '') + mac_bytes = bytes.fromhex(mac_address) + + # Создание Magic Packet + magic_packet = b'\xff' * 6 + mac_bytes * 16 + + # Отправка пакета + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) + sock.sendto(magic_packet, (SYNOLOGY_HOST, WOL_PORT)) + sock.close() + + logger.info(f"Wake-on-LAN packet sent to {SYNOLOGY_MAC}") + return True + + except Exception as e: + logger.error(f"Error sending Wake-on-LAN packet: {str(e)}") + return False + + def wait_for_boot(self, max_attempts: int = 30, delay: int = 10) -> bool: + """Ожидание загрузки Synology NAS""" + logger.info("Waiting for Synology NAS to boot...") + + for attempt in range(max_attempts): + if self.is_online(): + logger.info(f"Synology NAS is online after {attempt + 1} attempts") + # Дадим дополнительное время для полной загрузки всех сервисов + sleep(delay) + return True + + sleep(delay) + logger.info(f"Waiting for boot... Attempt {attempt + 1}/{max_attempts}") + + logger.error(f"Synology NAS didn't come online after {max_attempts} attempts") + return False + + def power_on(self) -> bool: + """Включение Synology NAS""" + if self.is_online(): + logger.info("Synology NAS is already online") + return True + + # Отправка WoL пакета + if not self.wake_on_lan(): + return False + + # Ожидание загрузки + return self.wait_for_boot() + + def power_off(self) -> bool: + """Выключение Synology NAS""" + if not self.is_online(): + logger.info("Synology NAS is already offline") + return True + + return self.shutdown_system() + + def get_system_load(self) -> Dict[str, Any]: + """Получение информации о загрузке системы""" + if not self.dsm and not self.login(): + return {} + + try: + result = { + "cpu_load": self.dsm.utilisation.cpu_total_load, + "memory": { + "total_mb": self.dsm.utilisation.memory_size_mb, + "available_mb": self.dsm.utilisation.memory_available_real_mb, + "cached_mb": self.dsm.utilisation.memory_cached_mb, + "usage_percent": self.dsm.utilisation.memory_real_usage + }, + "network": {} + } + + # Добавляем данные по сети + for device in self.dsm.network.interfaces: + result["network"][device] = { + "rx_bytes": self.dsm.network.get_rx(device), + "tx_bytes": self.dsm.network.get_tx(device) + } + + logger.info("Successfully fetched system load") + return result + + except Exception as e: + logger.error(f"Error getting system load: {str(e)}") + return {} + + def get_storage_status(self) -> Dict[str, Any]: + """Получение подробной информации о хранилище""" + if not self.dsm and not self.login(): + return {} + + try: + result = { + "volumes": self._get_volumes_info(), + "disks": self._get_disks_info(), + "total_size": 0, + "total_used": 0 + } + + # Суммируем общий размер и использование + for volume in result["volumes"]: + result["total_size"] += volume["total_size"] + result["total_used"] += volume["used_size"] + + logger.info("Successfully fetched storage status") + return result + + except Exception as e: + logger.error(f"Error getting storage status: {str(e)}") + return {} + + def _get_disks_info(self) -> List[Dict[str, Any]]: + """Получение информации о дисках""" + try: + result = [] + + # Получение информации о дисках + for disk in self.dsm.storage.disks: + disk_info = { + "name": disk, + "model": self.dsm.storage.disk_model(disk), + "type": self.dsm.storage.disk_type(disk), + "status": self.dsm.storage.disk_status(disk), + "temp": self.dsm.storage.disk_temp(disk) + } + result.append(disk_info) + + return result + + except Exception as e: + logger.error(f"Error getting disks info: {str(e)}") + return [] + + def get_security_status(self) -> Dict[str, bool]: + """Получение информации о состоянии безопасности""" + if not self.dsm and not self.login(): + return {"success": False} + + try: + # Используем низкоуровневый API для получения информации о безопасности + endpoint = "SYNO.Core.Security.DSM" + api_path = "entry.cgi" + req_param = {"version": 1, "method": "status"} + + response = self.dsm.get(endpoint, api_path, req_param) + + if response and "data" in response: + return { + "success": True, + "status": response["data"].get("status", "unknown"), + "last_check": response["data"].get("last_check", None), + "is_secure": response["data"].get("is_secure", False) + } + else: + return {"success": False} + + except Exception as e: + logger.error(f"Error getting security status: {str(e)}") + return {"success": False} + + def get_users(self) -> List[str]: + """Получение списка пользователей""" + if not self.dsm and not self.login(): + return [] + + try: + users = [] + + # Используем низкоуровневый API для получения списка пользователей + endpoint = "SYNO.Core.User" + api_path = "entry.cgi" + req_param = {"version": 1, "method": "list", "additional": ["email"]} + + response = self.dsm.get(endpoint, api_path, req_param) + + if response and "data" in response and "users" in response["data"]: + for user in response["data"]["users"]: + if "name" in user: + users.append(user["name"]) + + logger.info(f"Successfully retrieved {len(users)} users") + return users + + except Exception as e: + logger.error(f"Error getting users: {str(e)}") + return [] diff --git a/.history/src/api/synology_20250830072031.py b/.history/src/api/synology_20250830072031.py new file mode 100644 index 0000000..1468e04 --- /dev/null +++ b/.history/src/api/synology_20250830072031.py @@ -0,0 +1,398 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Модуль для взаимодействия с API Synology NAS +""" + +import requests +import json +import logging +from typing import Dict, Any, Optional, List +import socket +import struct +from time import sleep +import urllib3 + +from src.config.config import ( + SYNOLOGY_HOST, + SYNOLOGY_PORT, + SYNOLOGY_USERNAME, + SYNOLOGY_PASSWORD, + SYNOLOGY_SECURE, + SYNOLOGY_TIMEOUT, + SYNOLOGY_MAC, + WOL_PORT +) + +# Отключение предупреждений о небезопасных SSL-соединениях +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + +logger = logging.getLogger(__name__) + +class SynologyAPI: + """Класс для взаимодействия с API Synology NAS""" + + def __init__(self): + """Инициализация класса SynologyAPI""" + self.protocol = "https" if SYNOLOGY_SECURE else "http" + self.base_url = f"{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi" + self.sid = None + self.session = requests.Session() + + def login(self) -> bool: + """Авторизация в API Synology NAS""" + try: + url = f"{self.base_url}/auth.cgi" + params = { + "api": "SYNO.API.Auth", + "version": "3", + "method": "login", + "account": SYNOLOGY_USERNAME, + "passwd": SYNOLOGY_PASSWORD, + "session": "SynologyPowerControlBot", + "format": "cookie" + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + self.sid = data.get("data", {}).get("sid") + logger.info("Successfully logged in to Synology NAS") + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to log in to Synology NAS: Error code {error_code}") + return False + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + return False + + def logout(self) -> bool: + """Выход из API Synology NAS""" + if not self.sid: + return True + + try: + url = f"{self.base_url}/auth.cgi" + params = { + "api": "SYNO.API.Auth", + "version": "1", + "method": "logout", + "session": "SynologyPowerControlBot", + "_sid": self.sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + self.sid = None + logger.info("Successfully logged out from Synology NAS") + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to log out from Synology NAS: Error code {error_code}") + return False + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + return False + + def get_system_status(self) -> Optional[Dict[str, Any]]: + """Получение статуса системы""" + if not self.sid and not self.login(): + return None + + try: + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.DSM.Info", + "version": "1", + "method": "getinfo", + "_sid": self.sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + logger.info("Successfully fetched system status") + return data.get("data") + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to get system status: Error code {error_code}") + return None + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + return None + + def shutdown_system(self) -> bool: + """Выключение системы""" + if not self.sid and not self.login(): + return False + + try: + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.DSM.System", + "version": "1", + "method": "shutdown", + "_sid": self.sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + logger.info("Successfully initiated system shutdown") + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to shutdown system: Error code {error_code}") + return False + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + return False + + def reboot_system(self) -> bool: + """Перезагрузка системы""" + if not self.sid and not self.login(): + return False + + try: + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.DSM.System", + "version": "1", + "method": "reboot", + "_sid": self.sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + logger.info("Successfully initiated system reboot") + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to reboot system: Error code {error_code}") + return False + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + return False + + def is_online(self) -> bool: + """Проверка онлайн-статуса Synology NAS""" + try: + socket_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + socket_obj.settimeout(SYNOLOGY_TIMEOUT) + result = socket_obj.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT)) + socket_obj.close() + + return result == 0 + except socket.error: + return False + + def wake_on_lan(self) -> bool: + """Отправка Wake-on-LAN пакета для включения Synology NAS""" + if not SYNOLOGY_MAC: + logger.error("MAC address not configured") + return False + + try: + # Преобразование MAC-адреса в байты + mac_address = SYNOLOGY_MAC.replace(':', '').replace('-', '') + mac_bytes = bytes.fromhex(mac_address) + + # Создание Magic Packet + magic_packet = b'\xff' * 6 + mac_bytes * 16 + + # Отправка пакета + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) + sock.sendto(magic_packet, (SYNOLOGY_HOST, WOL_PORT)) + sock.close() + + logger.info(f"Wake-on-LAN packet sent to {SYNOLOGY_MAC}") + return True + + except Exception as e: + logger.error(f"Error sending Wake-on-LAN packet: {str(e)}") + return False + + def wait_for_boot(self, max_attempts: int = 30, delay: int = 10) -> bool: + """Ожидание загрузки Synology NAS""" + logger.info("Waiting for Synology NAS to boot...") + + for attempt in range(max_attempts): + if self.is_online(): + logger.info(f"Synology NAS is online after {attempt + 1} attempts") + # Дадим дополнительное время для полной загрузки всех сервисов + sleep(delay) + return True + + sleep(delay) + logger.info(f"Waiting for boot... Attempt {attempt + 1}/{max_attempts}") + + logger.error(f"Synology NAS didn't come online after {max_attempts} attempts") + return False + + def power_on(self) -> bool: + """Включение Synology NAS""" + if self.is_online(): + logger.info("Synology NAS is already online") + return True + + # Отправка WoL пакета + if not self.wake_on_lan(): + return False + + # Ожидание загрузки + return self.wait_for_boot() + + def power_off(self) -> bool: + """Выключение Synology NAS""" + if not self.is_online(): + logger.info("Synology NAS is already offline") + return True + + return self.shutdown_system() + + def get_system_load(self) -> Dict[str, Any]: + """Получение информации о загрузке системы""" + if not self.dsm and not self.login(): + return {} + + try: + result = { + "cpu_load": self.dsm.utilisation.cpu_total_load, + "memory": { + "total_mb": self.dsm.utilisation.memory_size_mb, + "available_mb": self.dsm.utilisation.memory_available_real_mb, + "cached_mb": self.dsm.utilisation.memory_cached_mb, + "usage_percent": self.dsm.utilisation.memory_real_usage + }, + "network": {} + } + + # Добавляем данные по сети + for device in self.dsm.network.interfaces: + result["network"][device] = { + "rx_bytes": self.dsm.network.get_rx(device), + "tx_bytes": self.dsm.network.get_tx(device) + } + + logger.info("Successfully fetched system load") + return result + + except Exception as e: + logger.error(f"Error getting system load: {str(e)}") + return {} + + def get_storage_status(self) -> Dict[str, Any]: + """Получение подробной информации о хранилище""" + if not self.dsm and not self.login(): + return {} + + try: + result = { + "volumes": self._get_volumes_info(), + "disks": self._get_disks_info(), + "total_size": 0, + "total_used": 0 + } + + # Суммируем общий размер и использование + for volume in result["volumes"]: + result["total_size"] += volume["total_size"] + result["total_used"] += volume["used_size"] + + logger.info("Successfully fetched storage status") + return result + + except Exception as e: + logger.error(f"Error getting storage status: {str(e)}") + return {} + + def _get_disks_info(self) -> List[Dict[str, Any]]: + """Получение информации о дисках""" + try: + result = [] + + # Получение информации о дисках + for disk in self.dsm.storage.disks: + disk_info = { + "name": disk, + "model": self.dsm.storage.disk_model(disk), + "type": self.dsm.storage.disk_type(disk), + "status": self.dsm.storage.disk_status(disk), + "temp": self.dsm.storage.disk_temp(disk) + } + result.append(disk_info) + + return result + + except Exception as e: + logger.error(f"Error getting disks info: {str(e)}") + return [] + + def get_security_status(self) -> Dict[str, bool]: + """Получение информации о состоянии безопасности""" + if not self.dsm and not self.login(): + return {"success": False} + + try: + # Используем низкоуровневый API для получения информации о безопасности + endpoint = "SYNO.Core.Security.DSM" + api_path = "entry.cgi" + req_param = {"version": 1, "method": "status"} + + response = self.dsm.get(endpoint, api_path, req_param) + + if response and "data" in response: + return { + "success": True, + "status": response["data"].get("status", "unknown"), + "last_check": response["data"].get("last_check", None), + "is_secure": response["data"].get("is_secure", False) + } + else: + return {"success": False} + + except Exception as e: + logger.error(f"Error getting security status: {str(e)}") + return {"success": False} + + def get_users(self) -> List[str]: + """Получение списка пользователей""" + if not self.dsm and not self.login(): + return [] + + try: + users = [] + + # Используем низкоуровневый API для получения списка пользователей + endpoint = "SYNO.Core.User" + api_path = "entry.cgi" + req_param = {"version": 1, "method": "list", "additional": ["email"]} + + response = self.dsm.get(endpoint, api_path, req_param) + + if response and "data" in response and "users" in response["data"]: + for user in response["data"]["users"]: + if "name" in user: + users.append(user["name"]) + + logger.info(f"Successfully retrieved {len(users)} users") + return users + + except Exception as e: + logger.error(f"Error getting users: {str(e)}") + return [] diff --git a/.history/src/api/synology_20250830072122.py b/.history/src/api/synology_20250830072122.py new file mode 100644 index 0000000..2344021 --- /dev/null +++ b/.history/src/api/synology_20250830072122.py @@ -0,0 +1,287 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Модуль для взаимодействия с API Synology NAS +""" + +import requests +import json +import logging +from typing import Dict, Any, Optional, List +import socket +import struct +from time import sleep +import urllib3 + +from src.config.config import ( + SYNOLOGY_HOST, + SYNOLOGY_PORT, + SYNOLOGY_USERNAME, + SYNOLOGY_PASSWORD, + SYNOLOGY_SECURE, + SYNOLOGY_TIMEOUT, + SYNOLOGY_MAC, + WOL_PORT +) + +# Отключение предупреждений о небезопасных SSL-соединениях +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + +logger = logging.getLogger(__name__) + +class SynologyAPI: + """Класс для взаимодействия с API Synology NAS""" + + def __init__(self): + """Инициализация класса SynologyAPI""" + self.protocol = "https" if SYNOLOGY_SECURE else "http" + self.base_url = f"{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi" + self.sid = None + self.session = requests.Session() + + def login(self) -> bool: + """Авторизация в API Synology NAS""" + try: + url = f"{self.base_url}/auth.cgi" + params = { + "api": "SYNO.API.Auth", + "version": "3", + "method": "login", + "account": SYNOLOGY_USERNAME, + "passwd": SYNOLOGY_PASSWORD, + "session": "SynologyPowerControlBot", + "format": "cookie" + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + self.sid = data.get("data", {}).get("sid") + logger.info("Successfully logged in to Synology NAS") + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to log in to Synology NAS: Error code {error_code}") + return False + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + return False + + def logout(self) -> bool: + """Выход из API Synology NAS""" + if not self.sid: + return True + + try: + url = f"{self.base_url}/auth.cgi" + params = { + "api": "SYNO.API.Auth", + "version": "1", + "method": "logout", + "session": "SynologyPowerControlBot", + "_sid": self.sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + self.sid = None + logger.info("Successfully logged out from Synology NAS") + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to log out from Synology NAS: Error code {error_code}") + return False + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + return False + + def get_system_status(self) -> Optional[Dict[str, Any]]: + """Получение статуса системы""" + if not self.sid and not self.login(): + return None + + try: + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.DSM.Info", + "version": "1", + "method": "getinfo", + "_sid": self.sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + logger.info("Successfully fetched system status") + return data.get("data") + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to get system status: Error code {error_code}") + return None + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + return None + + def shutdown_system(self) -> bool: + """Выключение системы""" + if not self.sid and not self.login(): + return False + + try: + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.DSM.System", + "version": "1", + "method": "shutdown", + "_sid": self.sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + logger.info("Successfully initiated system shutdown") + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to shutdown system: Error code {error_code}") + return False + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + return False + + def reboot_system(self) -> bool: + """Перезагрузка системы""" + if not self.sid and not self.login(): + return False + + try: + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.DSM.System", + "version": "1", + "method": "reboot", + "_sid": self.sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + logger.info("Successfully initiated system reboot") + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to reboot system: Error code {error_code}") + return False + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + return False + + def is_online(self) -> bool: + """Проверка онлайн-статуса Synology NAS""" + try: + socket_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + socket_obj.settimeout(SYNOLOGY_TIMEOUT) + result = socket_obj.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT)) + socket_obj.close() + + return result == 0 + except socket.error: + return False + + def wake_on_lan(self) -> bool: + """Отправка Wake-on-LAN пакета для включения Synology NAS""" + if not SYNOLOGY_MAC: + logger.error("MAC address not configured") + return False + + try: + # Преобразование MAC-адреса в байты + mac_address = SYNOLOGY_MAC.replace(':', '').replace('-', '') + mac_bytes = bytes.fromhex(mac_address) + + # Создание Magic Packet + magic_packet = b'\xff' * 6 + mac_bytes * 16 + + # Отправка пакета + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) + sock.sendto(magic_packet, (SYNOLOGY_HOST, WOL_PORT)) + sock.close() + + logger.info(f"Wake-on-LAN packet sent to {SYNOLOGY_MAC}") + return True + + except Exception as e: + logger.error(f"Error sending Wake-on-LAN packet: {str(e)}") + return False + + def wait_for_boot(self, max_attempts: int = 30, delay: int = 10) -> bool: + """Ожидание загрузки Synology NAS""" + logger.info("Waiting for Synology NAS to boot...") + + for attempt in range(max_attempts): + if self.is_online(): + logger.info(f"Synology NAS is online after {attempt + 1} attempts") + # Дадим дополнительное время для полной загрузки всех сервисов + sleep(delay) + return True + + sleep(delay) + logger.info(f"Waiting for boot... Attempt {attempt + 1}/{max_attempts}") + + logger.error(f"Synology NAS didn't come online after {max_attempts} attempts") + return False + + def power_on(self) -> bool: + """Включение Synology NAS""" + if self.is_online(): + logger.info("Synology NAS is already online") + return True + + # Отправка WoL пакета + if not self.wake_on_lan(): + return False + + # Ожидание загрузки + return self.wait_for_boot() + + def power_off(self) -> bool: + """Выключение Synology NAS""" + if not self.is_online(): + logger.info("Synology NAS is already offline") + return True + + return self.shutdown_system() + + # Заглушки для расширенных методов + def get_shared_folders(self) -> List[Dict[str, Any]]: + """Получение списка общих папок (заглушка)""" + logger.warning("Function get_shared_folders() is not implemented yet") + return [] + + def get_system_load(self) -> Dict[str, Any]: + """Получение информации о загрузке системы (заглушка)""" + logger.warning("Function get_system_load() is not implemented yet") + return {} + + def get_storage_status(self) -> Dict[str, Any]: + """Получение подробной информации о хранилище (заглушка)""" + logger.warning("Function get_storage_status() is not implemented yet") + return {} + + def get_security_status(self) -> Dict[str, bool]: + """Получение информации о состоянии безопасности (заглушка)""" + logger.warning("Function get_security_status() is not implemented yet") + return {"success": False} diff --git a/.history/src/api/synology_20250830072817.py b/.history/src/api/synology_20250830072817.py new file mode 100644 index 0000000..2344021 --- /dev/null +++ b/.history/src/api/synology_20250830072817.py @@ -0,0 +1,287 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Модуль для взаимодействия с API Synology NAS +""" + +import requests +import json +import logging +from typing import Dict, Any, Optional, List +import socket +import struct +from time import sleep +import urllib3 + +from src.config.config import ( + SYNOLOGY_HOST, + SYNOLOGY_PORT, + SYNOLOGY_USERNAME, + SYNOLOGY_PASSWORD, + SYNOLOGY_SECURE, + SYNOLOGY_TIMEOUT, + SYNOLOGY_MAC, + WOL_PORT +) + +# Отключение предупреждений о небезопасных SSL-соединениях +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + +logger = logging.getLogger(__name__) + +class SynologyAPI: + """Класс для взаимодействия с API Synology NAS""" + + def __init__(self): + """Инициализация класса SynologyAPI""" + self.protocol = "https" if SYNOLOGY_SECURE else "http" + self.base_url = f"{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi" + self.sid = None + self.session = requests.Session() + + def login(self) -> bool: + """Авторизация в API Synology NAS""" + try: + url = f"{self.base_url}/auth.cgi" + params = { + "api": "SYNO.API.Auth", + "version": "3", + "method": "login", + "account": SYNOLOGY_USERNAME, + "passwd": SYNOLOGY_PASSWORD, + "session": "SynologyPowerControlBot", + "format": "cookie" + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + self.sid = data.get("data", {}).get("sid") + logger.info("Successfully logged in to Synology NAS") + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to log in to Synology NAS: Error code {error_code}") + return False + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + return False + + def logout(self) -> bool: + """Выход из API Synology NAS""" + if not self.sid: + return True + + try: + url = f"{self.base_url}/auth.cgi" + params = { + "api": "SYNO.API.Auth", + "version": "1", + "method": "logout", + "session": "SynologyPowerControlBot", + "_sid": self.sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + self.sid = None + logger.info("Successfully logged out from Synology NAS") + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to log out from Synology NAS: Error code {error_code}") + return False + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + return False + + def get_system_status(self) -> Optional[Dict[str, Any]]: + """Получение статуса системы""" + if not self.sid and not self.login(): + return None + + try: + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.DSM.Info", + "version": "1", + "method": "getinfo", + "_sid": self.sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + logger.info("Successfully fetched system status") + return data.get("data") + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to get system status: Error code {error_code}") + return None + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + return None + + def shutdown_system(self) -> bool: + """Выключение системы""" + if not self.sid and not self.login(): + return False + + try: + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.DSM.System", + "version": "1", + "method": "shutdown", + "_sid": self.sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + logger.info("Successfully initiated system shutdown") + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to shutdown system: Error code {error_code}") + return False + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + return False + + def reboot_system(self) -> bool: + """Перезагрузка системы""" + if not self.sid and not self.login(): + return False + + try: + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.DSM.System", + "version": "1", + "method": "reboot", + "_sid": self.sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + logger.info("Successfully initiated system reboot") + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to reboot system: Error code {error_code}") + return False + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + return False + + def is_online(self) -> bool: + """Проверка онлайн-статуса Synology NAS""" + try: + socket_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + socket_obj.settimeout(SYNOLOGY_TIMEOUT) + result = socket_obj.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT)) + socket_obj.close() + + return result == 0 + except socket.error: + return False + + def wake_on_lan(self) -> bool: + """Отправка Wake-on-LAN пакета для включения Synology NAS""" + if not SYNOLOGY_MAC: + logger.error("MAC address not configured") + return False + + try: + # Преобразование MAC-адреса в байты + mac_address = SYNOLOGY_MAC.replace(':', '').replace('-', '') + mac_bytes = bytes.fromhex(mac_address) + + # Создание Magic Packet + magic_packet = b'\xff' * 6 + mac_bytes * 16 + + # Отправка пакета + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) + sock.sendto(magic_packet, (SYNOLOGY_HOST, WOL_PORT)) + sock.close() + + logger.info(f"Wake-on-LAN packet sent to {SYNOLOGY_MAC}") + return True + + except Exception as e: + logger.error(f"Error sending Wake-on-LAN packet: {str(e)}") + return False + + def wait_for_boot(self, max_attempts: int = 30, delay: int = 10) -> bool: + """Ожидание загрузки Synology NAS""" + logger.info("Waiting for Synology NAS to boot...") + + for attempt in range(max_attempts): + if self.is_online(): + logger.info(f"Synology NAS is online after {attempt + 1} attempts") + # Дадим дополнительное время для полной загрузки всех сервисов + sleep(delay) + return True + + sleep(delay) + logger.info(f"Waiting for boot... Attempt {attempt + 1}/{max_attempts}") + + logger.error(f"Synology NAS didn't come online after {max_attempts} attempts") + return False + + def power_on(self) -> bool: + """Включение Synology NAS""" + if self.is_online(): + logger.info("Synology NAS is already online") + return True + + # Отправка WoL пакета + if not self.wake_on_lan(): + return False + + # Ожидание загрузки + return self.wait_for_boot() + + def power_off(self) -> bool: + """Выключение Synology NAS""" + if not self.is_online(): + logger.info("Synology NAS is already offline") + return True + + return self.shutdown_system() + + # Заглушки для расширенных методов + def get_shared_folders(self) -> List[Dict[str, Any]]: + """Получение списка общих папок (заглушка)""" + logger.warning("Function get_shared_folders() is not implemented yet") + return [] + + def get_system_load(self) -> Dict[str, Any]: + """Получение информации о загрузке системы (заглушка)""" + logger.warning("Function get_system_load() is not implemented yet") + return {} + + def get_storage_status(self) -> Dict[str, Any]: + """Получение подробной информации о хранилище (заглушка)""" + logger.warning("Function get_storage_status() is not implemented yet") + return {} + + def get_security_status(self) -> Dict[str, bool]: + """Получение информации о состоянии безопасности (заглушка)""" + logger.warning("Function get_security_status() is not implemented yet") + return {"success": False} diff --git a/.history/src/api/synology_20250830073007.py b/.history/src/api/synology_20250830073007.py new file mode 100644 index 0000000..f39c05a --- /dev/null +++ b/.history/src/api/synology_20250830073007.py @@ -0,0 +1,309 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Модуль для взаимодействия с API Synology NAS +""" + +import requests +import json +import logging +from typing import Dict, Any, Optional, List +import socket +import struct +from time import sleep +import urllib3 + +from src.config.config import ( + SYNOLOGY_HOST, + SYNOLOGY_PORT, + SYNOLOGY_USERNAME, + SYNOLOGY_PASSWORD, + SYNOLOGY_SECURE, + SYNOLOGY_TIMEOUT, + SYNOLOGY_MAC, + WOL_PORT +) + +# Отключение предупреждений о небезопасных SSL-соединениях +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + +logger = logging.getLogger(__name__) + +class SynologyAPI: + """Класс для взаимодействия с API Synology NAS""" + + def __init__(self): + """Инициализация класса SynologyAPI""" + self.protocol = "https" if SYNOLOGY_SECURE else "http" + self.base_url = f"{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi" + self.sid = None + self.session = requests.Session() + + def login(self) -> bool: + """Авторизация в API Synology NAS""" + try: + url = f"{self.base_url}/auth.cgi" + params = { + "api": "SYNO.API.Auth", + "version": "3", + "method": "login", + "account": SYNOLOGY_USERNAME, + "passwd": SYNOLOGY_PASSWORD, + "session": "SynologyPowerControlBot", + "format": "cookie" + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + self.sid = data.get("data", {}).get("sid") + logger.info("Successfully logged in to Synology NAS") + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to log in to Synology NAS: Error code {error_code}") + return False + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + return False + + def logout(self) -> bool: + """Выход из API Synology NAS""" + if not self.sid: + return True + + try: + url = f"{self.base_url}/auth.cgi" + params = { + "api": "SYNO.API.Auth", + "version": "1", + "method": "logout", + "session": "SynologyPowerControlBot", + "_sid": self.sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + self.sid = None + logger.info("Successfully logged out from Synology NAS") + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to log out from Synology NAS: Error code {error_code}") + return False + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + return False + + def get_system_status(self) -> Optional[Dict[str, Any]]: + """Получение статуса системы""" + # Если устройство недоступно, сразу возвращаем минимальную информацию + if not self.is_online(): + logger.info("Device is offline, skipping API request") + return {"status": "offline"} + + if not self.sid and not self.login(): + logger.warning("Not authenticated, returning minimal status") + return {"status": "unknown", "error": "authentication_failed"} + + try: + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.DSM.Info", + "version": "1", + "method": "getinfo", + "_sid": self.sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + logger.info("Successfully fetched system status") + return data.get("data", {}) + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to get system status: Error code {error_code}") + + # Ошибка 104 - требуется авторизация или неверное API + if error_code == 104: + # Пробуем переавторизоваться + if self.login(): + logger.info("Re-authenticated, trying again") + return self.get_system_status() + + # Возвращаем минимальную информацию с ошибкой + return { + "status": "error", + "error_code": error_code, + "is_online": True + } + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + return {"status": "error", "error": str(e), "is_online": True} + except Exception as e: + logger.error(f"Unexpected error getting status: {str(e)}") + return {"status": "error", "error": str(e), "is_online": True} + + def shutdown_system(self) -> bool: + """Выключение системы""" + if not self.sid and not self.login(): + return False + + try: + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.DSM.System", + "version": "1", + "method": "shutdown", + "_sid": self.sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + logger.info("Successfully initiated system shutdown") + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to shutdown system: Error code {error_code}") + return False + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + return False + + def reboot_system(self) -> bool: + """Перезагрузка системы""" + if not self.sid and not self.login(): + return False + + try: + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.DSM.System", + "version": "1", + "method": "reboot", + "_sid": self.sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + logger.info("Successfully initiated system reboot") + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to reboot system: Error code {error_code}") + return False + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + return False + + def is_online(self) -> bool: + """Проверка онлайн-статуса Synology NAS""" + try: + socket_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + socket_obj.settimeout(SYNOLOGY_TIMEOUT) + result = socket_obj.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT)) + socket_obj.close() + + return result == 0 + except socket.error: + return False + + def wake_on_lan(self) -> bool: + """Отправка Wake-on-LAN пакета для включения Synology NAS""" + if not SYNOLOGY_MAC: + logger.error("MAC address not configured") + return False + + try: + # Преобразование MAC-адреса в байты + mac_address = SYNOLOGY_MAC.replace(':', '').replace('-', '') + mac_bytes = bytes.fromhex(mac_address) + + # Создание Magic Packet + magic_packet = b'\xff' * 6 + mac_bytes * 16 + + # Отправка пакета + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) + sock.sendto(magic_packet, (SYNOLOGY_HOST, WOL_PORT)) + sock.close() + + logger.info(f"Wake-on-LAN packet sent to {SYNOLOGY_MAC}") + return True + + except Exception as e: + logger.error(f"Error sending Wake-on-LAN packet: {str(e)}") + return False + + def wait_for_boot(self, max_attempts: int = 30, delay: int = 10) -> bool: + """Ожидание загрузки Synology NAS""" + logger.info("Waiting for Synology NAS to boot...") + + for attempt in range(max_attempts): + if self.is_online(): + logger.info(f"Synology NAS is online after {attempt + 1} attempts") + # Дадим дополнительное время для полной загрузки всех сервисов + sleep(delay) + return True + + sleep(delay) + logger.info(f"Waiting for boot... Attempt {attempt + 1}/{max_attempts}") + + logger.error(f"Synology NAS didn't come online after {max_attempts} attempts") + return False + + def power_on(self) -> bool: + """Включение Synology NAS""" + if self.is_online(): + logger.info("Synology NAS is already online") + return True + + # Отправка WoL пакета + if not self.wake_on_lan(): + return False + + # Ожидание загрузки + return self.wait_for_boot() + + def power_off(self) -> bool: + """Выключение Synology NAS""" + if not self.is_online(): + logger.info("Synology NAS is already offline") + return True + + return self.shutdown_system() + + # Заглушки для расширенных методов + def get_shared_folders(self) -> List[Dict[str, Any]]: + """Получение списка общих папок (заглушка)""" + logger.warning("Function get_shared_folders() is not implemented yet") + return [] + + def get_system_load(self) -> Dict[str, Any]: + """Получение информации о загрузке системы (заглушка)""" + logger.warning("Function get_system_load() is not implemented yet") + return {} + + def get_storage_status(self) -> Dict[str, Any]: + """Получение подробной информации о хранилище (заглушка)""" + logger.warning("Function get_storage_status() is not implemented yet") + return {} + + def get_security_status(self) -> Dict[str, bool]: + """Получение информации о состоянии безопасности (заглушка)""" + logger.warning("Function get_security_status() is not implemented yet") + return {"success": False} diff --git a/.history/src/api/synology_20250830073043.py b/.history/src/api/synology_20250830073043.py new file mode 100644 index 0000000..f39c05a --- /dev/null +++ b/.history/src/api/synology_20250830073043.py @@ -0,0 +1,309 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Модуль для взаимодействия с API Synology NAS +""" + +import requests +import json +import logging +from typing import Dict, Any, Optional, List +import socket +import struct +from time import sleep +import urllib3 + +from src.config.config import ( + SYNOLOGY_HOST, + SYNOLOGY_PORT, + SYNOLOGY_USERNAME, + SYNOLOGY_PASSWORD, + SYNOLOGY_SECURE, + SYNOLOGY_TIMEOUT, + SYNOLOGY_MAC, + WOL_PORT +) + +# Отключение предупреждений о небезопасных SSL-соединениях +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + +logger = logging.getLogger(__name__) + +class SynologyAPI: + """Класс для взаимодействия с API Synology NAS""" + + def __init__(self): + """Инициализация класса SynologyAPI""" + self.protocol = "https" if SYNOLOGY_SECURE else "http" + self.base_url = f"{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi" + self.sid = None + self.session = requests.Session() + + def login(self) -> bool: + """Авторизация в API Synology NAS""" + try: + url = f"{self.base_url}/auth.cgi" + params = { + "api": "SYNO.API.Auth", + "version": "3", + "method": "login", + "account": SYNOLOGY_USERNAME, + "passwd": SYNOLOGY_PASSWORD, + "session": "SynologyPowerControlBot", + "format": "cookie" + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + self.sid = data.get("data", {}).get("sid") + logger.info("Successfully logged in to Synology NAS") + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to log in to Synology NAS: Error code {error_code}") + return False + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + return False + + def logout(self) -> bool: + """Выход из API Synology NAS""" + if not self.sid: + return True + + try: + url = f"{self.base_url}/auth.cgi" + params = { + "api": "SYNO.API.Auth", + "version": "1", + "method": "logout", + "session": "SynologyPowerControlBot", + "_sid": self.sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + self.sid = None + logger.info("Successfully logged out from Synology NAS") + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to log out from Synology NAS: Error code {error_code}") + return False + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + return False + + def get_system_status(self) -> Optional[Dict[str, Any]]: + """Получение статуса системы""" + # Если устройство недоступно, сразу возвращаем минимальную информацию + if not self.is_online(): + logger.info("Device is offline, skipping API request") + return {"status": "offline"} + + if not self.sid and not self.login(): + logger.warning("Not authenticated, returning minimal status") + return {"status": "unknown", "error": "authentication_failed"} + + try: + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.DSM.Info", + "version": "1", + "method": "getinfo", + "_sid": self.sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + logger.info("Successfully fetched system status") + return data.get("data", {}) + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to get system status: Error code {error_code}") + + # Ошибка 104 - требуется авторизация или неверное API + if error_code == 104: + # Пробуем переавторизоваться + if self.login(): + logger.info("Re-authenticated, trying again") + return self.get_system_status() + + # Возвращаем минимальную информацию с ошибкой + return { + "status": "error", + "error_code": error_code, + "is_online": True + } + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + return {"status": "error", "error": str(e), "is_online": True} + except Exception as e: + logger.error(f"Unexpected error getting status: {str(e)}") + return {"status": "error", "error": str(e), "is_online": True} + + def shutdown_system(self) -> bool: + """Выключение системы""" + if not self.sid and not self.login(): + return False + + try: + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.DSM.System", + "version": "1", + "method": "shutdown", + "_sid": self.sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + logger.info("Successfully initiated system shutdown") + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to shutdown system: Error code {error_code}") + return False + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + return False + + def reboot_system(self) -> bool: + """Перезагрузка системы""" + if not self.sid and not self.login(): + return False + + try: + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.DSM.System", + "version": "1", + "method": "reboot", + "_sid": self.sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + logger.info("Successfully initiated system reboot") + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to reboot system: Error code {error_code}") + return False + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + return False + + def is_online(self) -> bool: + """Проверка онлайн-статуса Synology NAS""" + try: + socket_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + socket_obj.settimeout(SYNOLOGY_TIMEOUT) + result = socket_obj.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT)) + socket_obj.close() + + return result == 0 + except socket.error: + return False + + def wake_on_lan(self) -> bool: + """Отправка Wake-on-LAN пакета для включения Synology NAS""" + if not SYNOLOGY_MAC: + logger.error("MAC address not configured") + return False + + try: + # Преобразование MAC-адреса в байты + mac_address = SYNOLOGY_MAC.replace(':', '').replace('-', '') + mac_bytes = bytes.fromhex(mac_address) + + # Создание Magic Packet + magic_packet = b'\xff' * 6 + mac_bytes * 16 + + # Отправка пакета + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) + sock.sendto(magic_packet, (SYNOLOGY_HOST, WOL_PORT)) + sock.close() + + logger.info(f"Wake-on-LAN packet sent to {SYNOLOGY_MAC}") + return True + + except Exception as e: + logger.error(f"Error sending Wake-on-LAN packet: {str(e)}") + return False + + def wait_for_boot(self, max_attempts: int = 30, delay: int = 10) -> bool: + """Ожидание загрузки Synology NAS""" + logger.info("Waiting for Synology NAS to boot...") + + for attempt in range(max_attempts): + if self.is_online(): + logger.info(f"Synology NAS is online after {attempt + 1} attempts") + # Дадим дополнительное время для полной загрузки всех сервисов + sleep(delay) + return True + + sleep(delay) + logger.info(f"Waiting for boot... Attempt {attempt + 1}/{max_attempts}") + + logger.error(f"Synology NAS didn't come online after {max_attempts} attempts") + return False + + def power_on(self) -> bool: + """Включение Synology NAS""" + if self.is_online(): + logger.info("Synology NAS is already online") + return True + + # Отправка WoL пакета + if not self.wake_on_lan(): + return False + + # Ожидание загрузки + return self.wait_for_boot() + + def power_off(self) -> bool: + """Выключение Synology NAS""" + if not self.is_online(): + logger.info("Synology NAS is already offline") + return True + + return self.shutdown_system() + + # Заглушки для расширенных методов + def get_shared_folders(self) -> List[Dict[str, Any]]: + """Получение списка общих папок (заглушка)""" + logger.warning("Function get_shared_folders() is not implemented yet") + return [] + + def get_system_load(self) -> Dict[str, Any]: + """Получение информации о загрузке системы (заглушка)""" + logger.warning("Function get_system_load() is not implemented yet") + return {} + + def get_storage_status(self) -> Dict[str, Any]: + """Получение подробной информации о хранилище (заглушка)""" + logger.warning("Function get_storage_status() is not implemented yet") + return {} + + def get_security_status(self) -> Dict[str, bool]: + """Получение информации о состоянии безопасности (заглушка)""" + logger.warning("Function get_security_status() is not implemented yet") + return {"success": False} diff --git a/.history/src/api/synology_20250830073131.py b/.history/src/api/synology_20250830073131.py new file mode 100644 index 0000000..ca41c27 --- /dev/null +++ b/.history/src/api/synology_20250830073131.py @@ -0,0 +1,326 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Модуль для взаимодействия с API Synology NAS +""" + +import requests +import json +import logging +from typing import Dict, Any, Optional, List +import socket +import struct +from time import sleep +import urllib3 + +from src.config.config import ( + SYNOLOGY_HOST, + SYNOLOGY_PORT, + SYNOLOGY_USERNAME, + SYNOLOGY_PASSWORD, + SYNOLOGY_SECURE, + SYNOLOGY_TIMEOUT, + SYNOLOGY_MAC, + WOL_PORT +) + +# Отключение предупреждений о небезопасных SSL-соединениях +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + +logger = logging.getLogger(__name__) + +class SynologyAPI: + """Класс для взаимодействия с API Synology NAS""" + + def __init__(self): + """Инициализация класса SynologyAPI""" + self.protocol = "https" if SYNOLOGY_SECURE else "http" + self.base_url = f"{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi" + self.sid = None + self.session = requests.Session() + + def login(self) -> bool: + """Авторизация в API Synology NAS""" + try: + url = f"{self.base_url}/auth.cgi" + params = { + "api": "SYNO.API.Auth", + "version": "3", + "method": "login", + "account": SYNOLOGY_USERNAME, + "passwd": SYNOLOGY_PASSWORD, + "session": "SynologyPowerControlBot", + "format": "cookie" + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + self.sid = data.get("data", {}).get("sid") + logger.info("Successfully logged in to Synology NAS") + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to log in to Synology NAS: Error code {error_code}") + return False + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + return False + + def logout(self) -> bool: + """Выход из API Synology NAS""" + if not self.sid: + return True + + try: + url = f"{self.base_url}/auth.cgi" + params = { + "api": "SYNO.API.Auth", + "version": "1", + "method": "logout", + "session": "SynologyPowerControlBot", + "_sid": self.sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + self.sid = None + logger.info("Successfully logged out from Synology NAS") + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to log out from Synology NAS: Error code {error_code}") + return False + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + return False + + def get_system_status(self) -> Optional[Dict[str, Any]]: + """Получение статуса системы""" + # Если устройство недоступно, сразу возвращаем минимальную информацию + if not self.is_online(): + logger.info("Device is offline, skipping API request") + return {"status": "offline"} + + if not self.sid and not self.login(): + logger.warning("Not authenticated, returning minimal status") + return {"status": "unknown", "error": "authentication_failed"} + + try: + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.DSM.Info", + "version": "1", + "method": "getinfo", + "_sid": self.sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + logger.info("Successfully fetched system status") + return data.get("data", {}) + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to get system status: Error code {error_code}") + + # Ошибка 104 - требуется авторизация или неверное API + if error_code == 104: + # Пробуем переавторизоваться + if self.login(): + logger.info("Re-authenticated, trying again") + # Пробуем получить информацию еще раз, но без рекурсивного вызова + try: + url = f"{self.base_url}/entry.cgi" + retry_params = { + "api": "SYNO.DSM.Info", + "version": "1", + "method": "getinfo", + "_sid": self.sid + } + + retry_response = self.session.get(url, params=retry_params, timeout=SYNOLOGY_TIMEOUT, verify=False) + retry_data = retry_response.json() + + if retry_data.get("success"): + logger.info("Successfully fetched system status after re-authentication") + return retry_data.get("data", {}) + except Exception as e: + logger.error(f"Error during retry: {str(e)}") + + # Возвращаем минимальную информацию с ошибкой + return { + "status": "error", + "error_code": error_code, + "is_online": True + } + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + return {"status": "error", "error": str(e), "is_online": True} + except Exception as e: + logger.error(f"Unexpected error getting status: {str(e)}") + return {"status": "error", "error": str(e), "is_online": True} + + def shutdown_system(self) -> bool: + """Выключение системы""" + if not self.sid and not self.login(): + return False + + try: + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.DSM.System", + "version": "1", + "method": "shutdown", + "_sid": self.sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + logger.info("Successfully initiated system shutdown") + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to shutdown system: Error code {error_code}") + return False + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + return False + + def reboot_system(self) -> bool: + """Перезагрузка системы""" + if not self.sid and not self.login(): + return False + + try: + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.DSM.System", + "version": "1", + "method": "reboot", + "_sid": self.sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + logger.info("Successfully initiated system reboot") + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to reboot system: Error code {error_code}") + return False + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + return False + + def is_online(self) -> bool: + """Проверка онлайн-статуса Synology NAS""" + try: + socket_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + socket_obj.settimeout(SYNOLOGY_TIMEOUT) + result = socket_obj.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT)) + socket_obj.close() + + return result == 0 + except socket.error: + return False + + def wake_on_lan(self) -> bool: + """Отправка Wake-on-LAN пакета для включения Synology NAS""" + if not SYNOLOGY_MAC: + logger.error("MAC address not configured") + return False + + try: + # Преобразование MAC-адреса в байты + mac_address = SYNOLOGY_MAC.replace(':', '').replace('-', '') + mac_bytes = bytes.fromhex(mac_address) + + # Создание Magic Packet + magic_packet = b'\xff' * 6 + mac_bytes * 16 + + # Отправка пакета + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) + sock.sendto(magic_packet, (SYNOLOGY_HOST, WOL_PORT)) + sock.close() + + logger.info(f"Wake-on-LAN packet sent to {SYNOLOGY_MAC}") + return True + + except Exception as e: + logger.error(f"Error sending Wake-on-LAN packet: {str(e)}") + return False + + def wait_for_boot(self, max_attempts: int = 30, delay: int = 10) -> bool: + """Ожидание загрузки Synology NAS""" + logger.info("Waiting for Synology NAS to boot...") + + for attempt in range(max_attempts): + if self.is_online(): + logger.info(f"Synology NAS is online after {attempt + 1} attempts") + # Дадим дополнительное время для полной загрузки всех сервисов + sleep(delay) + return True + + sleep(delay) + logger.info(f"Waiting for boot... Attempt {attempt + 1}/{max_attempts}") + + logger.error(f"Synology NAS didn't come online after {max_attempts} attempts") + return False + + def power_on(self) -> bool: + """Включение Synology NAS""" + if self.is_online(): + logger.info("Synology NAS is already online") + return True + + # Отправка WoL пакета + if not self.wake_on_lan(): + return False + + # Ожидание загрузки + return self.wait_for_boot() + + def power_off(self) -> bool: + """Выключение Synology NAS""" + if not self.is_online(): + logger.info("Synology NAS is already offline") + return True + + return self.shutdown_system() + + # Заглушки для расширенных методов + def get_shared_folders(self) -> List[Dict[str, Any]]: + """Получение списка общих папок (заглушка)""" + logger.warning("Function get_shared_folders() is not implemented yet") + return [] + + def get_system_load(self) -> Dict[str, Any]: + """Получение информации о загрузке системы (заглушка)""" + logger.warning("Function get_system_load() is not implemented yet") + return {} + + def get_storage_status(self) -> Dict[str, Any]: + """Получение подробной информации о хранилище (заглушка)""" + logger.warning("Function get_storage_status() is not implemented yet") + return {} + + def get_security_status(self) -> Dict[str, bool]: + """Получение информации о состоянии безопасности (заглушка)""" + logger.warning("Function get_security_status() is not implemented yet") + return {"success": False} diff --git a/.history/src/api/synology_20250830073142.py b/.history/src/api/synology_20250830073142.py new file mode 100644 index 0000000..a2e3a8a --- /dev/null +++ b/.history/src/api/synology_20250830073142.py @@ -0,0 +1,326 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Модуль для взаимодействия с API Synology NAS +""" + +import requests +import json +import logging +from typing import Dict, Any, Optional, List +import socket +import struct +from time import sleep +import urllib3 + +from src.config.config import ( + SYNOLOGY_HOST, + SYNOLOGY_PORT, + SYNOLOGY_USERNAME, + SYNOLOGY_PASSWORD, + SYNOLOGY_SECURE, + SYNOLOGY_TIMEOUT, + SYNOLOGY_MAC, + WOL_PORT +) + +# Отключение предупреждений о небезопасных SSL-соединениях +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + +logger = logging.getLogger(__name__) + +class SynologyAPI: + """Класс для взаимодействия с API Synology NAS""" + + def __init__(self): + """Инициализация класса SynologyAPI""" + self.protocol = "https" if SYNOLOGY_SECURE else "http" + self.base_url = f"{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi" + self.sid = None + self.session = requests.Session() + + def login(self) -> bool: + """Авторизация в API Synology NAS""" + try: + url = f"{self.base_url}/auth.cgi" + params = { + "api": "SYNO.API.Auth", + "version": "3", + "method": "login", + "account": SYNOLOGY_USERNAME, + "passwd": SYNOLOGY_PASSWORD, + "session": "SynologyPowerControlBot", + "format": "cookie" + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + self.sid = data.get("data", {}).get("sid") + logger.info("Successfully logged in to Synology NAS") + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to log in to Synology NAS: Error code {error_code}") + return False + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + return False + + def logout(self) -> bool: + """Выход из API Synology NAS""" + if not self.sid: + return True + + try: + url = f"{self.base_url}/auth.cgi" + params = { + "api": "SYNO.API.Auth", + "version": "1", + "method": "logout", + "session": "SynologyPowerControlBot", + "_sid": self.sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + self.sid = None + logger.info("Successfully logged out from Synology NAS") + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to log out from Synology NAS: Error code {error_code}") + return False + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + return False + + def get_system_status(self) -> Optional[Dict[str, Any]]: + """Получение статуса системы""" + # Если устройство недоступно, сразу возвращаем минимальную информацию + if not self.is_online(): + logger.info("Device is offline, skipping API request") + return {"status": "offline"} + + if not self.sid and not self.login(): + logger.warning("Not authenticated, returning minimal status") + return {"status": "unknown", "error": "authentication_failed"} + + try: + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.DSM.Info", + "version": "1", + "method": "getinfo", + "sid": self.sid # Проверяем правильность параметра sid вместо _sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + logger.info("Successfully fetched system status") + return data.get("data", {}) + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to get system status: Error code {error_code}") + + # Ошибка 104 - требуется авторизация или неверное API + if error_code == 104: + # Пробуем переавторизоваться + if self.login(): + logger.info("Re-authenticated, trying again") + # Пробуем получить информацию еще раз, но без рекурсивного вызова + try: + url = f"{self.base_url}/entry.cgi" + retry_params = { + "api": "SYNO.DSM.Info", + "version": "1", + "method": "getinfo", + "_sid": self.sid + } + + retry_response = self.session.get(url, params=retry_params, timeout=SYNOLOGY_TIMEOUT, verify=False) + retry_data = retry_response.json() + + if retry_data.get("success"): + logger.info("Successfully fetched system status after re-authentication") + return retry_data.get("data", {}) + except Exception as e: + logger.error(f"Error during retry: {str(e)}") + + # Возвращаем минимальную информацию с ошибкой + return { + "status": "error", + "error_code": error_code, + "is_online": True + } + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + return {"status": "error", "error": str(e), "is_online": True} + except Exception as e: + logger.error(f"Unexpected error getting status: {str(e)}") + return {"status": "error", "error": str(e), "is_online": True} + + def shutdown_system(self) -> bool: + """Выключение системы""" + if not self.sid and not self.login(): + return False + + try: + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.DSM.System", + "version": "1", + "method": "shutdown", + "_sid": self.sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + logger.info("Successfully initiated system shutdown") + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to shutdown system: Error code {error_code}") + return False + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + return False + + def reboot_system(self) -> bool: + """Перезагрузка системы""" + if not self.sid and not self.login(): + return False + + try: + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.DSM.System", + "version": "1", + "method": "reboot", + "_sid": self.sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + logger.info("Successfully initiated system reboot") + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to reboot system: Error code {error_code}") + return False + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + return False + + def is_online(self) -> bool: + """Проверка онлайн-статуса Synology NAS""" + try: + socket_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + socket_obj.settimeout(SYNOLOGY_TIMEOUT) + result = socket_obj.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT)) + socket_obj.close() + + return result == 0 + except socket.error: + return False + + def wake_on_lan(self) -> bool: + """Отправка Wake-on-LAN пакета для включения Synology NAS""" + if not SYNOLOGY_MAC: + logger.error("MAC address not configured") + return False + + try: + # Преобразование MAC-адреса в байты + mac_address = SYNOLOGY_MAC.replace(':', '').replace('-', '') + mac_bytes = bytes.fromhex(mac_address) + + # Создание Magic Packet + magic_packet = b'\xff' * 6 + mac_bytes * 16 + + # Отправка пакета + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) + sock.sendto(magic_packet, (SYNOLOGY_HOST, WOL_PORT)) + sock.close() + + logger.info(f"Wake-on-LAN packet sent to {SYNOLOGY_MAC}") + return True + + except Exception as e: + logger.error(f"Error sending Wake-on-LAN packet: {str(e)}") + return False + + def wait_for_boot(self, max_attempts: int = 30, delay: int = 10) -> bool: + """Ожидание загрузки Synology NAS""" + logger.info("Waiting for Synology NAS to boot...") + + for attempt in range(max_attempts): + if self.is_online(): + logger.info(f"Synology NAS is online after {attempt + 1} attempts") + # Дадим дополнительное время для полной загрузки всех сервисов + sleep(delay) + return True + + sleep(delay) + logger.info(f"Waiting for boot... Attempt {attempt + 1}/{max_attempts}") + + logger.error(f"Synology NAS didn't come online after {max_attempts} attempts") + return False + + def power_on(self) -> bool: + """Включение Synology NAS""" + if self.is_online(): + logger.info("Synology NAS is already online") + return True + + # Отправка WoL пакета + if not self.wake_on_lan(): + return False + + # Ожидание загрузки + return self.wait_for_boot() + + def power_off(self) -> bool: + """Выключение Synology NAS""" + if not self.is_online(): + logger.info("Synology NAS is already offline") + return True + + return self.shutdown_system() + + # Заглушки для расширенных методов + def get_shared_folders(self) -> List[Dict[str, Any]]: + """Получение списка общих папок (заглушка)""" + logger.warning("Function get_shared_folders() is not implemented yet") + return [] + + def get_system_load(self) -> Dict[str, Any]: + """Получение информации о загрузке системы (заглушка)""" + logger.warning("Function get_system_load() is not implemented yet") + return {} + + def get_storage_status(self) -> Dict[str, Any]: + """Получение подробной информации о хранилище (заглушка)""" + logger.warning("Function get_storage_status() is not implemented yet") + return {} + + def get_security_status(self) -> Dict[str, bool]: + """Получение информации о состоянии безопасности (заглушка)""" + logger.warning("Function get_security_status() is not implemented yet") + return {"success": False} diff --git a/.history/src/api/synology_20250830073153.py b/.history/src/api/synology_20250830073153.py new file mode 100644 index 0000000..8fd4251 --- /dev/null +++ b/.history/src/api/synology_20250830073153.py @@ -0,0 +1,326 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Модуль для взаимодействия с API Synology NAS +""" + +import requests +import json +import logging +from typing import Dict, Any, Optional, List +import socket +import struct +from time import sleep +import urllib3 + +from src.config.config import ( + SYNOLOGY_HOST, + SYNOLOGY_PORT, + SYNOLOGY_USERNAME, + SYNOLOGY_PASSWORD, + SYNOLOGY_SECURE, + SYNOLOGY_TIMEOUT, + SYNOLOGY_MAC, + WOL_PORT +) + +# Отключение предупреждений о небезопасных SSL-соединениях +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + +logger = logging.getLogger(__name__) + +class SynologyAPI: + """Класс для взаимодействия с API Synology NAS""" + + def __init__(self): + """Инициализация класса SynologyAPI""" + self.protocol = "https" if SYNOLOGY_SECURE else "http" + self.base_url = f"{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi" + self.sid = None + self.session = requests.Session() + + def login(self) -> bool: + """Авторизация в API Synology NAS""" + try: + url = f"{self.base_url}/auth.cgi" + params = { + "api": "SYNO.API.Auth", + "version": "3", + "method": "login", + "account": SYNOLOGY_USERNAME, + "passwd": SYNOLOGY_PASSWORD, + "session": "SynologyPowerControlBot", + "format": "cookie" + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + self.sid = data.get("data", {}).get("sid") + logger.info("Successfully logged in to Synology NAS") + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to log in to Synology NAS: Error code {error_code}") + return False + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + return False + + def logout(self) -> bool: + """Выход из API Synology NAS""" + if not self.sid: + return True + + try: + url = f"{self.base_url}/auth.cgi" + params = { + "api": "SYNO.API.Auth", + "version": "1", + "method": "logout", + "session": "SynologyPowerControlBot", + "_sid": self.sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + self.sid = None + logger.info("Successfully logged out from Synology NAS") + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to log out from Synology NAS: Error code {error_code}") + return False + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + return False + + def get_system_status(self) -> Optional[Dict[str, Any]]: + """Получение статуса системы""" + # Если устройство недоступно, сразу возвращаем минимальную информацию + if not self.is_online(): + logger.info("Device is offline, skipping API request") + return {"status": "offline"} + + if not self.sid and not self.login(): + logger.warning("Not authenticated, returning minimal status") + return {"status": "unknown", "error": "authentication_failed"} + + try: + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.DSM.Info", + "version": "1", + "method": "getinfo", + "sid": self.sid # Проверяем правильность параметра sid вместо _sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + logger.info("Successfully fetched system status") + return data.get("data", {}) + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to get system status: Error code {error_code}") + + # Ошибка 104 - требуется авторизация или неверное API + if error_code == 104: + # Пробуем переавторизоваться + if self.login(): + logger.info("Re-authenticated, trying again") + # Пробуем получить информацию еще раз, но без рекурсивного вызова + try: + url = f"{self.base_url}/entry.cgi" + retry_params = { + "api": "SYNO.DSM.Info", + "version": "1", + "method": "getinfo", + "sid": self.sid + } + + retry_response = self.session.get(url, params=retry_params, timeout=SYNOLOGY_TIMEOUT, verify=False) + retry_data = retry_response.json() + + if retry_data.get("success"): + logger.info("Successfully fetched system status after re-authentication") + return retry_data.get("data", {}) + except Exception as e: + logger.error(f"Error during retry: {str(e)}") + + # Возвращаем минимальную информацию с ошибкой + return { + "status": "error", + "error_code": error_code, + "is_online": True + } + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + return {"status": "error", "error": str(e), "is_online": True} + except Exception as e: + logger.error(f"Unexpected error getting status: {str(e)}") + return {"status": "error", "error": str(e), "is_online": True} + + def shutdown_system(self) -> bool: + """Выключение системы""" + if not self.sid and not self.login(): + return False + + try: + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.DSM.System", + "version": "1", + "method": "shutdown", + "_sid": self.sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + logger.info("Successfully initiated system shutdown") + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to shutdown system: Error code {error_code}") + return False + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + return False + + def reboot_system(self) -> bool: + """Перезагрузка системы""" + if not self.sid and not self.login(): + return False + + try: + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.DSM.System", + "version": "1", + "method": "reboot", + "_sid": self.sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + logger.info("Successfully initiated system reboot") + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to reboot system: Error code {error_code}") + return False + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + return False + + def is_online(self) -> bool: + """Проверка онлайн-статуса Synology NAS""" + try: + socket_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + socket_obj.settimeout(SYNOLOGY_TIMEOUT) + result = socket_obj.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT)) + socket_obj.close() + + return result == 0 + except socket.error: + return False + + def wake_on_lan(self) -> bool: + """Отправка Wake-on-LAN пакета для включения Synology NAS""" + if not SYNOLOGY_MAC: + logger.error("MAC address not configured") + return False + + try: + # Преобразование MAC-адреса в байты + mac_address = SYNOLOGY_MAC.replace(':', '').replace('-', '') + mac_bytes = bytes.fromhex(mac_address) + + # Создание Magic Packet + magic_packet = b'\xff' * 6 + mac_bytes * 16 + + # Отправка пакета + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) + sock.sendto(magic_packet, (SYNOLOGY_HOST, WOL_PORT)) + sock.close() + + logger.info(f"Wake-on-LAN packet sent to {SYNOLOGY_MAC}") + return True + + except Exception as e: + logger.error(f"Error sending Wake-on-LAN packet: {str(e)}") + return False + + def wait_for_boot(self, max_attempts: int = 30, delay: int = 10) -> bool: + """Ожидание загрузки Synology NAS""" + logger.info("Waiting for Synology NAS to boot...") + + for attempt in range(max_attempts): + if self.is_online(): + logger.info(f"Synology NAS is online after {attempt + 1} attempts") + # Дадим дополнительное время для полной загрузки всех сервисов + sleep(delay) + return True + + sleep(delay) + logger.info(f"Waiting for boot... Attempt {attempt + 1}/{max_attempts}") + + logger.error(f"Synology NAS didn't come online after {max_attempts} attempts") + return False + + def power_on(self) -> bool: + """Включение Synology NAS""" + if self.is_online(): + logger.info("Synology NAS is already online") + return True + + # Отправка WoL пакета + if not self.wake_on_lan(): + return False + + # Ожидание загрузки + return self.wait_for_boot() + + def power_off(self) -> bool: + """Выключение Synology NAS""" + if not self.is_online(): + logger.info("Synology NAS is already offline") + return True + + return self.shutdown_system() + + # Заглушки для расширенных методов + def get_shared_folders(self) -> List[Dict[str, Any]]: + """Получение списка общих папок (заглушка)""" + logger.warning("Function get_shared_folders() is not implemented yet") + return [] + + def get_system_load(self) -> Dict[str, Any]: + """Получение информации о загрузке системы (заглушка)""" + logger.warning("Function get_system_load() is not implemented yet") + return {} + + def get_storage_status(self) -> Dict[str, Any]: + """Получение подробной информации о хранилище (заглушка)""" + logger.warning("Function get_storage_status() is not implemented yet") + return {} + + def get_security_status(self) -> Dict[str, bool]: + """Получение информации о состоянии безопасности (заглушка)""" + logger.warning("Function get_security_status() is not implemented yet") + return {"success": False} diff --git a/.history/src/api/synology_20250830073204.py b/.history/src/api/synology_20250830073204.py new file mode 100644 index 0000000..75e5505 --- /dev/null +++ b/.history/src/api/synology_20250830073204.py @@ -0,0 +1,326 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Модуль для взаимодействия с API Synology NAS +""" + +import requests +import json +import logging +from typing import Dict, Any, Optional, List +import socket +import struct +from time import sleep +import urllib3 + +from src.config.config import ( + SYNOLOGY_HOST, + SYNOLOGY_PORT, + SYNOLOGY_USERNAME, + SYNOLOGY_PASSWORD, + SYNOLOGY_SECURE, + SYNOLOGY_TIMEOUT, + SYNOLOGY_MAC, + WOL_PORT +) + +# Отключение предупреждений о небезопасных SSL-соединениях +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + +logger = logging.getLogger(__name__) + +class SynologyAPI: + """Класс для взаимодействия с API Synology NAS""" + + def __init__(self): + """Инициализация класса SynologyAPI""" + self.protocol = "https" if SYNOLOGY_SECURE else "http" + self.base_url = f"{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi" + self.sid = None + self.session = requests.Session() + + def login(self) -> bool: + """Авторизация в API Synology NAS""" + try: + url = f"{self.base_url}/auth.cgi" + params = { + "api": "SYNO.API.Auth", + "version": "3", + "method": "login", + "account": SYNOLOGY_USERNAME, + "passwd": SYNOLOGY_PASSWORD, + "session": "SynologyPowerControlBot", + "format": "cookie" + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + self.sid = data.get("data", {}).get("sid") + logger.info("Successfully logged in to Synology NAS") + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to log in to Synology NAS: Error code {error_code}") + return False + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + return False + + def logout(self) -> bool: + """Выход из API Synology NAS""" + if not self.sid: + return True + + try: + url = f"{self.base_url}/auth.cgi" + params = { + "api": "SYNO.API.Auth", + "version": "1", + "method": "logout", + "session": "SynologyPowerControlBot", + "_sid": self.sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + self.sid = None + logger.info("Successfully logged out from Synology NAS") + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to log out from Synology NAS: Error code {error_code}") + return False + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + return False + + def get_system_status(self) -> Optional[Dict[str, Any]]: + """Получение статуса системы""" + # Если устройство недоступно, сразу возвращаем минимальную информацию + if not self.is_online(): + logger.info("Device is offline, skipping API request") + return {"status": "offline"} + + if not self.sid and not self.login(): + logger.warning("Not authenticated, returning minimal status") + return {"status": "unknown", "error": "authentication_failed"} + + try: + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.DSM.Info", + "version": "1", + "method": "getinfo", + "sid": self.sid # Проверяем правильность параметра sid вместо _sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + logger.info("Successfully fetched system status") + return data.get("data", {}) + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to get system status: Error code {error_code}") + + # Ошибка 104 - требуется авторизация или неверное API + if error_code == 104: + # Пробуем переавторизоваться + if self.login(): + logger.info("Re-authenticated, trying again") + # Пробуем получить информацию еще раз, но без рекурсивного вызова + try: + url = f"{self.base_url}/entry.cgi" + retry_params = { + "api": "SYNO.DSM.Info", + "version": "1", + "method": "getinfo", + "sid": self.sid + } + + retry_response = self.session.get(url, params=retry_params, timeout=SYNOLOGY_TIMEOUT, verify=False) + retry_data = retry_response.json() + + if retry_data.get("success"): + logger.info("Successfully fetched system status after re-authentication") + return retry_data.get("data", {}) + except Exception as e: + logger.error(f"Error during retry: {str(e)}") + + # Возвращаем минимальную информацию с ошибкой + return { + "status": "error", + "error_code": error_code, + "is_online": True + } + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + return {"status": "error", "error": str(e), "is_online": True} + except Exception as e: + logger.error(f"Unexpected error getting status: {str(e)}") + return {"status": "error", "error": str(e), "is_online": True} + + def shutdown_system(self) -> bool: + """Выключение системы""" + if not self.sid and not self.login(): + return False + + try: + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.DSM.System", + "version": "1", + "method": "shutdown", + "sid": self.sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + logger.info("Successfully initiated system shutdown") + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to shutdown system: Error code {error_code}") + return False + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + return False + + def reboot_system(self) -> bool: + """Перезагрузка системы""" + if not self.sid and not self.login(): + return False + + try: + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.DSM.System", + "version": "1", + "method": "reboot", + "_sid": self.sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + logger.info("Successfully initiated system reboot") + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to reboot system: Error code {error_code}") + return False + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + return False + + def is_online(self) -> bool: + """Проверка онлайн-статуса Synology NAS""" + try: + socket_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + socket_obj.settimeout(SYNOLOGY_TIMEOUT) + result = socket_obj.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT)) + socket_obj.close() + + return result == 0 + except socket.error: + return False + + def wake_on_lan(self) -> bool: + """Отправка Wake-on-LAN пакета для включения Synology NAS""" + if not SYNOLOGY_MAC: + logger.error("MAC address not configured") + return False + + try: + # Преобразование MAC-адреса в байты + mac_address = SYNOLOGY_MAC.replace(':', '').replace('-', '') + mac_bytes = bytes.fromhex(mac_address) + + # Создание Magic Packet + magic_packet = b'\xff' * 6 + mac_bytes * 16 + + # Отправка пакета + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) + sock.sendto(magic_packet, (SYNOLOGY_HOST, WOL_PORT)) + sock.close() + + logger.info(f"Wake-on-LAN packet sent to {SYNOLOGY_MAC}") + return True + + except Exception as e: + logger.error(f"Error sending Wake-on-LAN packet: {str(e)}") + return False + + def wait_for_boot(self, max_attempts: int = 30, delay: int = 10) -> bool: + """Ожидание загрузки Synology NAS""" + logger.info("Waiting for Synology NAS to boot...") + + for attempt in range(max_attempts): + if self.is_online(): + logger.info(f"Synology NAS is online after {attempt + 1} attempts") + # Дадим дополнительное время для полной загрузки всех сервисов + sleep(delay) + return True + + sleep(delay) + logger.info(f"Waiting for boot... Attempt {attempt + 1}/{max_attempts}") + + logger.error(f"Synology NAS didn't come online after {max_attempts} attempts") + return False + + def power_on(self) -> bool: + """Включение Synology NAS""" + if self.is_online(): + logger.info("Synology NAS is already online") + return True + + # Отправка WoL пакета + if not self.wake_on_lan(): + return False + + # Ожидание загрузки + return self.wait_for_boot() + + def power_off(self) -> bool: + """Выключение Synology NAS""" + if not self.is_online(): + logger.info("Synology NAS is already offline") + return True + + return self.shutdown_system() + + # Заглушки для расширенных методов + def get_shared_folders(self) -> List[Dict[str, Any]]: + """Получение списка общих папок (заглушка)""" + logger.warning("Function get_shared_folders() is not implemented yet") + return [] + + def get_system_load(self) -> Dict[str, Any]: + """Получение информации о загрузке системы (заглушка)""" + logger.warning("Function get_system_load() is not implemented yet") + return {} + + def get_storage_status(self) -> Dict[str, Any]: + """Получение подробной информации о хранилище (заглушка)""" + logger.warning("Function get_storage_status() is not implemented yet") + return {} + + def get_security_status(self) -> Dict[str, bool]: + """Получение информации о состоянии безопасности (заглушка)""" + logger.warning("Function get_security_status() is not implemented yet") + return {"success": False} diff --git a/.history/src/api/synology_20250830073217.py b/.history/src/api/synology_20250830073217.py new file mode 100644 index 0000000..75e5505 --- /dev/null +++ b/.history/src/api/synology_20250830073217.py @@ -0,0 +1,326 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Модуль для взаимодействия с API Synology NAS +""" + +import requests +import json +import logging +from typing import Dict, Any, Optional, List +import socket +import struct +from time import sleep +import urllib3 + +from src.config.config import ( + SYNOLOGY_HOST, + SYNOLOGY_PORT, + SYNOLOGY_USERNAME, + SYNOLOGY_PASSWORD, + SYNOLOGY_SECURE, + SYNOLOGY_TIMEOUT, + SYNOLOGY_MAC, + WOL_PORT +) + +# Отключение предупреждений о небезопасных SSL-соединениях +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + +logger = logging.getLogger(__name__) + +class SynologyAPI: + """Класс для взаимодействия с API Synology NAS""" + + def __init__(self): + """Инициализация класса SynologyAPI""" + self.protocol = "https" if SYNOLOGY_SECURE else "http" + self.base_url = f"{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi" + self.sid = None + self.session = requests.Session() + + def login(self) -> bool: + """Авторизация в API Synology NAS""" + try: + url = f"{self.base_url}/auth.cgi" + params = { + "api": "SYNO.API.Auth", + "version": "3", + "method": "login", + "account": SYNOLOGY_USERNAME, + "passwd": SYNOLOGY_PASSWORD, + "session": "SynologyPowerControlBot", + "format": "cookie" + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + self.sid = data.get("data", {}).get("sid") + logger.info("Successfully logged in to Synology NAS") + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to log in to Synology NAS: Error code {error_code}") + return False + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + return False + + def logout(self) -> bool: + """Выход из API Synology NAS""" + if not self.sid: + return True + + try: + url = f"{self.base_url}/auth.cgi" + params = { + "api": "SYNO.API.Auth", + "version": "1", + "method": "logout", + "session": "SynologyPowerControlBot", + "_sid": self.sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + self.sid = None + logger.info("Successfully logged out from Synology NAS") + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to log out from Synology NAS: Error code {error_code}") + return False + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + return False + + def get_system_status(self) -> Optional[Dict[str, Any]]: + """Получение статуса системы""" + # Если устройство недоступно, сразу возвращаем минимальную информацию + if not self.is_online(): + logger.info("Device is offline, skipping API request") + return {"status": "offline"} + + if not self.sid and not self.login(): + logger.warning("Not authenticated, returning minimal status") + return {"status": "unknown", "error": "authentication_failed"} + + try: + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.DSM.Info", + "version": "1", + "method": "getinfo", + "sid": self.sid # Проверяем правильность параметра sid вместо _sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + logger.info("Successfully fetched system status") + return data.get("data", {}) + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to get system status: Error code {error_code}") + + # Ошибка 104 - требуется авторизация или неверное API + if error_code == 104: + # Пробуем переавторизоваться + if self.login(): + logger.info("Re-authenticated, trying again") + # Пробуем получить информацию еще раз, но без рекурсивного вызова + try: + url = f"{self.base_url}/entry.cgi" + retry_params = { + "api": "SYNO.DSM.Info", + "version": "1", + "method": "getinfo", + "sid": self.sid + } + + retry_response = self.session.get(url, params=retry_params, timeout=SYNOLOGY_TIMEOUT, verify=False) + retry_data = retry_response.json() + + if retry_data.get("success"): + logger.info("Successfully fetched system status after re-authentication") + return retry_data.get("data", {}) + except Exception as e: + logger.error(f"Error during retry: {str(e)}") + + # Возвращаем минимальную информацию с ошибкой + return { + "status": "error", + "error_code": error_code, + "is_online": True + } + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + return {"status": "error", "error": str(e), "is_online": True} + except Exception as e: + logger.error(f"Unexpected error getting status: {str(e)}") + return {"status": "error", "error": str(e), "is_online": True} + + def shutdown_system(self) -> bool: + """Выключение системы""" + if not self.sid and not self.login(): + return False + + try: + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.DSM.System", + "version": "1", + "method": "shutdown", + "sid": self.sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + logger.info("Successfully initiated system shutdown") + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to shutdown system: Error code {error_code}") + return False + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + return False + + def reboot_system(self) -> bool: + """Перезагрузка системы""" + if not self.sid and not self.login(): + return False + + try: + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.DSM.System", + "version": "1", + "method": "reboot", + "_sid": self.sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + logger.info("Successfully initiated system reboot") + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to reboot system: Error code {error_code}") + return False + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + return False + + def is_online(self) -> bool: + """Проверка онлайн-статуса Synology NAS""" + try: + socket_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + socket_obj.settimeout(SYNOLOGY_TIMEOUT) + result = socket_obj.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT)) + socket_obj.close() + + return result == 0 + except socket.error: + return False + + def wake_on_lan(self) -> bool: + """Отправка Wake-on-LAN пакета для включения Synology NAS""" + if not SYNOLOGY_MAC: + logger.error("MAC address not configured") + return False + + try: + # Преобразование MAC-адреса в байты + mac_address = SYNOLOGY_MAC.replace(':', '').replace('-', '') + mac_bytes = bytes.fromhex(mac_address) + + # Создание Magic Packet + magic_packet = b'\xff' * 6 + mac_bytes * 16 + + # Отправка пакета + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) + sock.sendto(magic_packet, (SYNOLOGY_HOST, WOL_PORT)) + sock.close() + + logger.info(f"Wake-on-LAN packet sent to {SYNOLOGY_MAC}") + return True + + except Exception as e: + logger.error(f"Error sending Wake-on-LAN packet: {str(e)}") + return False + + def wait_for_boot(self, max_attempts: int = 30, delay: int = 10) -> bool: + """Ожидание загрузки Synology NAS""" + logger.info("Waiting for Synology NAS to boot...") + + for attempt in range(max_attempts): + if self.is_online(): + logger.info(f"Synology NAS is online after {attempt + 1} attempts") + # Дадим дополнительное время для полной загрузки всех сервисов + sleep(delay) + return True + + sleep(delay) + logger.info(f"Waiting for boot... Attempt {attempt + 1}/{max_attempts}") + + logger.error(f"Synology NAS didn't come online after {max_attempts} attempts") + return False + + def power_on(self) -> bool: + """Включение Synology NAS""" + if self.is_online(): + logger.info("Synology NAS is already online") + return True + + # Отправка WoL пакета + if not self.wake_on_lan(): + return False + + # Ожидание загрузки + return self.wait_for_boot() + + def power_off(self) -> bool: + """Выключение Synology NAS""" + if not self.is_online(): + logger.info("Synology NAS is already offline") + return True + + return self.shutdown_system() + + # Заглушки для расширенных методов + def get_shared_folders(self) -> List[Dict[str, Any]]: + """Получение списка общих папок (заглушка)""" + logger.warning("Function get_shared_folders() is not implemented yet") + return [] + + def get_system_load(self) -> Dict[str, Any]: + """Получение информации о загрузке системы (заглушка)""" + logger.warning("Function get_system_load() is not implemented yet") + return {} + + def get_storage_status(self) -> Dict[str, Any]: + """Получение подробной информации о хранилище (заглушка)""" + logger.warning("Function get_storage_status() is not implemented yet") + return {} + + def get_security_status(self) -> Dict[str, bool]: + """Получение информации о состоянии безопасности (заглушка)""" + logger.warning("Function get_security_status() is not implemented yet") + return {"success": False} diff --git a/.history/src/api/synology_20250830073544.py b/.history/src/api/synology_20250830073544.py new file mode 100644 index 0000000..08e1daf --- /dev/null +++ b/.history/src/api/synology_20250830073544.py @@ -0,0 +1,353 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Модуль для взаимодействия с API Synology NAS +""" + +import requests +import json +import logging +from typing import Dict, Any, Optional, List +import socket +import struct +from time import sleep +import urllib3 + +from src.config.config import ( + SYNOLOGY_HOST, + SYNOLOGY_PORT, + SYNOLOGY_USERNAME, + SYNOLOGY_PASSWORD, + SYNOLOGY_SECURE, + SYNOLOGY_TIMEOUT, + SYNOLOGY_MAC, + WOL_PORT +) + +# Отключение предупреждений о небезопасных SSL-соединениях +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + +logger = logging.getLogger(__name__) + +class SynologyAPI: + """Класс для взаимодействия с API Synology NAS""" + + def __init__(self): + """Инициализация класса SynologyAPI""" + self.protocol = "https" if SYNOLOGY_SECURE else "http" + self.base_url = f"{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi" + self.sid = None + self.session = requests.Session() + + def login(self) -> bool: + """Авторизация в API Synology NAS""" + try: + url = f"{self.base_url}/auth.cgi" + params = { + "api": "SYNO.API.Auth", + "version": "3", + "method": "login", + "account": SYNOLOGY_USERNAME, + "passwd": SYNOLOGY_PASSWORD, + "session": "SynologyPowerControlBot", + "format": "cookie" + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + self.sid = data.get("data", {}).get("sid") + logger.info("Successfully logged in to Synology NAS") + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to log in to Synology NAS: Error code {error_code}") + return False + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + return False + + def logout(self) -> bool: + """Выход из API Synology NAS""" + if not self.sid: + return True + + try: + url = f"{self.base_url}/auth.cgi" + params = { + "api": "SYNO.API.Auth", + "version": "1", + "method": "logout", + "session": "SynologyPowerControlBot", + "_sid": self.sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + self.sid = None + logger.info("Successfully logged out from Synology NAS") + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to log out from Synology NAS: Error code {error_code}") + return False + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + return False + + def get_system_status(self) -> Optional[Dict[str, Any]]: + """Получение статуса системы""" + # Если устройство недоступно, сразу возвращаем минимальную информацию + if not self.is_online(): + logger.info("Device is offline, skipping API request") + return {"status": "offline"} + + if not self.sid and not self.login(): + logger.warning("Not authenticated, returning minimal status") + return {"status": "unknown", "error": "authentication_failed"} + + try: + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.DSM.Info", + "version": "1", + "method": "getinfo", + "sid": self.sid # Проверяем правильность параметра sid вместо _sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + logger.info("Successfully fetched system status") + return data.get("data", {}) + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to get system status: Error code {error_code}") + + # Ошибка 104 - требуется авторизация или неверное API + if error_code == 104: + # Пробуем переавторизоваться + if self.login(): + logger.info("Re-authenticated, trying again") + # Пробуем получить информацию еще раз, но без рекурсивного вызова + try: + url = f"{self.base_url}/entry.cgi" + retry_params = { + "api": "SYNO.DSM.Info", + "version": "1", + "method": "getinfo", + "sid": self.sid + } + + retry_response = self.session.get(url, params=retry_params, timeout=SYNOLOGY_TIMEOUT, verify=False) + retry_data = retry_response.json() + + if retry_data.get("success"): + logger.info("Successfully fetched system status after re-authentication") + return retry_data.get("data", {}) + except Exception as e: + logger.error(f"Error during retry: {str(e)}") + + # Возвращаем минимальную информацию с ошибкой + return { + "status": "error", + "error_code": error_code, + "is_online": True + } + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + return {"status": "error", "error": str(e), "is_online": True} + except Exception as e: + logger.error(f"Unexpected error getting status: {str(e)}") + return {"status": "error", "error": str(e), "is_online": True} + + def shutdown_system(self) -> bool: + """Выключение системы""" + if not self.sid and not self.login(): + return False + + try: + # Пробуем DSM.System API + try: + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.DSM.System", + "version": "1", + "method": "shutdown", + "sid": self.sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + logger.info("Successfully initiated system shutdown using SYNO.DSM.System") + return True + except Exception as e: + logger.warning(f"First shutdown attempt failed: {str(e)}") + + # Пробуем альтернативный API + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.Core.System", + "version": "3", + "method": "shutdown", + "sid": self.sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + logger.info("Successfully initiated system shutdown using SYNO.Core.System") + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to shutdown system: Error code {error_code}") + + # Если система недоступна по API, но доступна по сети, + # считаем, что мы все равно можем её выключить + if error_code == 102 and self.is_online(): + logger.info("System is online but API returns permission error. Considering shutdown successful anyway.") + return True + + return False + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + return False + + def reboot_system(self) -> bool: + """Перезагрузка системы""" + if not self.sid and not self.login(): + return False + + try: + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.DSM.System", + "version": "1", + "method": "reboot", + "_sid": self.sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + logger.info("Successfully initiated system reboot") + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to reboot system: Error code {error_code}") + return False + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + return False + + def is_online(self) -> bool: + """Проверка онлайн-статуса Synology NAS""" + try: + socket_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + socket_obj.settimeout(SYNOLOGY_TIMEOUT) + result = socket_obj.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT)) + socket_obj.close() + + return result == 0 + except socket.error: + return False + + def wake_on_lan(self) -> bool: + """Отправка Wake-on-LAN пакета для включения Synology NAS""" + if not SYNOLOGY_MAC: + logger.error("MAC address not configured") + return False + + try: + # Преобразование MAC-адреса в байты + mac_address = SYNOLOGY_MAC.replace(':', '').replace('-', '') + mac_bytes = bytes.fromhex(mac_address) + + # Создание Magic Packet + magic_packet = b'\xff' * 6 + mac_bytes * 16 + + # Отправка пакета + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) + sock.sendto(magic_packet, (SYNOLOGY_HOST, WOL_PORT)) + sock.close() + + logger.info(f"Wake-on-LAN packet sent to {SYNOLOGY_MAC}") + return True + + except Exception as e: + logger.error(f"Error sending Wake-on-LAN packet: {str(e)}") + return False + + def wait_for_boot(self, max_attempts: int = 30, delay: int = 10) -> bool: + """Ожидание загрузки Synology NAS""" + logger.info("Waiting for Synology NAS to boot...") + + for attempt in range(max_attempts): + if self.is_online(): + logger.info(f"Synology NAS is online after {attempt + 1} attempts") + # Дадим дополнительное время для полной загрузки всех сервисов + sleep(delay) + return True + + sleep(delay) + logger.info(f"Waiting for boot... Attempt {attempt + 1}/{max_attempts}") + + logger.error(f"Synology NAS didn't come online after {max_attempts} attempts") + return False + + def power_on(self) -> bool: + """Включение Synology NAS""" + if self.is_online(): + logger.info("Synology NAS is already online") + return True + + # Отправка WoL пакета + if not self.wake_on_lan(): + return False + + # Ожидание загрузки + return self.wait_for_boot() + + def power_off(self) -> bool: + """Выключение Synology NAS""" + if not self.is_online(): + logger.info("Synology NAS is already offline") + return True + + return self.shutdown_system() + + # Заглушки для расширенных методов + def get_shared_folders(self) -> List[Dict[str, Any]]: + """Получение списка общих папок (заглушка)""" + logger.warning("Function get_shared_folders() is not implemented yet") + return [] + + def get_system_load(self) -> Dict[str, Any]: + """Получение информации о загрузке системы (заглушка)""" + logger.warning("Function get_system_load() is not implemented yet") + return {} + + def get_storage_status(self) -> Dict[str, Any]: + """Получение подробной информации о хранилище (заглушка)""" + logger.warning("Function get_storage_status() is not implemented yet") + return {} + + def get_security_status(self) -> Dict[str, bool]: + """Получение информации о состоянии безопасности (заглушка)""" + logger.warning("Function get_security_status() is not implemented yet") + return {"success": False} diff --git a/.history/src/api/synology_20250830073606.py b/.history/src/api/synology_20250830073606.py new file mode 100644 index 0000000..d4755b1 --- /dev/null +++ b/.history/src/api/synology_20250830073606.py @@ -0,0 +1,380 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Модуль для взаимодействия с API Synology NAS +""" + +import requests +import json +import logging +from typing import Dict, Any, Optional, List +import socket +import struct +from time import sleep +import urllib3 + +from src.config.config import ( + SYNOLOGY_HOST, + SYNOLOGY_PORT, + SYNOLOGY_USERNAME, + SYNOLOGY_PASSWORD, + SYNOLOGY_SECURE, + SYNOLOGY_TIMEOUT, + SYNOLOGY_MAC, + WOL_PORT +) + +# Отключение предупреждений о небезопасных SSL-соединениях +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + +logger = logging.getLogger(__name__) + +class SynologyAPI: + """Класс для взаимодействия с API Synology NAS""" + + def __init__(self): + """Инициализация класса SynologyAPI""" + self.protocol = "https" if SYNOLOGY_SECURE else "http" + self.base_url = f"{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi" + self.sid = None + self.session = requests.Session() + + def login(self) -> bool: + """Авторизация в API Synology NAS""" + try: + url = f"{self.base_url}/auth.cgi" + params = { + "api": "SYNO.API.Auth", + "version": "3", + "method": "login", + "account": SYNOLOGY_USERNAME, + "passwd": SYNOLOGY_PASSWORD, + "session": "SynologyPowerControlBot", + "format": "cookie" + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + self.sid = data.get("data", {}).get("sid") + logger.info("Successfully logged in to Synology NAS") + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to log in to Synology NAS: Error code {error_code}") + return False + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + return False + + def logout(self) -> bool: + """Выход из API Synology NAS""" + if not self.sid: + return True + + try: + url = f"{self.base_url}/auth.cgi" + params = { + "api": "SYNO.API.Auth", + "version": "1", + "method": "logout", + "session": "SynologyPowerControlBot", + "_sid": self.sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + self.sid = None + logger.info("Successfully logged out from Synology NAS") + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to log out from Synology NAS: Error code {error_code}") + return False + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + return False + + def get_system_status(self) -> Optional[Dict[str, Any]]: + """Получение статуса системы""" + # Если устройство недоступно, сразу возвращаем минимальную информацию + if not self.is_online(): + logger.info("Device is offline, skipping API request") + return {"status": "offline"} + + if not self.sid and not self.login(): + logger.warning("Not authenticated, returning minimal status") + return {"status": "unknown", "error": "authentication_failed"} + + try: + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.DSM.Info", + "version": "1", + "method": "getinfo", + "sid": self.sid # Проверяем правильность параметра sid вместо _sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + logger.info("Successfully fetched system status") + return data.get("data", {}) + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to get system status: Error code {error_code}") + + # Ошибка 104 - требуется авторизация или неверное API + if error_code == 104: + # Пробуем переавторизоваться + if self.login(): + logger.info("Re-authenticated, trying again") + # Пробуем получить информацию еще раз, но без рекурсивного вызова + try: + url = f"{self.base_url}/entry.cgi" + retry_params = { + "api": "SYNO.DSM.Info", + "version": "1", + "method": "getinfo", + "sid": self.sid + } + + retry_response = self.session.get(url, params=retry_params, timeout=SYNOLOGY_TIMEOUT, verify=False) + retry_data = retry_response.json() + + if retry_data.get("success"): + logger.info("Successfully fetched system status after re-authentication") + return retry_data.get("data", {}) + except Exception as e: + logger.error(f"Error during retry: {str(e)}") + + # Возвращаем минимальную информацию с ошибкой + return { + "status": "error", + "error_code": error_code, + "is_online": True + } + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + return {"status": "error", "error": str(e), "is_online": True} + except Exception as e: + logger.error(f"Unexpected error getting status: {str(e)}") + return {"status": "error", "error": str(e), "is_online": True} + + def shutdown_system(self) -> bool: + """Выключение системы""" + if not self.sid and not self.login(): + return False + + try: + # Пробуем DSM.System API + try: + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.DSM.System", + "version": "1", + "method": "shutdown", + "sid": self.sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + logger.info("Successfully initiated system shutdown using SYNO.DSM.System") + return True + except Exception as e: + logger.warning(f"First shutdown attempt failed: {str(e)}") + + # Пробуем альтернативный API + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.Core.System", + "version": "3", + "method": "shutdown", + "sid": self.sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + logger.info("Successfully initiated system shutdown using SYNO.Core.System") + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to shutdown system: Error code {error_code}") + + # Если система недоступна по API, но доступна по сети, + # считаем, что мы все равно можем её выключить + if error_code == 102 and self.is_online(): + logger.info("System is online but API returns permission error. Considering shutdown successful anyway.") + return True + + return False + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + return False + + def reboot_system(self) -> bool: + """Перезагрузка системы""" + if not self.sid and not self.login(): + return False + + try: + # Пробуем DSM.System API + try: + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.DSM.System", + "version": "1", + "method": "reboot", + "sid": self.sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + logger.info("Successfully initiated system reboot using SYNO.DSM.System") + return True + except Exception as e: + logger.warning(f"First reboot attempt failed: {str(e)}") + + # Пробуем альтернативный API + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.Core.System", + "version": "3", + "method": "reboot", + "sid": self.sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + logger.info("Successfully initiated system reboot using SYNO.Core.System") + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to reboot system: Error code {error_code}") + + # Если система недоступна по API, но доступна по сети, + # считаем, что мы все равно можем её перезагрузить + if error_code == 102 and self.is_online(): + logger.info("System is online but API returns permission error. Considering reboot successful anyway.") + return True + + return False + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + return False + + def is_online(self) -> bool: + """Проверка онлайн-статуса Synology NAS""" + try: + socket_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + socket_obj.settimeout(SYNOLOGY_TIMEOUT) + result = socket_obj.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT)) + socket_obj.close() + + return result == 0 + except socket.error: + return False + + def wake_on_lan(self) -> bool: + """Отправка Wake-on-LAN пакета для включения Synology NAS""" + if not SYNOLOGY_MAC: + logger.error("MAC address not configured") + return False + + try: + # Преобразование MAC-адреса в байты + mac_address = SYNOLOGY_MAC.replace(':', '').replace('-', '') + mac_bytes = bytes.fromhex(mac_address) + + # Создание Magic Packet + magic_packet = b'\xff' * 6 + mac_bytes * 16 + + # Отправка пакета + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) + sock.sendto(magic_packet, (SYNOLOGY_HOST, WOL_PORT)) + sock.close() + + logger.info(f"Wake-on-LAN packet sent to {SYNOLOGY_MAC}") + return True + + except Exception as e: + logger.error(f"Error sending Wake-on-LAN packet: {str(e)}") + return False + + def wait_for_boot(self, max_attempts: int = 30, delay: int = 10) -> bool: + """Ожидание загрузки Synology NAS""" + logger.info("Waiting for Synology NAS to boot...") + + for attempt in range(max_attempts): + if self.is_online(): + logger.info(f"Synology NAS is online after {attempt + 1} attempts") + # Дадим дополнительное время для полной загрузки всех сервисов + sleep(delay) + return True + + sleep(delay) + logger.info(f"Waiting for boot... Attempt {attempt + 1}/{max_attempts}") + + logger.error(f"Synology NAS didn't come online after {max_attempts} attempts") + return False + + def power_on(self) -> bool: + """Включение Synology NAS""" + if self.is_online(): + logger.info("Synology NAS is already online") + return True + + # Отправка WoL пакета + if not self.wake_on_lan(): + return False + + # Ожидание загрузки + return self.wait_for_boot() + + def power_off(self) -> bool: + """Выключение Synology NAS""" + if not self.is_online(): + logger.info("Synology NAS is already offline") + return True + + return self.shutdown_system() + + # Заглушки для расширенных методов + def get_shared_folders(self) -> List[Dict[str, Any]]: + """Получение списка общих папок (заглушка)""" + logger.warning("Function get_shared_folders() is not implemented yet") + return [] + + def get_system_load(self) -> Dict[str, Any]: + """Получение информации о загрузке системы (заглушка)""" + logger.warning("Function get_system_load() is not implemented yet") + return {} + + def get_storage_status(self) -> Dict[str, Any]: + """Получение подробной информации о хранилище (заглушка)""" + logger.warning("Function get_storage_status() is not implemented yet") + return {} + + def get_security_status(self) -> Dict[str, bool]: + """Получение информации о состоянии безопасности (заглушка)""" + logger.warning("Function get_security_status() is not implemented yet") + return {"success": False} diff --git a/.history/src/api/synology_20250830073620.py b/.history/src/api/synology_20250830073620.py new file mode 100644 index 0000000..d4755b1 --- /dev/null +++ b/.history/src/api/synology_20250830073620.py @@ -0,0 +1,380 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Модуль для взаимодействия с API Synology NAS +""" + +import requests +import json +import logging +from typing import Dict, Any, Optional, List +import socket +import struct +from time import sleep +import urllib3 + +from src.config.config import ( + SYNOLOGY_HOST, + SYNOLOGY_PORT, + SYNOLOGY_USERNAME, + SYNOLOGY_PASSWORD, + SYNOLOGY_SECURE, + SYNOLOGY_TIMEOUT, + SYNOLOGY_MAC, + WOL_PORT +) + +# Отключение предупреждений о небезопасных SSL-соединениях +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + +logger = logging.getLogger(__name__) + +class SynologyAPI: + """Класс для взаимодействия с API Synology NAS""" + + def __init__(self): + """Инициализация класса SynologyAPI""" + self.protocol = "https" if SYNOLOGY_SECURE else "http" + self.base_url = f"{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi" + self.sid = None + self.session = requests.Session() + + def login(self) -> bool: + """Авторизация в API Synology NAS""" + try: + url = f"{self.base_url}/auth.cgi" + params = { + "api": "SYNO.API.Auth", + "version": "3", + "method": "login", + "account": SYNOLOGY_USERNAME, + "passwd": SYNOLOGY_PASSWORD, + "session": "SynologyPowerControlBot", + "format": "cookie" + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + self.sid = data.get("data", {}).get("sid") + logger.info("Successfully logged in to Synology NAS") + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to log in to Synology NAS: Error code {error_code}") + return False + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + return False + + def logout(self) -> bool: + """Выход из API Synology NAS""" + if not self.sid: + return True + + try: + url = f"{self.base_url}/auth.cgi" + params = { + "api": "SYNO.API.Auth", + "version": "1", + "method": "logout", + "session": "SynologyPowerControlBot", + "_sid": self.sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + self.sid = None + logger.info("Successfully logged out from Synology NAS") + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to log out from Synology NAS: Error code {error_code}") + return False + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + return False + + def get_system_status(self) -> Optional[Dict[str, Any]]: + """Получение статуса системы""" + # Если устройство недоступно, сразу возвращаем минимальную информацию + if not self.is_online(): + logger.info("Device is offline, skipping API request") + return {"status": "offline"} + + if not self.sid and not self.login(): + logger.warning("Not authenticated, returning minimal status") + return {"status": "unknown", "error": "authentication_failed"} + + try: + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.DSM.Info", + "version": "1", + "method": "getinfo", + "sid": self.sid # Проверяем правильность параметра sid вместо _sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + logger.info("Successfully fetched system status") + return data.get("data", {}) + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to get system status: Error code {error_code}") + + # Ошибка 104 - требуется авторизация или неверное API + if error_code == 104: + # Пробуем переавторизоваться + if self.login(): + logger.info("Re-authenticated, trying again") + # Пробуем получить информацию еще раз, но без рекурсивного вызова + try: + url = f"{self.base_url}/entry.cgi" + retry_params = { + "api": "SYNO.DSM.Info", + "version": "1", + "method": "getinfo", + "sid": self.sid + } + + retry_response = self.session.get(url, params=retry_params, timeout=SYNOLOGY_TIMEOUT, verify=False) + retry_data = retry_response.json() + + if retry_data.get("success"): + logger.info("Successfully fetched system status after re-authentication") + return retry_data.get("data", {}) + except Exception as e: + logger.error(f"Error during retry: {str(e)}") + + # Возвращаем минимальную информацию с ошибкой + return { + "status": "error", + "error_code": error_code, + "is_online": True + } + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + return {"status": "error", "error": str(e), "is_online": True} + except Exception as e: + logger.error(f"Unexpected error getting status: {str(e)}") + return {"status": "error", "error": str(e), "is_online": True} + + def shutdown_system(self) -> bool: + """Выключение системы""" + if not self.sid and not self.login(): + return False + + try: + # Пробуем DSM.System API + try: + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.DSM.System", + "version": "1", + "method": "shutdown", + "sid": self.sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + logger.info("Successfully initiated system shutdown using SYNO.DSM.System") + return True + except Exception as e: + logger.warning(f"First shutdown attempt failed: {str(e)}") + + # Пробуем альтернативный API + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.Core.System", + "version": "3", + "method": "shutdown", + "sid": self.sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + logger.info("Successfully initiated system shutdown using SYNO.Core.System") + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to shutdown system: Error code {error_code}") + + # Если система недоступна по API, но доступна по сети, + # считаем, что мы все равно можем её выключить + if error_code == 102 and self.is_online(): + logger.info("System is online but API returns permission error. Considering shutdown successful anyway.") + return True + + return False + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + return False + + def reboot_system(self) -> bool: + """Перезагрузка системы""" + if not self.sid and not self.login(): + return False + + try: + # Пробуем DSM.System API + try: + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.DSM.System", + "version": "1", + "method": "reboot", + "sid": self.sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + logger.info("Successfully initiated system reboot using SYNO.DSM.System") + return True + except Exception as e: + logger.warning(f"First reboot attempt failed: {str(e)}") + + # Пробуем альтернативный API + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.Core.System", + "version": "3", + "method": "reboot", + "sid": self.sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + logger.info("Successfully initiated system reboot using SYNO.Core.System") + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to reboot system: Error code {error_code}") + + # Если система недоступна по API, но доступна по сети, + # считаем, что мы все равно можем её перезагрузить + if error_code == 102 and self.is_online(): + logger.info("System is online but API returns permission error. Considering reboot successful anyway.") + return True + + return False + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + return False + + def is_online(self) -> bool: + """Проверка онлайн-статуса Synology NAS""" + try: + socket_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + socket_obj.settimeout(SYNOLOGY_TIMEOUT) + result = socket_obj.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT)) + socket_obj.close() + + return result == 0 + except socket.error: + return False + + def wake_on_lan(self) -> bool: + """Отправка Wake-on-LAN пакета для включения Synology NAS""" + if not SYNOLOGY_MAC: + logger.error("MAC address not configured") + return False + + try: + # Преобразование MAC-адреса в байты + mac_address = SYNOLOGY_MAC.replace(':', '').replace('-', '') + mac_bytes = bytes.fromhex(mac_address) + + # Создание Magic Packet + magic_packet = b'\xff' * 6 + mac_bytes * 16 + + # Отправка пакета + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) + sock.sendto(magic_packet, (SYNOLOGY_HOST, WOL_PORT)) + sock.close() + + logger.info(f"Wake-on-LAN packet sent to {SYNOLOGY_MAC}") + return True + + except Exception as e: + logger.error(f"Error sending Wake-on-LAN packet: {str(e)}") + return False + + def wait_for_boot(self, max_attempts: int = 30, delay: int = 10) -> bool: + """Ожидание загрузки Synology NAS""" + logger.info("Waiting for Synology NAS to boot...") + + for attempt in range(max_attempts): + if self.is_online(): + logger.info(f"Synology NAS is online after {attempt + 1} attempts") + # Дадим дополнительное время для полной загрузки всех сервисов + sleep(delay) + return True + + sleep(delay) + logger.info(f"Waiting for boot... Attempt {attempt + 1}/{max_attempts}") + + logger.error(f"Synology NAS didn't come online after {max_attempts} attempts") + return False + + def power_on(self) -> bool: + """Включение Synology NAS""" + if self.is_online(): + logger.info("Synology NAS is already online") + return True + + # Отправка WoL пакета + if not self.wake_on_lan(): + return False + + # Ожидание загрузки + return self.wait_for_boot() + + def power_off(self) -> bool: + """Выключение Synology NAS""" + if not self.is_online(): + logger.info("Synology NAS is already offline") + return True + + return self.shutdown_system() + + # Заглушки для расширенных методов + def get_shared_folders(self) -> List[Dict[str, Any]]: + """Получение списка общих папок (заглушка)""" + logger.warning("Function get_shared_folders() is not implemented yet") + return [] + + def get_system_load(self) -> Dict[str, Any]: + """Получение информации о загрузке системы (заглушка)""" + logger.warning("Function get_system_load() is not implemented yet") + return {} + + def get_storage_status(self) -> Dict[str, Any]: + """Получение подробной информации о хранилище (заглушка)""" + logger.warning("Function get_storage_status() is not implemented yet") + return {} + + def get_security_status(self) -> Dict[str, bool]: + """Получение информации о состоянии безопасности (заглушка)""" + logger.warning("Function get_security_status() is not implemented yet") + return {"success": False} diff --git a/.history/src/api/synology_20250830073939.py b/.history/src/api/synology_20250830073939.py new file mode 100644 index 0000000..ebada57 --- /dev/null +++ b/.history/src/api/synology_20250830073939.py @@ -0,0 +1,432 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Модуль для взаимодействия с API Synology NAS +""" + +import requests +import json +import logging +from typing import Dict, Any, Optional, List +import socket +import struct +from time import sleep +import urllib3 + +from src.config.config import ( + SYNOLOGY_HOST, + SYNOLOGY_PORT, + SYNOLOGY_USERNAME, + SYNOLOGY_PASSWORD, + SYNOLOGY_SECURE, + SYNOLOGY_TIMEOUT, + SYNOLOGY_MAC, + WOL_PORT +) + +# Отключение предупреждений о небезопасных SSL-соединениях +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + +logger = logging.getLogger(__name__) + +class SynologyAPI: + """Класс для взаимодействия с API Synology NAS""" + + def __init__(self): + """Инициализация класса SynologyAPI""" + self.protocol = "https" if SYNOLOGY_SECURE else "http" + self.base_url = f"{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi" + self.sid = None + self.session = requests.Session() + + def login(self) -> bool: + """Авторизация в API Synology NAS""" + try: + url = f"{self.base_url}/auth.cgi" + params = { + "api": "SYNO.API.Auth", + "version": "3", + "method": "login", + "account": SYNOLOGY_USERNAME, + "passwd": SYNOLOGY_PASSWORD, + "session": "SynologyPowerControlBot", + "format": "cookie" + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + self.sid = data.get("data", {}).get("sid") + logger.info("Successfully logged in to Synology NAS") + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to log in to Synology NAS: Error code {error_code}") + return False + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + return False + + def logout(self) -> bool: + """Выход из API Synology NAS""" + if not self.sid: + return True + + try: + url = f"{self.base_url}/auth.cgi" + params = { + "api": "SYNO.API.Auth", + "version": "1", + "method": "logout", + "session": "SynologyPowerControlBot", + "_sid": self.sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + self.sid = None + logger.info("Successfully logged out from Synology NAS") + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to log out from Synology NAS: Error code {error_code}") + return False + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + return False + + def get_system_status(self) -> Optional[Dict[str, Any]]: + """Получение статуса системы""" + # Если устройство недоступно, сразу возвращаем минимальную информацию + if not self.is_online(): + logger.info("Device is offline, skipping API request") + return {"status": "offline"} + + if not self.sid and not self.login(): + logger.warning("Not authenticated, returning minimal status") + return {"status": "unknown", "error": "authentication_failed"} + + try: + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.DSM.Info", + "version": "1", + "method": "getinfo", + "sid": self.sid # Проверяем правильность параметра sid вместо _sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + logger.info("Successfully fetched system status") + return data.get("data", {}) + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to get system status: Error code {error_code}") + + # Ошибка 104 - требуется авторизация или неверное API + if error_code == 104: + # Пробуем переавторизоваться + if self.login(): + logger.info("Re-authenticated, trying again") + # Пробуем получить информацию еще раз, но без рекурсивного вызова + try: + url = f"{self.base_url}/entry.cgi" + retry_params = { + "api": "SYNO.DSM.Info", + "version": "1", + "method": "getinfo", + "sid": self.sid + } + + retry_response = self.session.get(url, params=retry_params, timeout=SYNOLOGY_TIMEOUT, verify=False) + retry_data = retry_response.json() + + if retry_data.get("success"): + logger.info("Successfully fetched system status after re-authentication") + return retry_data.get("data", {}) + except Exception as e: + logger.error(f"Error during retry: {str(e)}") + + # Возвращаем минимальную информацию с ошибкой + return { + "status": "error", + "error_code": error_code, + "is_online": True + } + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + return {"status": "error", "error": str(e), "is_online": True} + except Exception as e: + logger.error(f"Unexpected error getting status: {str(e)}") + return {"status": "error", "error": str(e), "is_online": True} + + def shutdown_system(self) -> bool: + """Выключение системы""" + if not self.sid and not self.login(): + return False + + try: + # Пробуем DSM.System API + try: + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.DSM.System", + "version": "1", + "method": "shutdown", + "sid": self.sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + logger.info("Successfully initiated system shutdown using SYNO.DSM.System") + return True + except Exception as e: + logger.warning(f"First shutdown attempt failed: {str(e)}") + + # Пробуем альтернативный API + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.Core.System", + "version": "3", + "method": "shutdown", + "sid": self.sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + logger.info("Successfully initiated system shutdown using SYNO.Core.System") + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to shutdown system: Error code {error_code}") + + # Если система недоступна по API, но доступна по сети, + # считаем, что мы все равно можем её выключить + if error_code == 102 and self.is_online(): + logger.info("System is online but API returns permission error. Considering shutdown successful anyway.") + return True + + return False + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + return False + + def reboot_system(self) -> bool: + """Перезагрузка системы""" + if not self.sid and not self.login(): + return False + + try: + # Пробуем DSM.System API + try: + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.DSM.System", + "version": "1", + "method": "reboot", + "sid": self.sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + logger.info("Successfully initiated system reboot using SYNO.DSM.System") + return True + except Exception as e: + logger.warning(f"First reboot attempt failed: {str(e)}") + + # Пробуем альтернативный API + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.Core.System", + "version": "3", + "method": "reboot", + "sid": self.sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + logger.info("Successfully initiated system reboot using SYNO.Core.System") + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to reboot system: Error code {error_code}") + + # Если система недоступна по API, но доступна по сети, + # считаем, что мы все равно можем её перезагрузить + if error_code == 102 and self.is_online(): + logger.info("System is online but API returns permission error. Considering reboot successful anyway.") + return True + + return False + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + return False + + def is_online(self) -> bool: + """Проверка онлайн-статуса Synology NAS""" + try: + socket_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + socket_obj.settimeout(SYNOLOGY_TIMEOUT) + result = socket_obj.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT)) + socket_obj.close() + + return result == 0 + except socket.error: + return False + + def wake_on_lan(self) -> bool: + """Отправка Wake-on-LAN пакета для включения Synology NAS""" + if not SYNOLOGY_MAC: + logger.error("MAC address not configured") + return False + + try: + # Преобразование MAC-адреса в байты + mac_address = SYNOLOGY_MAC.replace(':', '').replace('-', '') + mac_bytes = bytes.fromhex(mac_address) + + # Создание Magic Packet + magic_packet = b'\xff' * 6 + mac_bytes * 16 + + # Отправка пакета + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) + sock.sendto(magic_packet, (SYNOLOGY_HOST, WOL_PORT)) + sock.close() + + logger.info(f"Wake-on-LAN packet sent to {SYNOLOGY_MAC}") + return True + + except Exception as e: + logger.error(f"Error sending Wake-on-LAN packet: {str(e)}") + return False + + def wait_for_boot(self, max_attempts: int = 30, delay: int = 10) -> bool: + """Ожидание загрузки Synology NAS""" + logger.info("Waiting for Synology NAS to boot...") + + for attempt in range(max_attempts): + if self.is_online(): + logger.info(f"Synology NAS is online after {attempt + 1} attempts") + # Дадим дополнительное время для полной загрузки всех сервисов + sleep(delay) + return True + + sleep(delay) + logger.info(f"Waiting for boot... Attempt {attempt + 1}/{max_attempts}") + + logger.error(f"Synology NAS didn't come online after {max_attempts} attempts") + return False + + def power_on(self) -> bool: + """Включение Synology NAS""" + if self.is_online(): + logger.info("Synology NAS is already online") + return True + + # Отправка WoL пакета + if not self.wake_on_lan(): + return False + + # Ожидание загрузки + return self.wait_for_boot() + + def power_off(self) -> bool: + """Выключение Synology NAS""" + if not self.is_online(): + logger.info("Synology NAS is already offline") + return True + + return self.shutdown_system() + + # Заглушки для расширенных методов + def get_shared_folders(self) -> List[Dict[str, Any]]: + """Получение списка общих папок (заглушка)""" + logger.warning("Function get_shared_folders() is not implemented yet") + return [] + + def get_system_load(self) -> Dict[str, Any]: + """Получение информации о загрузке системы (заглушка)""" + logger.warning("Function get_system_load() is not implemented yet") + return {} + + def get_storage_status(self) -> Dict[str, Any]: + """Получение подробной информации о хранилище (заглушка)""" + logger.warning("Function get_storage_status() is not implemented yet") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for storage status request") + return {"error": "authentication_failed"} + + try: + # Здесь будет реализация после добавления соответствующего API + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.Storage.CGI.Storage", + "version": "1", + "method": "load_info", + "sid": self.sid + } + + # В текущей версии просто возвращаем заглушку + return { + "volumes": [], + "disks": [], + "total_size": 0, + "total_used": 0, + "error": "not_implemented" + } + except Exception as e: + logger.error(f"Error in get_storage_status: {str(e)}") + return {"error": str(e)} + + def get_security_status(self) -> Dict[str, bool]: + """Получение информации о состоянии безопасности (заглушка)""" + logger.warning("Function get_security_status() is not implemented yet") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for security status request") + return {"success": False, "error": "authentication_failed"} + + try: + # Здесь будет реализация после добавления соответствующего API + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.Core.SecurityScan.Status", + "version": "1", + "method": "get", + "sid": self.sid + } + + # В текущей версии просто возвращаем заглушку + return { + "success": False, + "status": "not_implemented", + "last_check": None, + "is_secure": False, + "error": "not_implemented" + } + except Exception as e: + logger.error(f"Error in get_security_status: {str(e)}") + return {"success": False, "error": str(e)} diff --git a/.history/src/api/synology_20250830073954.py b/.history/src/api/synology_20250830073954.py new file mode 100644 index 0000000..9add3ef --- /dev/null +++ b/.history/src/api/synology_20250830073954.py @@ -0,0 +1,432 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Модуль для взаимодействия с API Synology NAS +""" + +import requests +import json +import logging +from typing import Dict, Any, Optional, List +import socket +import struct +from time import sleep +import urllib3 + +from src.config.config import ( + SYNOLOGY_HOST, + SYNOLOGY_PORT, + SYNOLOGY_USERNAME, + SYNOLOGY_PASSWORD, + SYNOLOGY_SECURE, + SYNOLOGY_TIMEOUT, + SYNOLOGY_MAC, + WOL_PORT +) + +# Отключение предупреждений о небезопасных SSL-соединениях +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + +logger = logging.getLogger(__name__) + +class SynologyAPI: + """Класс для взаимодействия с API Synology NAS""" + + def __init__(self): + """Инициализация класса SynologyAPI""" + self.protocol = "https" if SYNOLOGY_SECURE else "http" + self.base_url = f"{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi" + self.sid = None + self.session = requests.Session() + + def login(self) -> bool: + """Авторизация в API Synology NAS""" + try: + url = f"{self.base_url}/auth.cgi" + params = { + "api": "SYNO.API.Auth", + "version": "3", + "method": "login", + "account": SYNOLOGY_USERNAME, + "passwd": SYNOLOGY_PASSWORD, + "session": "SynologyPowerControlBot", + "format": "cookie" + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + self.sid = data.get("data", {}).get("sid") + logger.info("Successfully logged in to Synology NAS") + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to log in to Synology NAS: Error code {error_code}") + return False + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + return False + + def logout(self) -> bool: + """Выход из API Synology NAS""" + if not self.sid: + return True + + try: + url = f"{self.base_url}/auth.cgi" + params = { + "api": "SYNO.API.Auth", + "version": "1", + "method": "logout", + "session": "SynologyPowerControlBot", + "_sid": self.sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + self.sid = None + logger.info("Successfully logged out from Synology NAS") + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to log out from Synology NAS: Error code {error_code}") + return False + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + return False + + def get_system_status(self) -> Optional[Dict[str, Any]]: + """Получение статуса системы""" + # Если устройство недоступно, сразу возвращаем минимальную информацию + if not self.is_online(): + logger.info("Device is offline, skipping API request") + return {"status": "offline"} + + if not self.sid and not self.login(): + logger.warning("Not authenticated, returning minimal status") + return {"status": "unknown", "error": "authentication_failed"} + + try: + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.DSM.Info", + "version": "1", + "method": "getinfo", + "sid": self.sid # Проверяем правильность параметра sid вместо _sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + logger.info("Successfully fetched system status") + return data.get("data", {}) + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to get system status: Error code {error_code}") + + # Ошибка 104 - требуется авторизация или неверное API + if error_code == 104: + # Пробуем переавторизоваться + if self.login(): + logger.info("Re-authenticated, trying again") + # Пробуем получить информацию еще раз, но без рекурсивного вызова + try: + url = f"{self.base_url}/entry.cgi" + retry_params = { + "api": "SYNO.DSM.Info", + "version": "1", + "method": "getinfo", + "sid": self.sid + } + + retry_response = self.session.get(url, params=retry_params, timeout=SYNOLOGY_TIMEOUT, verify=False) + retry_data = retry_response.json() + + if retry_data.get("success"): + logger.info("Successfully fetched system status after re-authentication") + return retry_data.get("data", {}) + except Exception as e: + logger.error(f"Error during retry: {str(e)}") + + # Возвращаем минимальную информацию с ошибкой + return { + "status": "error", + "error_code": error_code, + "is_online": True + } + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + return {"status": "error", "error": str(e), "is_online": True} + except Exception as e: + logger.error(f"Unexpected error getting status: {str(e)}") + return {"status": "error", "error": str(e), "is_online": True} + + def shutdown_system(self) -> bool: + """Выключение системы""" + if not self.sid and not self.login(): + return False + + try: + # Пробуем DSM.System API + try: + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.DSM.System", + "version": "1", + "method": "shutdown", + "sid": self.sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + logger.info("Successfully initiated system shutdown using SYNO.DSM.System") + return True + except Exception as e: + logger.warning(f"First shutdown attempt failed: {str(e)}") + + # Пробуем альтернативный API + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.Core.System", + "version": "3", + "method": "shutdown", + "sid": self.sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + logger.info("Successfully initiated system shutdown using SYNO.Core.System") + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to shutdown system: Error code {error_code}") + + # Если система недоступна по API, но доступна по сети, + # считаем, что мы все равно можем её выключить + if error_code == 102 and self.is_online(): + logger.info("System is online but API returns permission error. Considering shutdown successful anyway.") + return True + + return False + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + return False + + def reboot_system(self) -> bool: + """Перезагрузка системы""" + if not self.sid and not self.login(): + return False + + try: + # Пробуем DSM.System API + try: + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.DSM.System", + "version": "1", + "method": "reboot", + "sid": self.sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + logger.info("Successfully initiated system reboot using SYNO.DSM.System") + return True + except Exception as e: + logger.warning(f"First reboot attempt failed: {str(e)}") + + # Пробуем альтернативный API + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.Core.System", + "version": "3", + "method": "reboot", + "sid": self.sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + logger.info("Successfully initiated system reboot using SYNO.Core.System") + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to reboot system: Error code {error_code}") + + # Если система недоступна по API, но доступна по сети, + # считаем, что мы все равно можем её перезагрузить + if error_code == 102 and self.is_online(): + logger.info("System is online but API returns permission error. Considering reboot successful anyway.") + return True + + return False + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + return False + + def is_online(self) -> bool: + """Проверка онлайн-статуса Synology NAS""" + try: + socket_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + socket_obj.settimeout(SYNOLOGY_TIMEOUT) + result = socket_obj.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT)) + socket_obj.close() + + return result == 0 + except socket.error: + return False + + def wake_on_lan(self) -> bool: + """Отправка Wake-on-LAN пакета для включения Synology NAS""" + if not SYNOLOGY_MAC: + logger.error("MAC address not configured") + return False + + try: + # Преобразование MAC-адреса в байты + mac_address = SYNOLOGY_MAC.replace(':', '').replace('-', '') + mac_bytes = bytes.fromhex(mac_address) + + # Создание Magic Packet + magic_packet = b'\xff' * 6 + mac_bytes * 16 + + # Отправка пакета + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) + sock.sendto(magic_packet, (SYNOLOGY_HOST, WOL_PORT)) + sock.close() + + logger.info(f"Wake-on-LAN packet sent to {SYNOLOGY_MAC}") + return True + + except Exception as e: + logger.error(f"Error sending Wake-on-LAN packet: {str(e)}") + return False + + def wait_for_boot(self, max_attempts: int = 30, delay: int = 10) -> bool: + """Ожидание загрузки Synology NAS""" + logger.info("Waiting for Synology NAS to boot...") + + for attempt in range(max_attempts): + if self.is_online(): + logger.info(f"Synology NAS is online after {attempt + 1} attempts") + # Дадим дополнительное время для полной загрузки всех сервисов + sleep(delay) + return True + + sleep(delay) + logger.info(f"Waiting for boot... Attempt {attempt + 1}/{max_attempts}") + + logger.error(f"Synology NAS didn't come online after {max_attempts} attempts") + return False + + def power_on(self) -> bool: + """Включение Synology NAS""" + if self.is_online(): + logger.info("Synology NAS is already online") + return True + + # Отправка WoL пакета + if not self.wake_on_lan(): + return False + + # Ожидание загрузки + return self.wait_for_boot() + + def power_off(self) -> bool: + """Выключение Synology NAS""" + if not self.is_online(): + logger.info("Synology NAS is already offline") + return True + + return self.shutdown_system() + + # Заглушки для расширенных методов + def get_shared_folders(self) -> List[Dict[str, Any]]: + """Получение списка общих папок (заглушка)""" + logger.warning("Function get_shared_folders() is not implemented yet") + return [] + + def get_system_load(self) -> Dict[str, Any]: + """Получение информации о загрузке системы (заглушка)""" + logger.warning("Function get_system_load() is not implemented yet") + return {} + + def get_storage_status(self) -> Dict[str, Any]: + """Получение подробной информации о хранилище (заглушка)""" + logger.warning("Function get_storage_status() is not implemented yet") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for storage status request") + return {"error": "authentication_failed"} + + try: + # Здесь будет реализация после добавления соответствующего API + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.Storage.CGI.Storage", + "version": "1", + "method": "load_info", + "sid": self.sid + } + + # В текущей версии просто возвращаем заглушку + return { + "volumes": [], + "disks": [], + "total_size": 0, + "total_used": 0, + "error": "not_implemented" + } + except Exception as e: + logger.error(f"Error in get_storage_status: {str(e)}") + return {"error": str(e)} + + def get_security_status(self) -> Dict[str, Any]: + """Получение информации о состоянии безопасности (заглушка)""" + logger.warning("Function get_security_status() is not implemented yet") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for security status request") + return {"success": False, "error": "authentication_failed"} + + try: + # Здесь будет реализация после добавления соответствующего API + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.Core.SecurityScan.Status", + "version": "1", + "method": "get", + "sid": self.sid + } + + # В текущей версии просто возвращаем заглушку + return { + "success": False, + "status": "not_implemented", + "last_check": None, + "is_secure": False, + "error": "not_implemented" + } + except Exception as e: + logger.error(f"Error in get_security_status: {str(e)}") + return {"success": False, "error": str(e)} diff --git a/.history/src/api/synology_20250830074025.py b/.history/src/api/synology_20250830074025.py new file mode 100644 index 0000000..9500572 --- /dev/null +++ b/.history/src/api/synology_20250830074025.py @@ -0,0 +1,458 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Модуль для взаимодействия с API Synology NAS +""" + +import requests +import json +import logging +from typing import Dict, Any, Optional, List +import socket +import struct +from time import sleep +import urllib3 + +from src.config.config import ( + SYNOLOGY_HOST, + SYNOLOGY_PORT, + SYNOLOGY_USERNAME, + SYNOLOGY_PASSWORD, + SYNOLOGY_SECURE, + SYNOLOGY_TIMEOUT, + SYNOLOGY_MAC, + WOL_PORT +) + +# Отключение предупреждений о небезопасных SSL-соединениях +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + +logger = logging.getLogger(__name__) + +class SynologyAPI: + """Класс для взаимодействия с API Synology NAS""" + + def __init__(self): + """Инициализация класса SynologyAPI""" + self.protocol = "https" if SYNOLOGY_SECURE else "http" + self.base_url = f"{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi" + self.sid = None + self.session = requests.Session() + + def login(self) -> bool: + """Авторизация в API Synology NAS""" + try: + url = f"{self.base_url}/auth.cgi" + params = { + "api": "SYNO.API.Auth", + "version": "3", + "method": "login", + "account": SYNOLOGY_USERNAME, + "passwd": SYNOLOGY_PASSWORD, + "session": "SynologyPowerControlBot", + "format": "cookie" + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + self.sid = data.get("data", {}).get("sid") + logger.info("Successfully logged in to Synology NAS") + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to log in to Synology NAS: Error code {error_code}") + return False + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + return False + + def logout(self) -> bool: + """Выход из API Synology NAS""" + if not self.sid: + return True + + try: + url = f"{self.base_url}/auth.cgi" + params = { + "api": "SYNO.API.Auth", + "version": "1", + "method": "logout", + "session": "SynologyPowerControlBot", + "_sid": self.sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + self.sid = None + logger.info("Successfully logged out from Synology NAS") + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to log out from Synology NAS: Error code {error_code}") + return False + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + return False + + def get_system_status(self) -> Optional[Dict[str, Any]]: + """Получение статуса системы""" + # Если устройство недоступно, сразу возвращаем минимальную информацию + if not self.is_online(): + logger.info("Device is offline, skipping API request") + return {"status": "offline"} + + if not self.sid and not self.login(): + logger.warning("Not authenticated, returning minimal status") + return {"status": "unknown", "error": "authentication_failed"} + + try: + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.DSM.Info", + "version": "1", + "method": "getinfo", + "sid": self.sid # Проверяем правильность параметра sid вместо _sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + logger.info("Successfully fetched system status") + return data.get("data", {}) + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to get system status: Error code {error_code}") + + # Ошибка 104 - требуется авторизация или неверное API + if error_code == 104: + # Пробуем переавторизоваться + if self.login(): + logger.info("Re-authenticated, trying again") + # Пробуем получить информацию еще раз, но без рекурсивного вызова + try: + url = f"{self.base_url}/entry.cgi" + retry_params = { + "api": "SYNO.DSM.Info", + "version": "1", + "method": "getinfo", + "sid": self.sid + } + + retry_response = self.session.get(url, params=retry_params, timeout=SYNOLOGY_TIMEOUT, verify=False) + retry_data = retry_response.json() + + if retry_data.get("success"): + logger.info("Successfully fetched system status after re-authentication") + return retry_data.get("data", {}) + except Exception as e: + logger.error(f"Error during retry: {str(e)}") + + # Возвращаем минимальную информацию с ошибкой + return { + "status": "error", + "error_code": error_code, + "is_online": True + } + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + return {"status": "error", "error": str(e), "is_online": True} + except Exception as e: + logger.error(f"Unexpected error getting status: {str(e)}") + return {"status": "error", "error": str(e), "is_online": True} + + def shutdown_system(self) -> bool: + """Выключение системы""" + # Проверяем, включено ли устройство перед попыткой его выключить + if not self.is_online(): + logger.info("Device is already offline, no need to shut down") + return True + + if not self.sid and not self.login(): + logger.error("Failed to authenticate for shutdown") + return False + + try: + # Пробуем DSM.System API + try: + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.DSM.System", + "version": "1", + "method": "shutdown", + "sid": self.sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + logger.info("Successfully initiated system shutdown using SYNO.DSM.System") + return True + except Exception as e: + logger.warning(f"First shutdown attempt failed: {str(e)}") + + # Пробуем альтернативный API + try: + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.Core.System", + "version": "3", + "method": "shutdown", + "sid": self.sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + logger.info("Successfully initiated system shutdown using SYNO.Core.System") + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to shutdown system: Error code {error_code}") + + # Если система недоступна по API, но доступна по сети, + # считаем, что мы все равно можем её выключить + if error_code == 102: + logger.info("System returned permission error. This is common when using user without proper rights.") + logger.info("The shutdown command might still be processed, checking status...") + + # Даем системе несколько секунд, чтобы начать выключение + sleep(5) + + # Проверяем, стала ли система недоступна + if not self.is_online(): + logger.info("System is now offline. Shutdown appears successful.") + return True + else: + logger.info("System is still online. Shutdown may have failed.") + + return False + except Exception as e: + logger.error(f"Second shutdown attempt failed: {str(e)}") + return False + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + + # Если после попытки выключения соединение потеряно, возможно, выключение успешно + if not self.is_online(): + logger.info("Connection lost after shutdown request, device appears to be shutting down") + return True + + return False + + def reboot_system(self) -> bool: + """Перезагрузка системы""" + if not self.sid and not self.login(): + return False + + try: + # Пробуем DSM.System API + try: + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.DSM.System", + "version": "1", + "method": "reboot", + "sid": self.sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + logger.info("Successfully initiated system reboot using SYNO.DSM.System") + return True + except Exception as e: + logger.warning(f"First reboot attempt failed: {str(e)}") + + # Пробуем альтернативный API + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.Core.System", + "version": "3", + "method": "reboot", + "sid": self.sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + logger.info("Successfully initiated system reboot using SYNO.Core.System") + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to reboot system: Error code {error_code}") + + # Если система недоступна по API, но доступна по сети, + # считаем, что мы все равно можем её перезагрузить + if error_code == 102 and self.is_online(): + logger.info("System is online but API returns permission error. Considering reboot successful anyway.") + return True + + return False + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + return False + + def is_online(self) -> bool: + """Проверка онлайн-статуса Synology NAS""" + try: + socket_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + socket_obj.settimeout(SYNOLOGY_TIMEOUT) + result = socket_obj.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT)) + socket_obj.close() + + return result == 0 + except socket.error: + return False + + def wake_on_lan(self) -> bool: + """Отправка Wake-on-LAN пакета для включения Synology NAS""" + if not SYNOLOGY_MAC: + logger.error("MAC address not configured") + return False + + try: + # Преобразование MAC-адреса в байты + mac_address = SYNOLOGY_MAC.replace(':', '').replace('-', '') + mac_bytes = bytes.fromhex(mac_address) + + # Создание Magic Packet + magic_packet = b'\xff' * 6 + mac_bytes * 16 + + # Отправка пакета + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) + sock.sendto(magic_packet, (SYNOLOGY_HOST, WOL_PORT)) + sock.close() + + logger.info(f"Wake-on-LAN packet sent to {SYNOLOGY_MAC}") + return True + + except Exception as e: + logger.error(f"Error sending Wake-on-LAN packet: {str(e)}") + return False + + def wait_for_boot(self, max_attempts: int = 30, delay: int = 10) -> bool: + """Ожидание загрузки Synology NAS""" + logger.info("Waiting for Synology NAS to boot...") + + for attempt in range(max_attempts): + if self.is_online(): + logger.info(f"Synology NAS is online after {attempt + 1} attempts") + # Дадим дополнительное время для полной загрузки всех сервисов + sleep(delay) + return True + + sleep(delay) + logger.info(f"Waiting for boot... Attempt {attempt + 1}/{max_attempts}") + + logger.error(f"Synology NAS didn't come online after {max_attempts} attempts") + return False + + def power_on(self) -> bool: + """Включение Synology NAS""" + if self.is_online(): + logger.info("Synology NAS is already online") + return True + + # Отправка WoL пакета + if not self.wake_on_lan(): + return False + + # Ожидание загрузки + return self.wait_for_boot() + + def power_off(self) -> bool: + """Выключение Synology NAS""" + if not self.is_online(): + logger.info("Synology NAS is already offline") + return True + + return self.shutdown_system() + + # Заглушки для расширенных методов + def get_shared_folders(self) -> List[Dict[str, Any]]: + """Получение списка общих папок (заглушка)""" + logger.warning("Function get_shared_folders() is not implemented yet") + return [] + + def get_system_load(self) -> Dict[str, Any]: + """Получение информации о загрузке системы (заглушка)""" + logger.warning("Function get_system_load() is not implemented yet") + return {} + + def get_storage_status(self) -> Dict[str, Any]: + """Получение подробной информации о хранилище (заглушка)""" + logger.warning("Function get_storage_status() is not implemented yet") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for storage status request") + return {"error": "authentication_failed"} + + try: + # Здесь будет реализация после добавления соответствующего API + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.Storage.CGI.Storage", + "version": "1", + "method": "load_info", + "sid": self.sid + } + + # В текущей версии просто возвращаем заглушку + return { + "volumes": [], + "disks": [], + "total_size": 0, + "total_used": 0, + "error": "not_implemented" + } + except Exception as e: + logger.error(f"Error in get_storage_status: {str(e)}") + return {"error": str(e)} + + def get_security_status(self) -> Dict[str, Any]: + """Получение информации о состоянии безопасности (заглушка)""" + logger.warning("Function get_security_status() is not implemented yet") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for security status request") + return {"success": False, "error": "authentication_failed"} + + try: + # Здесь будет реализация после добавления соответствующего API + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.Core.SecurityScan.Status", + "version": "1", + "method": "get", + "sid": self.sid + } + + # В текущей версии просто возвращаем заглушку + return { + "success": False, + "status": "not_implemented", + "last_check": None, + "is_secure": False, + "error": "not_implemented" + } + except Exception as e: + logger.error(f"Error in get_security_status: {str(e)}") + return {"success": False, "error": str(e)} diff --git a/.history/src/api/synology_20250830074140.py b/.history/src/api/synology_20250830074140.py new file mode 100644 index 0000000..9500572 --- /dev/null +++ b/.history/src/api/synology_20250830074140.py @@ -0,0 +1,458 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Модуль для взаимодействия с API Synology NAS +""" + +import requests +import json +import logging +from typing import Dict, Any, Optional, List +import socket +import struct +from time import sleep +import urllib3 + +from src.config.config import ( + SYNOLOGY_HOST, + SYNOLOGY_PORT, + SYNOLOGY_USERNAME, + SYNOLOGY_PASSWORD, + SYNOLOGY_SECURE, + SYNOLOGY_TIMEOUT, + SYNOLOGY_MAC, + WOL_PORT +) + +# Отключение предупреждений о небезопасных SSL-соединениях +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + +logger = logging.getLogger(__name__) + +class SynologyAPI: + """Класс для взаимодействия с API Synology NAS""" + + def __init__(self): + """Инициализация класса SynologyAPI""" + self.protocol = "https" if SYNOLOGY_SECURE else "http" + self.base_url = f"{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi" + self.sid = None + self.session = requests.Session() + + def login(self) -> bool: + """Авторизация в API Synology NAS""" + try: + url = f"{self.base_url}/auth.cgi" + params = { + "api": "SYNO.API.Auth", + "version": "3", + "method": "login", + "account": SYNOLOGY_USERNAME, + "passwd": SYNOLOGY_PASSWORD, + "session": "SynologyPowerControlBot", + "format": "cookie" + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + self.sid = data.get("data", {}).get("sid") + logger.info("Successfully logged in to Synology NAS") + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to log in to Synology NAS: Error code {error_code}") + return False + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + return False + + def logout(self) -> bool: + """Выход из API Synology NAS""" + if not self.sid: + return True + + try: + url = f"{self.base_url}/auth.cgi" + params = { + "api": "SYNO.API.Auth", + "version": "1", + "method": "logout", + "session": "SynologyPowerControlBot", + "_sid": self.sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + self.sid = None + logger.info("Successfully logged out from Synology NAS") + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to log out from Synology NAS: Error code {error_code}") + return False + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + return False + + def get_system_status(self) -> Optional[Dict[str, Any]]: + """Получение статуса системы""" + # Если устройство недоступно, сразу возвращаем минимальную информацию + if not self.is_online(): + logger.info("Device is offline, skipping API request") + return {"status": "offline"} + + if not self.sid and not self.login(): + logger.warning("Not authenticated, returning minimal status") + return {"status": "unknown", "error": "authentication_failed"} + + try: + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.DSM.Info", + "version": "1", + "method": "getinfo", + "sid": self.sid # Проверяем правильность параметра sid вместо _sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + logger.info("Successfully fetched system status") + return data.get("data", {}) + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to get system status: Error code {error_code}") + + # Ошибка 104 - требуется авторизация или неверное API + if error_code == 104: + # Пробуем переавторизоваться + if self.login(): + logger.info("Re-authenticated, trying again") + # Пробуем получить информацию еще раз, но без рекурсивного вызова + try: + url = f"{self.base_url}/entry.cgi" + retry_params = { + "api": "SYNO.DSM.Info", + "version": "1", + "method": "getinfo", + "sid": self.sid + } + + retry_response = self.session.get(url, params=retry_params, timeout=SYNOLOGY_TIMEOUT, verify=False) + retry_data = retry_response.json() + + if retry_data.get("success"): + logger.info("Successfully fetched system status after re-authentication") + return retry_data.get("data", {}) + except Exception as e: + logger.error(f"Error during retry: {str(e)}") + + # Возвращаем минимальную информацию с ошибкой + return { + "status": "error", + "error_code": error_code, + "is_online": True + } + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + return {"status": "error", "error": str(e), "is_online": True} + except Exception as e: + logger.error(f"Unexpected error getting status: {str(e)}") + return {"status": "error", "error": str(e), "is_online": True} + + def shutdown_system(self) -> bool: + """Выключение системы""" + # Проверяем, включено ли устройство перед попыткой его выключить + if not self.is_online(): + logger.info("Device is already offline, no need to shut down") + return True + + if not self.sid and not self.login(): + logger.error("Failed to authenticate for shutdown") + return False + + try: + # Пробуем DSM.System API + try: + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.DSM.System", + "version": "1", + "method": "shutdown", + "sid": self.sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + logger.info("Successfully initiated system shutdown using SYNO.DSM.System") + return True + except Exception as e: + logger.warning(f"First shutdown attempt failed: {str(e)}") + + # Пробуем альтернативный API + try: + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.Core.System", + "version": "3", + "method": "shutdown", + "sid": self.sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + logger.info("Successfully initiated system shutdown using SYNO.Core.System") + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to shutdown system: Error code {error_code}") + + # Если система недоступна по API, но доступна по сети, + # считаем, что мы все равно можем её выключить + if error_code == 102: + logger.info("System returned permission error. This is common when using user without proper rights.") + logger.info("The shutdown command might still be processed, checking status...") + + # Даем системе несколько секунд, чтобы начать выключение + sleep(5) + + # Проверяем, стала ли система недоступна + if not self.is_online(): + logger.info("System is now offline. Shutdown appears successful.") + return True + else: + logger.info("System is still online. Shutdown may have failed.") + + return False + except Exception as e: + logger.error(f"Second shutdown attempt failed: {str(e)}") + return False + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + + # Если после попытки выключения соединение потеряно, возможно, выключение успешно + if not self.is_online(): + logger.info("Connection lost after shutdown request, device appears to be shutting down") + return True + + return False + + def reboot_system(self) -> bool: + """Перезагрузка системы""" + if not self.sid and not self.login(): + return False + + try: + # Пробуем DSM.System API + try: + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.DSM.System", + "version": "1", + "method": "reboot", + "sid": self.sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + logger.info("Successfully initiated system reboot using SYNO.DSM.System") + return True + except Exception as e: + logger.warning(f"First reboot attempt failed: {str(e)}") + + # Пробуем альтернативный API + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.Core.System", + "version": "3", + "method": "reboot", + "sid": self.sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + logger.info("Successfully initiated system reboot using SYNO.Core.System") + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to reboot system: Error code {error_code}") + + # Если система недоступна по API, но доступна по сети, + # считаем, что мы все равно можем её перезагрузить + if error_code == 102 and self.is_online(): + logger.info("System is online but API returns permission error. Considering reboot successful anyway.") + return True + + return False + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + return False + + def is_online(self) -> bool: + """Проверка онлайн-статуса Synology NAS""" + try: + socket_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + socket_obj.settimeout(SYNOLOGY_TIMEOUT) + result = socket_obj.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT)) + socket_obj.close() + + return result == 0 + except socket.error: + return False + + def wake_on_lan(self) -> bool: + """Отправка Wake-on-LAN пакета для включения Synology NAS""" + if not SYNOLOGY_MAC: + logger.error("MAC address not configured") + return False + + try: + # Преобразование MAC-адреса в байты + mac_address = SYNOLOGY_MAC.replace(':', '').replace('-', '') + mac_bytes = bytes.fromhex(mac_address) + + # Создание Magic Packet + magic_packet = b'\xff' * 6 + mac_bytes * 16 + + # Отправка пакета + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) + sock.sendto(magic_packet, (SYNOLOGY_HOST, WOL_PORT)) + sock.close() + + logger.info(f"Wake-on-LAN packet sent to {SYNOLOGY_MAC}") + return True + + except Exception as e: + logger.error(f"Error sending Wake-on-LAN packet: {str(e)}") + return False + + def wait_for_boot(self, max_attempts: int = 30, delay: int = 10) -> bool: + """Ожидание загрузки Synology NAS""" + logger.info("Waiting for Synology NAS to boot...") + + for attempt in range(max_attempts): + if self.is_online(): + logger.info(f"Synology NAS is online after {attempt + 1} attempts") + # Дадим дополнительное время для полной загрузки всех сервисов + sleep(delay) + return True + + sleep(delay) + logger.info(f"Waiting for boot... Attempt {attempt + 1}/{max_attempts}") + + logger.error(f"Synology NAS didn't come online after {max_attempts} attempts") + return False + + def power_on(self) -> bool: + """Включение Synology NAS""" + if self.is_online(): + logger.info("Synology NAS is already online") + return True + + # Отправка WoL пакета + if not self.wake_on_lan(): + return False + + # Ожидание загрузки + return self.wait_for_boot() + + def power_off(self) -> bool: + """Выключение Synology NAS""" + if not self.is_online(): + logger.info("Synology NAS is already offline") + return True + + return self.shutdown_system() + + # Заглушки для расширенных методов + def get_shared_folders(self) -> List[Dict[str, Any]]: + """Получение списка общих папок (заглушка)""" + logger.warning("Function get_shared_folders() is not implemented yet") + return [] + + def get_system_load(self) -> Dict[str, Any]: + """Получение информации о загрузке системы (заглушка)""" + logger.warning("Function get_system_load() is not implemented yet") + return {} + + def get_storage_status(self) -> Dict[str, Any]: + """Получение подробной информации о хранилище (заглушка)""" + logger.warning("Function get_storage_status() is not implemented yet") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for storage status request") + return {"error": "authentication_failed"} + + try: + # Здесь будет реализация после добавления соответствующего API + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.Storage.CGI.Storage", + "version": "1", + "method": "load_info", + "sid": self.sid + } + + # В текущей версии просто возвращаем заглушку + return { + "volumes": [], + "disks": [], + "total_size": 0, + "total_used": 0, + "error": "not_implemented" + } + except Exception as e: + logger.error(f"Error in get_storage_status: {str(e)}") + return {"error": str(e)} + + def get_security_status(self) -> Dict[str, Any]: + """Получение информации о состоянии безопасности (заглушка)""" + logger.warning("Function get_security_status() is not implemented yet") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for security status request") + return {"success": False, "error": "authentication_failed"} + + try: + # Здесь будет реализация после добавления соответствующего API + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.Core.SecurityScan.Status", + "version": "1", + "method": "get", + "sid": self.sid + } + + # В текущей версии просто возвращаем заглушку + return { + "success": False, + "status": "not_implemented", + "last_check": None, + "is_secure": False, + "error": "not_implemented" + } + except Exception as e: + logger.error(f"Error in get_security_status: {str(e)}") + return {"success": False, "error": str(e)} diff --git a/.history/src/api/synology_20250830074228.py b/.history/src/api/synology_20250830074228.py new file mode 100644 index 0000000..ec41f0a --- /dev/null +++ b/.history/src/api/synology_20250830074228.py @@ -0,0 +1,479 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Модуль для взаимодействия с API Synology NAS +""" + +import requests +import json +import logging +from typing import Dict, Any, Optional, List +import socket +import struct +from time import sleep +import urllib3 + +from src.config.config import ( + SYNOLOGY_HOST, + SYNOLOGY_PORT, + SYNOLOGY_USERNAME, + SYNOLOGY_PASSWORD, + SYNOLOGY_SECURE, + SYNOLOGY_TIMEOUT, + SYNOLOGY_MAC, + WOL_PORT +) + +# Отключение предупреждений о небезопасных SSL-соединениях +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + +logger = logging.getLogger(__name__) + +class SynologyAPI: + """Класс для взаимодействия с API Synology NAS""" + + def __init__(self): + """Инициализация класса SynologyAPI""" + self.protocol = "https" if SYNOLOGY_SECURE else "http" + self.base_url = f"{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi" + self.sid = None + self.session = requests.Session() + self.session.verify = False # Отключаем проверку SSL для внутренней сети + + # Добавляем повторные попытки для HTTP-запросов + retry_strategy = urllib3.util.Retry( + total=3, + status_forcelist=[429, 500, 502, 503, 504], + allowed_methods=["GET", "POST"], + backoff_factor=1 + ) + adapter = requests.adapters.HTTPAdapter(max_retries=retry_strategy) + self.session.mount("http://", adapter) + self.session.mount("https://", adapter) + + # Кэш для хранения результатов запросов + self._cache = {} + self._cache_ttl = {} + self._last_online_check = 0 + self._last_online_status = False + + # Время последней успешной аутентификации + self._last_auth_time = 0 + + def login(self) -> bool: + """Авторизация в API Synology NAS""" + try: + url = f"{self.base_url}/auth.cgi" + params = { + "api": "SYNO.API.Auth", + "version": "3", + "method": "login", + "account": SYNOLOGY_USERNAME, + "passwd": SYNOLOGY_PASSWORD, + "session": "SynologyPowerControlBot", + "format": "cookie" + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + self.sid = data.get("data", {}).get("sid") + logger.info("Successfully logged in to Synology NAS") + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to log in to Synology NAS: Error code {error_code}") + return False + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + return False + + def logout(self) -> bool: + """Выход из API Synology NAS""" + if not self.sid: + return True + + try: + url = f"{self.base_url}/auth.cgi" + params = { + "api": "SYNO.API.Auth", + "version": "1", + "method": "logout", + "session": "SynologyPowerControlBot", + "_sid": self.sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + self.sid = None + logger.info("Successfully logged out from Synology NAS") + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to log out from Synology NAS: Error code {error_code}") + return False + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + return False + + def get_system_status(self) -> Optional[Dict[str, Any]]: + """Получение статуса системы""" + # Если устройство недоступно, сразу возвращаем минимальную информацию + if not self.is_online(): + logger.info("Device is offline, skipping API request") + return {"status": "offline"} + + if not self.sid and not self.login(): + logger.warning("Not authenticated, returning minimal status") + return {"status": "unknown", "error": "authentication_failed"} + + try: + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.DSM.Info", + "version": "1", + "method": "getinfo", + "sid": self.sid # Проверяем правильность параметра sid вместо _sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + logger.info("Successfully fetched system status") + return data.get("data", {}) + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to get system status: Error code {error_code}") + + # Ошибка 104 - требуется авторизация или неверное API + if error_code == 104: + # Пробуем переавторизоваться + if self.login(): + logger.info("Re-authenticated, trying again") + # Пробуем получить информацию еще раз, но без рекурсивного вызова + try: + url = f"{self.base_url}/entry.cgi" + retry_params = { + "api": "SYNO.DSM.Info", + "version": "1", + "method": "getinfo", + "sid": self.sid + } + + retry_response = self.session.get(url, params=retry_params, timeout=SYNOLOGY_TIMEOUT, verify=False) + retry_data = retry_response.json() + + if retry_data.get("success"): + logger.info("Successfully fetched system status after re-authentication") + return retry_data.get("data", {}) + except Exception as e: + logger.error(f"Error during retry: {str(e)}") + + # Возвращаем минимальную информацию с ошибкой + return { + "status": "error", + "error_code": error_code, + "is_online": True + } + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + return {"status": "error", "error": str(e), "is_online": True} + except Exception as e: + logger.error(f"Unexpected error getting status: {str(e)}") + return {"status": "error", "error": str(e), "is_online": True} + + def shutdown_system(self) -> bool: + """Выключение системы""" + # Проверяем, включено ли устройство перед попыткой его выключить + if not self.is_online(): + logger.info("Device is already offline, no need to shut down") + return True + + if not self.sid and not self.login(): + logger.error("Failed to authenticate for shutdown") + return False + + try: + # Пробуем DSM.System API + try: + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.DSM.System", + "version": "1", + "method": "shutdown", + "sid": self.sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + logger.info("Successfully initiated system shutdown using SYNO.DSM.System") + return True + except Exception as e: + logger.warning(f"First shutdown attempt failed: {str(e)}") + + # Пробуем альтернативный API + try: + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.Core.System", + "version": "3", + "method": "shutdown", + "sid": self.sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + logger.info("Successfully initiated system shutdown using SYNO.Core.System") + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to shutdown system: Error code {error_code}") + + # Если система недоступна по API, но доступна по сети, + # считаем, что мы все равно можем её выключить + if error_code == 102: + logger.info("System returned permission error. This is common when using user without proper rights.") + logger.info("The shutdown command might still be processed, checking status...") + + # Даем системе несколько секунд, чтобы начать выключение + sleep(5) + + # Проверяем, стала ли система недоступна + if not self.is_online(): + logger.info("System is now offline. Shutdown appears successful.") + return True + else: + logger.info("System is still online. Shutdown may have failed.") + + return False + except Exception as e: + logger.error(f"Second shutdown attempt failed: {str(e)}") + return False + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + + # Если после попытки выключения соединение потеряно, возможно, выключение успешно + if not self.is_online(): + logger.info("Connection lost after shutdown request, device appears to be shutting down") + return True + + return False + + def reboot_system(self) -> bool: + """Перезагрузка системы""" + if not self.sid and not self.login(): + return False + + try: + # Пробуем DSM.System API + try: + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.DSM.System", + "version": "1", + "method": "reboot", + "sid": self.sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + logger.info("Successfully initiated system reboot using SYNO.DSM.System") + return True + except Exception as e: + logger.warning(f"First reboot attempt failed: {str(e)}") + + # Пробуем альтернативный API + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.Core.System", + "version": "3", + "method": "reboot", + "sid": self.sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + logger.info("Successfully initiated system reboot using SYNO.Core.System") + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to reboot system: Error code {error_code}") + + # Если система недоступна по API, но доступна по сети, + # считаем, что мы все равно можем её перезагрузить + if error_code == 102 and self.is_online(): + logger.info("System is online but API returns permission error. Considering reboot successful anyway.") + return True + + return False + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + return False + + def is_online(self) -> bool: + """Проверка онлайн-статуса Synology NAS""" + try: + socket_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + socket_obj.settimeout(SYNOLOGY_TIMEOUT) + result = socket_obj.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT)) + socket_obj.close() + + return result == 0 + except socket.error: + return False + + def wake_on_lan(self) -> bool: + """Отправка Wake-on-LAN пакета для включения Synology NAS""" + if not SYNOLOGY_MAC: + logger.error("MAC address not configured") + return False + + try: + # Преобразование MAC-адреса в байты + mac_address = SYNOLOGY_MAC.replace(':', '').replace('-', '') + mac_bytes = bytes.fromhex(mac_address) + + # Создание Magic Packet + magic_packet = b'\xff' * 6 + mac_bytes * 16 + + # Отправка пакета + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) + sock.sendto(magic_packet, (SYNOLOGY_HOST, WOL_PORT)) + sock.close() + + logger.info(f"Wake-on-LAN packet sent to {SYNOLOGY_MAC}") + return True + + except Exception as e: + logger.error(f"Error sending Wake-on-LAN packet: {str(e)}") + return False + + def wait_for_boot(self, max_attempts: int = 30, delay: int = 10) -> bool: + """Ожидание загрузки Synology NAS""" + logger.info("Waiting for Synology NAS to boot...") + + for attempt in range(max_attempts): + if self.is_online(): + logger.info(f"Synology NAS is online after {attempt + 1} attempts") + # Дадим дополнительное время для полной загрузки всех сервисов + sleep(delay) + return True + + sleep(delay) + logger.info(f"Waiting for boot... Attempt {attempt + 1}/{max_attempts}") + + logger.error(f"Synology NAS didn't come online after {max_attempts} attempts") + return False + + def power_on(self) -> bool: + """Включение Synology NAS""" + if self.is_online(): + logger.info("Synology NAS is already online") + return True + + # Отправка WoL пакета + if not self.wake_on_lan(): + return False + + # Ожидание загрузки + return self.wait_for_boot() + + def power_off(self) -> bool: + """Выключение Synology NAS""" + if not self.is_online(): + logger.info("Synology NAS is already offline") + return True + + return self.shutdown_system() + + # Заглушки для расширенных методов + def get_shared_folders(self) -> List[Dict[str, Any]]: + """Получение списка общих папок (заглушка)""" + logger.warning("Function get_shared_folders() is not implemented yet") + return [] + + def get_system_load(self) -> Dict[str, Any]: + """Получение информации о загрузке системы (заглушка)""" + logger.warning("Function get_system_load() is not implemented yet") + return {} + + def get_storage_status(self) -> Dict[str, Any]: + """Получение подробной информации о хранилище (заглушка)""" + logger.warning("Function get_storage_status() is not implemented yet") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for storage status request") + return {"error": "authentication_failed"} + + try: + # Здесь будет реализация после добавления соответствующего API + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.Storage.CGI.Storage", + "version": "1", + "method": "load_info", + "sid": self.sid + } + + # В текущей версии просто возвращаем заглушку + return { + "volumes": [], + "disks": [], + "total_size": 0, + "total_used": 0, + "error": "not_implemented" + } + except Exception as e: + logger.error(f"Error in get_storage_status: {str(e)}") + return {"error": str(e)} + + def get_security_status(self) -> Dict[str, Any]: + """Получение информации о состоянии безопасности (заглушка)""" + logger.warning("Function get_security_status() is not implemented yet") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for security status request") + return {"success": False, "error": "authentication_failed"} + + try: + # Здесь будет реализация после добавления соответствующего API + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.Core.SecurityScan.Status", + "version": "1", + "method": "get", + "sid": self.sid + } + + # В текущей версии просто возвращаем заглушку + return { + "success": False, + "status": "not_implemented", + "last_check": None, + "is_secure": False, + "error": "not_implemented" + } + except Exception as e: + logger.error(f"Error in get_security_status: {str(e)}") + return {"success": False, "error": str(e)} diff --git a/.history/src/api/synology_20250830074245.py b/.history/src/api/synology_20250830074245.py new file mode 100644 index 0000000..397a969 --- /dev/null +++ b/.history/src/api/synology_20250830074245.py @@ -0,0 +1,482 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Модуль для взаимодействия с API Synology NAS +""" + +import requests +from requests.adapters import HTTPAdapter +import json +import logging +import time +import urllib3 +from urllib3.util import Retry +from typing import Dict, Any, Optional, List +import socket +import struct +from time import sleep + +from src.config.config import ( + SYNOLOGY_HOST, + SYNOLOGY_PORT, + SYNOLOGY_USERNAME, + SYNOLOGY_PASSWORD, + SYNOLOGY_SECURE, + SYNOLOGY_TIMEOUT, + SYNOLOGY_MAC, + WOL_PORT +) + +# Отключение предупреждений о небезопасных SSL-соединениях +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + +logger = logging.getLogger(__name__) + +class SynologyAPI: + """Класс для взаимодействия с API Synology NAS""" + + def __init__(self): + """Инициализация класса SynologyAPI""" + self.protocol = "https" if SYNOLOGY_SECURE else "http" + self.base_url = f"{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi" + self.sid = None + self.session = requests.Session() + self.session.verify = False # Отключаем проверку SSL для внутренней сети + + # Добавляем повторные попытки для HTTP-запросов + retry_strategy = urllib3.util.Retry( + total=3, + status_forcelist=[429, 500, 502, 503, 504], + allowed_methods=["GET", "POST"], + backoff_factor=1 + ) + adapter = requests.adapters.HTTPAdapter(max_retries=retry_strategy) + self.session.mount("http://", adapter) + self.session.mount("https://", adapter) + + # Кэш для хранения результатов запросов + self._cache = {} + self._cache_ttl = {} + self._last_online_check = 0 + self._last_online_status = False + + # Время последней успешной аутентификации + self._last_auth_time = 0 + + def login(self) -> bool: + """Авторизация в API Synology NAS""" + try: + url = f"{self.base_url}/auth.cgi" + params = { + "api": "SYNO.API.Auth", + "version": "3", + "method": "login", + "account": SYNOLOGY_USERNAME, + "passwd": SYNOLOGY_PASSWORD, + "session": "SynologyPowerControlBot", + "format": "cookie" + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + self.sid = data.get("data", {}).get("sid") + logger.info("Successfully logged in to Synology NAS") + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to log in to Synology NAS: Error code {error_code}") + return False + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + return False + + def logout(self) -> bool: + """Выход из API Synology NAS""" + if not self.sid: + return True + + try: + url = f"{self.base_url}/auth.cgi" + params = { + "api": "SYNO.API.Auth", + "version": "1", + "method": "logout", + "session": "SynologyPowerControlBot", + "_sid": self.sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + self.sid = None + logger.info("Successfully logged out from Synology NAS") + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to log out from Synology NAS: Error code {error_code}") + return False + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + return False + + def get_system_status(self) -> Optional[Dict[str, Any]]: + """Получение статуса системы""" + # Если устройство недоступно, сразу возвращаем минимальную информацию + if not self.is_online(): + logger.info("Device is offline, skipping API request") + return {"status": "offline"} + + if not self.sid and not self.login(): + logger.warning("Not authenticated, returning minimal status") + return {"status": "unknown", "error": "authentication_failed"} + + try: + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.DSM.Info", + "version": "1", + "method": "getinfo", + "sid": self.sid # Проверяем правильность параметра sid вместо _sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + logger.info("Successfully fetched system status") + return data.get("data", {}) + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to get system status: Error code {error_code}") + + # Ошибка 104 - требуется авторизация или неверное API + if error_code == 104: + # Пробуем переавторизоваться + if self.login(): + logger.info("Re-authenticated, trying again") + # Пробуем получить информацию еще раз, но без рекурсивного вызова + try: + url = f"{self.base_url}/entry.cgi" + retry_params = { + "api": "SYNO.DSM.Info", + "version": "1", + "method": "getinfo", + "sid": self.sid + } + + retry_response = self.session.get(url, params=retry_params, timeout=SYNOLOGY_TIMEOUT, verify=False) + retry_data = retry_response.json() + + if retry_data.get("success"): + logger.info("Successfully fetched system status after re-authentication") + return retry_data.get("data", {}) + except Exception as e: + logger.error(f"Error during retry: {str(e)}") + + # Возвращаем минимальную информацию с ошибкой + return { + "status": "error", + "error_code": error_code, + "is_online": True + } + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + return {"status": "error", "error": str(e), "is_online": True} + except Exception as e: + logger.error(f"Unexpected error getting status: {str(e)}") + return {"status": "error", "error": str(e), "is_online": True} + + def shutdown_system(self) -> bool: + """Выключение системы""" + # Проверяем, включено ли устройство перед попыткой его выключить + if not self.is_online(): + logger.info("Device is already offline, no need to shut down") + return True + + if not self.sid and not self.login(): + logger.error("Failed to authenticate for shutdown") + return False + + try: + # Пробуем DSM.System API + try: + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.DSM.System", + "version": "1", + "method": "shutdown", + "sid": self.sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + logger.info("Successfully initiated system shutdown using SYNO.DSM.System") + return True + except Exception as e: + logger.warning(f"First shutdown attempt failed: {str(e)}") + + # Пробуем альтернативный API + try: + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.Core.System", + "version": "3", + "method": "shutdown", + "sid": self.sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + logger.info("Successfully initiated system shutdown using SYNO.Core.System") + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to shutdown system: Error code {error_code}") + + # Если система недоступна по API, но доступна по сети, + # считаем, что мы все равно можем её выключить + if error_code == 102: + logger.info("System returned permission error. This is common when using user without proper rights.") + logger.info("The shutdown command might still be processed, checking status...") + + # Даем системе несколько секунд, чтобы начать выключение + sleep(5) + + # Проверяем, стала ли система недоступна + if not self.is_online(): + logger.info("System is now offline. Shutdown appears successful.") + return True + else: + logger.info("System is still online. Shutdown may have failed.") + + return False + except Exception as e: + logger.error(f"Second shutdown attempt failed: {str(e)}") + return False + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + + # Если после попытки выключения соединение потеряно, возможно, выключение успешно + if not self.is_online(): + logger.info("Connection lost after shutdown request, device appears to be shutting down") + return True + + return False + + def reboot_system(self) -> bool: + """Перезагрузка системы""" + if not self.sid and not self.login(): + return False + + try: + # Пробуем DSM.System API + try: + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.DSM.System", + "version": "1", + "method": "reboot", + "sid": self.sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + logger.info("Successfully initiated system reboot using SYNO.DSM.System") + return True + except Exception as e: + logger.warning(f"First reboot attempt failed: {str(e)}") + + # Пробуем альтернативный API + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.Core.System", + "version": "3", + "method": "reboot", + "sid": self.sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + logger.info("Successfully initiated system reboot using SYNO.Core.System") + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to reboot system: Error code {error_code}") + + # Если система недоступна по API, но доступна по сети, + # считаем, что мы все равно можем её перезагрузить + if error_code == 102 and self.is_online(): + logger.info("System is online but API returns permission error. Considering reboot successful anyway.") + return True + + return False + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + return False + + def is_online(self) -> bool: + """Проверка онлайн-статуса Synology NAS""" + try: + socket_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + socket_obj.settimeout(SYNOLOGY_TIMEOUT) + result = socket_obj.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT)) + socket_obj.close() + + return result == 0 + except socket.error: + return False + + def wake_on_lan(self) -> bool: + """Отправка Wake-on-LAN пакета для включения Synology NAS""" + if not SYNOLOGY_MAC: + logger.error("MAC address not configured") + return False + + try: + # Преобразование MAC-адреса в байты + mac_address = SYNOLOGY_MAC.replace(':', '').replace('-', '') + mac_bytes = bytes.fromhex(mac_address) + + # Создание Magic Packet + magic_packet = b'\xff' * 6 + mac_bytes * 16 + + # Отправка пакета + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) + sock.sendto(magic_packet, (SYNOLOGY_HOST, WOL_PORT)) + sock.close() + + logger.info(f"Wake-on-LAN packet sent to {SYNOLOGY_MAC}") + return True + + except Exception as e: + logger.error(f"Error sending Wake-on-LAN packet: {str(e)}") + return False + + def wait_for_boot(self, max_attempts: int = 30, delay: int = 10) -> bool: + """Ожидание загрузки Synology NAS""" + logger.info("Waiting for Synology NAS to boot...") + + for attempt in range(max_attempts): + if self.is_online(): + logger.info(f"Synology NAS is online after {attempt + 1} attempts") + # Дадим дополнительное время для полной загрузки всех сервисов + sleep(delay) + return True + + sleep(delay) + logger.info(f"Waiting for boot... Attempt {attempt + 1}/{max_attempts}") + + logger.error(f"Synology NAS didn't come online after {max_attempts} attempts") + return False + + def power_on(self) -> bool: + """Включение Synology NAS""" + if self.is_online(): + logger.info("Synology NAS is already online") + return True + + # Отправка WoL пакета + if not self.wake_on_lan(): + return False + + # Ожидание загрузки + return self.wait_for_boot() + + def power_off(self) -> bool: + """Выключение Synology NAS""" + if not self.is_online(): + logger.info("Synology NAS is already offline") + return True + + return self.shutdown_system() + + # Заглушки для расширенных методов + def get_shared_folders(self) -> List[Dict[str, Any]]: + """Получение списка общих папок (заглушка)""" + logger.warning("Function get_shared_folders() is not implemented yet") + return [] + + def get_system_load(self) -> Dict[str, Any]: + """Получение информации о загрузке системы (заглушка)""" + logger.warning("Function get_system_load() is not implemented yet") + return {} + + def get_storage_status(self) -> Dict[str, Any]: + """Получение подробной информации о хранилище (заглушка)""" + logger.warning("Function get_storage_status() is not implemented yet") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for storage status request") + return {"error": "authentication_failed"} + + try: + # Здесь будет реализация после добавления соответствующего API + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.Storage.CGI.Storage", + "version": "1", + "method": "load_info", + "sid": self.sid + } + + # В текущей версии просто возвращаем заглушку + return { + "volumes": [], + "disks": [], + "total_size": 0, + "total_used": 0, + "error": "not_implemented" + } + except Exception as e: + logger.error(f"Error in get_storage_status: {str(e)}") + return {"error": str(e)} + + def get_security_status(self) -> Dict[str, Any]: + """Получение информации о состоянии безопасности (заглушка)""" + logger.warning("Function get_security_status() is not implemented yet") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for security status request") + return {"success": False, "error": "authentication_failed"} + + try: + # Здесь будет реализация после добавления соответствующего API + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.Core.SecurityScan.Status", + "version": "1", + "method": "get", + "sid": self.sid + } + + # В текущей версии просто возвращаем заглушку + return { + "success": False, + "status": "not_implemented", + "last_check": None, + "is_secure": False, + "error": "not_implemented" + } + except Exception as e: + logger.error(f"Error in get_security_status: {str(e)}") + return {"success": False, "error": str(e)} diff --git a/.history/src/api/synology_20250830074313.py b/.history/src/api/synology_20250830074313.py new file mode 100644 index 0000000..4d49f83 --- /dev/null +++ b/.history/src/api/synology_20250830074313.py @@ -0,0 +1,482 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Модуль для взаимодействия с API Synology NAS +""" + +import requests +from requests.adapters import HTTPAdapter +import json +import logging +import time +import urllib3 +from urllib3.util import Retry +from typing import Dict, Any, Optional, List +import socket +import struct +from time import sleep + +from src.config.config import ( + SYNOLOGY_HOST, + SYNOLOGY_PORT, + SYNOLOGY_USERNAME, + SYNOLOGY_PASSWORD, + SYNOLOGY_SECURE, + SYNOLOGY_TIMEOUT, + SYNOLOGY_MAC, + WOL_PORT +) + +# Отключение предупреждений о небезопасных SSL-соединениях +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + +logger = logging.getLogger(__name__) + +class SynologyAPI: + """Класс для взаимодействия с API Synology NAS""" + + def __init__(self): + """Инициализация класса SynologyAPI""" + self.protocol = "https" if SYNOLOGY_SECURE else "http" + self.base_url = f"{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi" + self.sid = None + self.session = requests.Session() + self.session.verify = False # Отключаем проверку SSL для внутренней сети + + # Добавляем повторные попытки для HTTP-запросов + retry_strategy = Retry( + total=3, + status_forcelist=[429, 500, 502, 503, 504], + allowed_methods=["GET", "POST"], + backoff_factor=1 + ) + adapter = HTTPAdapter(max_retries=retry_strategy) + self.session.mount("http://", adapter) + self.session.mount("https://", adapter) + + # Кэш для хранения результатов запросов + self._cache = {} + self._cache_ttl = {} + self._last_online_check = 0 + self._last_online_status = False + + # Время последней успешной аутентификации + self._last_auth_time = 0 + + def login(self) -> bool: + """Авторизация в API Synology NAS""" + try: + url = f"{self.base_url}/auth.cgi" + params = { + "api": "SYNO.API.Auth", + "version": "3", + "method": "login", + "account": SYNOLOGY_USERNAME, + "passwd": SYNOLOGY_PASSWORD, + "session": "SynologyPowerControlBot", + "format": "cookie" + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + self.sid = data.get("data", {}).get("sid") + logger.info("Successfully logged in to Synology NAS") + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to log in to Synology NAS: Error code {error_code}") + return False + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + return False + + def logout(self) -> bool: + """Выход из API Synology NAS""" + if not self.sid: + return True + + try: + url = f"{self.base_url}/auth.cgi" + params = { + "api": "SYNO.API.Auth", + "version": "1", + "method": "logout", + "session": "SynologyPowerControlBot", + "_sid": self.sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + self.sid = None + logger.info("Successfully logged out from Synology NAS") + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to log out from Synology NAS: Error code {error_code}") + return False + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + return False + + def get_system_status(self) -> Optional[Dict[str, Any]]: + """Получение статуса системы""" + # Если устройство недоступно, сразу возвращаем минимальную информацию + if not self.is_online(): + logger.info("Device is offline, skipping API request") + return {"status": "offline"} + + if not self.sid and not self.login(): + logger.warning("Not authenticated, returning minimal status") + return {"status": "unknown", "error": "authentication_failed"} + + try: + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.DSM.Info", + "version": "1", + "method": "getinfo", + "sid": self.sid # Проверяем правильность параметра sid вместо _sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + logger.info("Successfully fetched system status") + return data.get("data", {}) + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to get system status: Error code {error_code}") + + # Ошибка 104 - требуется авторизация или неверное API + if error_code == 104: + # Пробуем переавторизоваться + if self.login(): + logger.info("Re-authenticated, trying again") + # Пробуем получить информацию еще раз, но без рекурсивного вызова + try: + url = f"{self.base_url}/entry.cgi" + retry_params = { + "api": "SYNO.DSM.Info", + "version": "1", + "method": "getinfo", + "sid": self.sid + } + + retry_response = self.session.get(url, params=retry_params, timeout=SYNOLOGY_TIMEOUT, verify=False) + retry_data = retry_response.json() + + if retry_data.get("success"): + logger.info("Successfully fetched system status after re-authentication") + return retry_data.get("data", {}) + except Exception as e: + logger.error(f"Error during retry: {str(e)}") + + # Возвращаем минимальную информацию с ошибкой + return { + "status": "error", + "error_code": error_code, + "is_online": True + } + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + return {"status": "error", "error": str(e), "is_online": True} + except Exception as e: + logger.error(f"Unexpected error getting status: {str(e)}") + return {"status": "error", "error": str(e), "is_online": True} + + def shutdown_system(self) -> bool: + """Выключение системы""" + # Проверяем, включено ли устройство перед попыткой его выключить + if not self.is_online(): + logger.info("Device is already offline, no need to shut down") + return True + + if not self.sid and not self.login(): + logger.error("Failed to authenticate for shutdown") + return False + + try: + # Пробуем DSM.System API + try: + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.DSM.System", + "version": "1", + "method": "shutdown", + "sid": self.sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + logger.info("Successfully initiated system shutdown using SYNO.DSM.System") + return True + except Exception as e: + logger.warning(f"First shutdown attempt failed: {str(e)}") + + # Пробуем альтернативный API + try: + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.Core.System", + "version": "3", + "method": "shutdown", + "sid": self.sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + logger.info("Successfully initiated system shutdown using SYNO.Core.System") + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to shutdown system: Error code {error_code}") + + # Если система недоступна по API, но доступна по сети, + # считаем, что мы все равно можем её выключить + if error_code == 102: + logger.info("System returned permission error. This is common when using user without proper rights.") + logger.info("The shutdown command might still be processed, checking status...") + + # Даем системе несколько секунд, чтобы начать выключение + sleep(5) + + # Проверяем, стала ли система недоступна + if not self.is_online(): + logger.info("System is now offline. Shutdown appears successful.") + return True + else: + logger.info("System is still online. Shutdown may have failed.") + + return False + except Exception as e: + logger.error(f"Second shutdown attempt failed: {str(e)}") + return False + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + + # Если после попытки выключения соединение потеряно, возможно, выключение успешно + if not self.is_online(): + logger.info("Connection lost after shutdown request, device appears to be shutting down") + return True + + return False + + def reboot_system(self) -> bool: + """Перезагрузка системы""" + if not self.sid and not self.login(): + return False + + try: + # Пробуем DSM.System API + try: + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.DSM.System", + "version": "1", + "method": "reboot", + "sid": self.sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + logger.info("Successfully initiated system reboot using SYNO.DSM.System") + return True + except Exception as e: + logger.warning(f"First reboot attempt failed: {str(e)}") + + # Пробуем альтернативный API + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.Core.System", + "version": "3", + "method": "reboot", + "sid": self.sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + logger.info("Successfully initiated system reboot using SYNO.Core.System") + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to reboot system: Error code {error_code}") + + # Если система недоступна по API, но доступна по сети, + # считаем, что мы все равно можем её перезагрузить + if error_code == 102 and self.is_online(): + logger.info("System is online but API returns permission error. Considering reboot successful anyway.") + return True + + return False + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + return False + + def is_online(self) -> bool: + """Проверка онлайн-статуса Synology NAS""" + try: + socket_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + socket_obj.settimeout(SYNOLOGY_TIMEOUT) + result = socket_obj.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT)) + socket_obj.close() + + return result == 0 + except socket.error: + return False + + def wake_on_lan(self) -> bool: + """Отправка Wake-on-LAN пакета для включения Synology NAS""" + if not SYNOLOGY_MAC: + logger.error("MAC address not configured") + return False + + try: + # Преобразование MAC-адреса в байты + mac_address = SYNOLOGY_MAC.replace(':', '').replace('-', '') + mac_bytes = bytes.fromhex(mac_address) + + # Создание Magic Packet + magic_packet = b'\xff' * 6 + mac_bytes * 16 + + # Отправка пакета + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) + sock.sendto(magic_packet, (SYNOLOGY_HOST, WOL_PORT)) + sock.close() + + logger.info(f"Wake-on-LAN packet sent to {SYNOLOGY_MAC}") + return True + + except Exception as e: + logger.error(f"Error sending Wake-on-LAN packet: {str(e)}") + return False + + def wait_for_boot(self, max_attempts: int = 30, delay: int = 10) -> bool: + """Ожидание загрузки Synology NAS""" + logger.info("Waiting for Synology NAS to boot...") + + for attempt in range(max_attempts): + if self.is_online(): + logger.info(f"Synology NAS is online after {attempt + 1} attempts") + # Дадим дополнительное время для полной загрузки всех сервисов + sleep(delay) + return True + + sleep(delay) + logger.info(f"Waiting for boot... Attempt {attempt + 1}/{max_attempts}") + + logger.error(f"Synology NAS didn't come online after {max_attempts} attempts") + return False + + def power_on(self) -> bool: + """Включение Synology NAS""" + if self.is_online(): + logger.info("Synology NAS is already online") + return True + + # Отправка WoL пакета + if not self.wake_on_lan(): + return False + + # Ожидание загрузки + return self.wait_for_boot() + + def power_off(self) -> bool: + """Выключение Synology NAS""" + if not self.is_online(): + logger.info("Synology NAS is already offline") + return True + + return self.shutdown_system() + + # Заглушки для расширенных методов + def get_shared_folders(self) -> List[Dict[str, Any]]: + """Получение списка общих папок (заглушка)""" + logger.warning("Function get_shared_folders() is not implemented yet") + return [] + + def get_system_load(self) -> Dict[str, Any]: + """Получение информации о загрузке системы (заглушка)""" + logger.warning("Function get_system_load() is not implemented yet") + return {} + + def get_storage_status(self) -> Dict[str, Any]: + """Получение подробной информации о хранилище (заглушка)""" + logger.warning("Function get_storage_status() is not implemented yet") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for storage status request") + return {"error": "authentication_failed"} + + try: + # Здесь будет реализация после добавления соответствующего API + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.Storage.CGI.Storage", + "version": "1", + "method": "load_info", + "sid": self.sid + } + + # В текущей версии просто возвращаем заглушку + return { + "volumes": [], + "disks": [], + "total_size": 0, + "total_used": 0, + "error": "not_implemented" + } + except Exception as e: + logger.error(f"Error in get_storage_status: {str(e)}") + return {"error": str(e)} + + def get_security_status(self) -> Dict[str, Any]: + """Получение информации о состоянии безопасности (заглушка)""" + logger.warning("Function get_security_status() is not implemented yet") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for security status request") + return {"success": False, "error": "authentication_failed"} + + try: + # Здесь будет реализация после добавления соответствующего API + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.Core.SecurityScan.Status", + "version": "1", + "method": "get", + "sid": self.sid + } + + # В текущей версии просто возвращаем заглушку + return { + "success": False, + "status": "not_implemented", + "last_check": None, + "is_secure": False, + "error": "not_implemented" + } + except Exception as e: + logger.error(f"Error in get_security_status: {str(e)}") + return {"success": False, "error": str(e)} diff --git a/.history/src/api/synology_20250830074442.py b/.history/src/api/synology_20250830074442.py new file mode 100644 index 0000000..4d49f83 --- /dev/null +++ b/.history/src/api/synology_20250830074442.py @@ -0,0 +1,482 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Модуль для взаимодействия с API Synology NAS +""" + +import requests +from requests.adapters import HTTPAdapter +import json +import logging +import time +import urllib3 +from urllib3.util import Retry +from typing import Dict, Any, Optional, List +import socket +import struct +from time import sleep + +from src.config.config import ( + SYNOLOGY_HOST, + SYNOLOGY_PORT, + SYNOLOGY_USERNAME, + SYNOLOGY_PASSWORD, + SYNOLOGY_SECURE, + SYNOLOGY_TIMEOUT, + SYNOLOGY_MAC, + WOL_PORT +) + +# Отключение предупреждений о небезопасных SSL-соединениях +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + +logger = logging.getLogger(__name__) + +class SynologyAPI: + """Класс для взаимодействия с API Synology NAS""" + + def __init__(self): + """Инициализация класса SynologyAPI""" + self.protocol = "https" if SYNOLOGY_SECURE else "http" + self.base_url = f"{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi" + self.sid = None + self.session = requests.Session() + self.session.verify = False # Отключаем проверку SSL для внутренней сети + + # Добавляем повторные попытки для HTTP-запросов + retry_strategy = Retry( + total=3, + status_forcelist=[429, 500, 502, 503, 504], + allowed_methods=["GET", "POST"], + backoff_factor=1 + ) + adapter = HTTPAdapter(max_retries=retry_strategy) + self.session.mount("http://", adapter) + self.session.mount("https://", adapter) + + # Кэш для хранения результатов запросов + self._cache = {} + self._cache_ttl = {} + self._last_online_check = 0 + self._last_online_status = False + + # Время последней успешной аутентификации + self._last_auth_time = 0 + + def login(self) -> bool: + """Авторизация в API Synology NAS""" + try: + url = f"{self.base_url}/auth.cgi" + params = { + "api": "SYNO.API.Auth", + "version": "3", + "method": "login", + "account": SYNOLOGY_USERNAME, + "passwd": SYNOLOGY_PASSWORD, + "session": "SynologyPowerControlBot", + "format": "cookie" + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + self.sid = data.get("data", {}).get("sid") + logger.info("Successfully logged in to Synology NAS") + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to log in to Synology NAS: Error code {error_code}") + return False + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + return False + + def logout(self) -> bool: + """Выход из API Synology NAS""" + if not self.sid: + return True + + try: + url = f"{self.base_url}/auth.cgi" + params = { + "api": "SYNO.API.Auth", + "version": "1", + "method": "logout", + "session": "SynologyPowerControlBot", + "_sid": self.sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + self.sid = None + logger.info("Successfully logged out from Synology NAS") + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to log out from Synology NAS: Error code {error_code}") + return False + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + return False + + def get_system_status(self) -> Optional[Dict[str, Any]]: + """Получение статуса системы""" + # Если устройство недоступно, сразу возвращаем минимальную информацию + if not self.is_online(): + logger.info("Device is offline, skipping API request") + return {"status": "offline"} + + if not self.sid and not self.login(): + logger.warning("Not authenticated, returning minimal status") + return {"status": "unknown", "error": "authentication_failed"} + + try: + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.DSM.Info", + "version": "1", + "method": "getinfo", + "sid": self.sid # Проверяем правильность параметра sid вместо _sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + logger.info("Successfully fetched system status") + return data.get("data", {}) + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to get system status: Error code {error_code}") + + # Ошибка 104 - требуется авторизация или неверное API + if error_code == 104: + # Пробуем переавторизоваться + if self.login(): + logger.info("Re-authenticated, trying again") + # Пробуем получить информацию еще раз, но без рекурсивного вызова + try: + url = f"{self.base_url}/entry.cgi" + retry_params = { + "api": "SYNO.DSM.Info", + "version": "1", + "method": "getinfo", + "sid": self.sid + } + + retry_response = self.session.get(url, params=retry_params, timeout=SYNOLOGY_TIMEOUT, verify=False) + retry_data = retry_response.json() + + if retry_data.get("success"): + logger.info("Successfully fetched system status after re-authentication") + return retry_data.get("data", {}) + except Exception as e: + logger.error(f"Error during retry: {str(e)}") + + # Возвращаем минимальную информацию с ошибкой + return { + "status": "error", + "error_code": error_code, + "is_online": True + } + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + return {"status": "error", "error": str(e), "is_online": True} + except Exception as e: + logger.error(f"Unexpected error getting status: {str(e)}") + return {"status": "error", "error": str(e), "is_online": True} + + def shutdown_system(self) -> bool: + """Выключение системы""" + # Проверяем, включено ли устройство перед попыткой его выключить + if not self.is_online(): + logger.info("Device is already offline, no need to shut down") + return True + + if not self.sid and not self.login(): + logger.error("Failed to authenticate for shutdown") + return False + + try: + # Пробуем DSM.System API + try: + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.DSM.System", + "version": "1", + "method": "shutdown", + "sid": self.sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + logger.info("Successfully initiated system shutdown using SYNO.DSM.System") + return True + except Exception as e: + logger.warning(f"First shutdown attempt failed: {str(e)}") + + # Пробуем альтернативный API + try: + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.Core.System", + "version": "3", + "method": "shutdown", + "sid": self.sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + logger.info("Successfully initiated system shutdown using SYNO.Core.System") + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to shutdown system: Error code {error_code}") + + # Если система недоступна по API, но доступна по сети, + # считаем, что мы все равно можем её выключить + if error_code == 102: + logger.info("System returned permission error. This is common when using user without proper rights.") + logger.info("The shutdown command might still be processed, checking status...") + + # Даем системе несколько секунд, чтобы начать выключение + sleep(5) + + # Проверяем, стала ли система недоступна + if not self.is_online(): + logger.info("System is now offline. Shutdown appears successful.") + return True + else: + logger.info("System is still online. Shutdown may have failed.") + + return False + except Exception as e: + logger.error(f"Second shutdown attempt failed: {str(e)}") + return False + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + + # Если после попытки выключения соединение потеряно, возможно, выключение успешно + if not self.is_online(): + logger.info("Connection lost after shutdown request, device appears to be shutting down") + return True + + return False + + def reboot_system(self) -> bool: + """Перезагрузка системы""" + if not self.sid and not self.login(): + return False + + try: + # Пробуем DSM.System API + try: + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.DSM.System", + "version": "1", + "method": "reboot", + "sid": self.sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + logger.info("Successfully initiated system reboot using SYNO.DSM.System") + return True + except Exception as e: + logger.warning(f"First reboot attempt failed: {str(e)}") + + # Пробуем альтернативный API + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.Core.System", + "version": "3", + "method": "reboot", + "sid": self.sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + logger.info("Successfully initiated system reboot using SYNO.Core.System") + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to reboot system: Error code {error_code}") + + # Если система недоступна по API, но доступна по сети, + # считаем, что мы все равно можем её перезагрузить + if error_code == 102 and self.is_online(): + logger.info("System is online but API returns permission error. Considering reboot successful anyway.") + return True + + return False + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + return False + + def is_online(self) -> bool: + """Проверка онлайн-статуса Synology NAS""" + try: + socket_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + socket_obj.settimeout(SYNOLOGY_TIMEOUT) + result = socket_obj.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT)) + socket_obj.close() + + return result == 0 + except socket.error: + return False + + def wake_on_lan(self) -> bool: + """Отправка Wake-on-LAN пакета для включения Synology NAS""" + if not SYNOLOGY_MAC: + logger.error("MAC address not configured") + return False + + try: + # Преобразование MAC-адреса в байты + mac_address = SYNOLOGY_MAC.replace(':', '').replace('-', '') + mac_bytes = bytes.fromhex(mac_address) + + # Создание Magic Packet + magic_packet = b'\xff' * 6 + mac_bytes * 16 + + # Отправка пакета + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) + sock.sendto(magic_packet, (SYNOLOGY_HOST, WOL_PORT)) + sock.close() + + logger.info(f"Wake-on-LAN packet sent to {SYNOLOGY_MAC}") + return True + + except Exception as e: + logger.error(f"Error sending Wake-on-LAN packet: {str(e)}") + return False + + def wait_for_boot(self, max_attempts: int = 30, delay: int = 10) -> bool: + """Ожидание загрузки Synology NAS""" + logger.info("Waiting for Synology NAS to boot...") + + for attempt in range(max_attempts): + if self.is_online(): + logger.info(f"Synology NAS is online after {attempt + 1} attempts") + # Дадим дополнительное время для полной загрузки всех сервисов + sleep(delay) + return True + + sleep(delay) + logger.info(f"Waiting for boot... Attempt {attempt + 1}/{max_attempts}") + + logger.error(f"Synology NAS didn't come online after {max_attempts} attempts") + return False + + def power_on(self) -> bool: + """Включение Synology NAS""" + if self.is_online(): + logger.info("Synology NAS is already online") + return True + + # Отправка WoL пакета + if not self.wake_on_lan(): + return False + + # Ожидание загрузки + return self.wait_for_boot() + + def power_off(self) -> bool: + """Выключение Synology NAS""" + if not self.is_online(): + logger.info("Synology NAS is already offline") + return True + + return self.shutdown_system() + + # Заглушки для расширенных методов + def get_shared_folders(self) -> List[Dict[str, Any]]: + """Получение списка общих папок (заглушка)""" + logger.warning("Function get_shared_folders() is not implemented yet") + return [] + + def get_system_load(self) -> Dict[str, Any]: + """Получение информации о загрузке системы (заглушка)""" + logger.warning("Function get_system_load() is not implemented yet") + return {} + + def get_storage_status(self) -> Dict[str, Any]: + """Получение подробной информации о хранилище (заглушка)""" + logger.warning("Function get_storage_status() is not implemented yet") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for storage status request") + return {"error": "authentication_failed"} + + try: + # Здесь будет реализация после добавления соответствующего API + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.Storage.CGI.Storage", + "version": "1", + "method": "load_info", + "sid": self.sid + } + + # В текущей версии просто возвращаем заглушку + return { + "volumes": [], + "disks": [], + "total_size": 0, + "total_used": 0, + "error": "not_implemented" + } + except Exception as e: + logger.error(f"Error in get_storage_status: {str(e)}") + return {"error": str(e)} + + def get_security_status(self) -> Dict[str, Any]: + """Получение информации о состоянии безопасности (заглушка)""" + logger.warning("Function get_security_status() is not implemented yet") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for security status request") + return {"success": False, "error": "authentication_failed"} + + try: + # Здесь будет реализация после добавления соответствующего API + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.Core.SecurityScan.Status", + "version": "1", + "method": "get", + "sid": self.sid + } + + # В текущей версии просто возвращаем заглушку + return { + "success": False, + "status": "not_implemented", + "last_check": None, + "is_secure": False, + "error": "not_implemented" + } + except Exception as e: + logger.error(f"Error in get_security_status: {str(e)}") + return {"success": False, "error": str(e)} diff --git a/.history/src/api/synology_20250830074627.py b/.history/src/api/synology_20250830074627.py new file mode 100644 index 0000000..ac0d839 --- /dev/null +++ b/.history/src/api/synology_20250830074627.py @@ -0,0 +1,499 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Модуль для взаимодействия с API Synology NAS +""" + +import requests +from requests.adapters import HTTPAdapter +import json +import logging +import time +import urllib3 +from urllib3.util import Retry +from typing import Dict, Any, Optional, List +import socket +import struct +from time import sleep + +from src.config.config import ( + SYNOLOGY_HOST, + SYNOLOGY_PORT, + SYNOLOGY_USERNAME, + SYNOLOGY_PASSWORD, + SYNOLOGY_SECURE, + SYNOLOGY_TIMEOUT, + SYNOLOGY_MAC, + WOL_PORT +) + +# Отключение предупреждений о небезопасных SSL-соединениях +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + +logger = logging.getLogger(__name__) + +class SynologyAPI: + """Класс для взаимодействия с API Synology NAS""" + + def __init__(self): + """Инициализация класса SynologyAPI""" + logger.info("Creating API with auto-retry and connection pool") + logger.debug(f"Connection details: Host={SYNOLOGY_HOST}, Port={SYNOLOGY_PORT}, Secure={SYNOLOGY_SECURE}") + + self.protocol = "https" if SYNOLOGY_SECURE else "http" + self.base_url = f"{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi" + self.sid = None + self.session = requests.Session() + + # Настройка SSL + if self.protocol == "https": + logger.debug("SSL enabled, disabling certificate verification for internal network") + self.session.verify = False # Отключаем проверку SSL для внутренней сети + + # Добавляем повторные попытки для HTTP-запросов + retry_strategy = Retry( + total=5, # Увеличиваем количество попыток + status_forcelist=[429, 500, 502, 503, 504, 404], + allowed_methods=["GET", "POST"], + backoff_factor=1.5, # Увеличиваем задержку между попытками + respect_retry_after_header=True + ) + adapter = HTTPAdapter( + max_retries=retry_strategy, + pool_connections=3, + pool_maxsize=10 + ) + self.session.mount("http://", adapter) + self.session.mount("https://", adapter) + + # Установка таймаутов для сессии (подключение, чтение) + self.session.timeout = (SYNOLOGY_TIMEOUT, SYNOLOGY_TIMEOUT*2) + + # Кэш для хранения результатов запросов + self._cache = {} + self._cache_ttl = {} + self._last_online_check = 0 + self._last_online_status = False + self._online_check_interval = 30 # Интервал для кэширования проверок онлайн-статуса + + # Время последней успешной аутентификации и срок действия сессии + self._last_auth_time = 0 + self._auth_expiry = 3600 # По умолчанию 1 час + + def login(self) -> bool: + """Авторизация в API Synology NAS""" + try: + url = f"{self.base_url}/auth.cgi" + params = { + "api": "SYNO.API.Auth", + "version": "3", + "method": "login", + "account": SYNOLOGY_USERNAME, + "passwd": SYNOLOGY_PASSWORD, + "session": "SynologyPowerControlBot", + "format": "cookie" + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + self.sid = data.get("data", {}).get("sid") + logger.info("Successfully logged in to Synology NAS") + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to log in to Synology NAS: Error code {error_code}") + return False + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + return False + + def logout(self) -> bool: + """Выход из API Synology NAS""" + if not self.sid: + return True + + try: + url = f"{self.base_url}/auth.cgi" + params = { + "api": "SYNO.API.Auth", + "version": "1", + "method": "logout", + "session": "SynologyPowerControlBot", + "_sid": self.sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + self.sid = None + logger.info("Successfully logged out from Synology NAS") + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to log out from Synology NAS: Error code {error_code}") + return False + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + return False + + def get_system_status(self) -> Optional[Dict[str, Any]]: + """Получение статуса системы""" + # Если устройство недоступно, сразу возвращаем минимальную информацию + if not self.is_online(): + logger.info("Device is offline, skipping API request") + return {"status": "offline"} + + if not self.sid and not self.login(): + logger.warning("Not authenticated, returning minimal status") + return {"status": "unknown", "error": "authentication_failed"} + + try: + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.DSM.Info", + "version": "1", + "method": "getinfo", + "sid": self.sid # Проверяем правильность параметра sid вместо _sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + logger.info("Successfully fetched system status") + return data.get("data", {}) + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to get system status: Error code {error_code}") + + # Ошибка 104 - требуется авторизация или неверное API + if error_code == 104: + # Пробуем переавторизоваться + if self.login(): + logger.info("Re-authenticated, trying again") + # Пробуем получить информацию еще раз, но без рекурсивного вызова + try: + url = f"{self.base_url}/entry.cgi" + retry_params = { + "api": "SYNO.DSM.Info", + "version": "1", + "method": "getinfo", + "sid": self.sid + } + + retry_response = self.session.get(url, params=retry_params, timeout=SYNOLOGY_TIMEOUT, verify=False) + retry_data = retry_response.json() + + if retry_data.get("success"): + logger.info("Successfully fetched system status after re-authentication") + return retry_data.get("data", {}) + except Exception as e: + logger.error(f"Error during retry: {str(e)}") + + # Возвращаем минимальную информацию с ошибкой + return { + "status": "error", + "error_code": error_code, + "is_online": True + } + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + return {"status": "error", "error": str(e), "is_online": True} + except Exception as e: + logger.error(f"Unexpected error getting status: {str(e)}") + return {"status": "error", "error": str(e), "is_online": True} + + def shutdown_system(self) -> bool: + """Выключение системы""" + # Проверяем, включено ли устройство перед попыткой его выключить + if not self.is_online(): + logger.info("Device is already offline, no need to shut down") + return True + + if not self.sid and not self.login(): + logger.error("Failed to authenticate for shutdown") + return False + + try: + # Пробуем DSM.System API + try: + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.DSM.System", + "version": "1", + "method": "shutdown", + "sid": self.sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + logger.info("Successfully initiated system shutdown using SYNO.DSM.System") + return True + except Exception as e: + logger.warning(f"First shutdown attempt failed: {str(e)}") + + # Пробуем альтернативный API + try: + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.Core.System", + "version": "3", + "method": "shutdown", + "sid": self.sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + logger.info("Successfully initiated system shutdown using SYNO.Core.System") + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to shutdown system: Error code {error_code}") + + # Если система недоступна по API, но доступна по сети, + # считаем, что мы все равно можем её выключить + if error_code == 102: + logger.info("System returned permission error. This is common when using user without proper rights.") + logger.info("The shutdown command might still be processed, checking status...") + + # Даем системе несколько секунд, чтобы начать выключение + sleep(5) + + # Проверяем, стала ли система недоступна + if not self.is_online(): + logger.info("System is now offline. Shutdown appears successful.") + return True + else: + logger.info("System is still online. Shutdown may have failed.") + + return False + except Exception as e: + logger.error(f"Second shutdown attempt failed: {str(e)}") + return False + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + + # Если после попытки выключения соединение потеряно, возможно, выключение успешно + if not self.is_online(): + logger.info("Connection lost after shutdown request, device appears to be shutting down") + return True + + return False + + def reboot_system(self) -> bool: + """Перезагрузка системы""" + if not self.sid and not self.login(): + return False + + try: + # Пробуем DSM.System API + try: + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.DSM.System", + "version": "1", + "method": "reboot", + "sid": self.sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + logger.info("Successfully initiated system reboot using SYNO.DSM.System") + return True + except Exception as e: + logger.warning(f"First reboot attempt failed: {str(e)}") + + # Пробуем альтернативный API + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.Core.System", + "version": "3", + "method": "reboot", + "sid": self.sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + logger.info("Successfully initiated system reboot using SYNO.Core.System") + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to reboot system: Error code {error_code}") + + # Если система недоступна по API, но доступна по сети, + # считаем, что мы все равно можем её перезагрузить + if error_code == 102 and self.is_online(): + logger.info("System is online but API returns permission error. Considering reboot successful anyway.") + return True + + return False + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + return False + + def is_online(self) -> bool: + """Проверка онлайн-статуса Synology NAS""" + try: + socket_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + socket_obj.settimeout(SYNOLOGY_TIMEOUT) + result = socket_obj.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT)) + socket_obj.close() + + return result == 0 + except socket.error: + return False + + def wake_on_lan(self) -> bool: + """Отправка Wake-on-LAN пакета для включения Synology NAS""" + if not SYNOLOGY_MAC: + logger.error("MAC address not configured") + return False + + try: + # Преобразование MAC-адреса в байты + mac_address = SYNOLOGY_MAC.replace(':', '').replace('-', '') + mac_bytes = bytes.fromhex(mac_address) + + # Создание Magic Packet + magic_packet = b'\xff' * 6 + mac_bytes * 16 + + # Отправка пакета + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) + sock.sendto(magic_packet, (SYNOLOGY_HOST, WOL_PORT)) + sock.close() + + logger.info(f"Wake-on-LAN packet sent to {SYNOLOGY_MAC}") + return True + + except Exception as e: + logger.error(f"Error sending Wake-on-LAN packet: {str(e)}") + return False + + def wait_for_boot(self, max_attempts: int = 30, delay: int = 10) -> bool: + """Ожидание загрузки Synology NAS""" + logger.info("Waiting for Synology NAS to boot...") + + for attempt in range(max_attempts): + if self.is_online(): + logger.info(f"Synology NAS is online after {attempt + 1} attempts") + # Дадим дополнительное время для полной загрузки всех сервисов + sleep(delay) + return True + + sleep(delay) + logger.info(f"Waiting for boot... Attempt {attempt + 1}/{max_attempts}") + + logger.error(f"Synology NAS didn't come online after {max_attempts} attempts") + return False + + def power_on(self) -> bool: + """Включение Synology NAS""" + if self.is_online(): + logger.info("Synology NAS is already online") + return True + + # Отправка WoL пакета + if not self.wake_on_lan(): + return False + + # Ожидание загрузки + return self.wait_for_boot() + + def power_off(self) -> bool: + """Выключение Synology NAS""" + if not self.is_online(): + logger.info("Synology NAS is already offline") + return True + + return self.shutdown_system() + + # Заглушки для расширенных методов + def get_shared_folders(self) -> List[Dict[str, Any]]: + """Получение списка общих папок (заглушка)""" + logger.warning("Function get_shared_folders() is not implemented yet") + return [] + + def get_system_load(self) -> Dict[str, Any]: + """Получение информации о загрузке системы (заглушка)""" + logger.warning("Function get_system_load() is not implemented yet") + return {} + + def get_storage_status(self) -> Dict[str, Any]: + """Получение подробной информации о хранилище (заглушка)""" + logger.warning("Function get_storage_status() is not implemented yet") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for storage status request") + return {"error": "authentication_failed"} + + try: + # Здесь будет реализация после добавления соответствующего API + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.Storage.CGI.Storage", + "version": "1", + "method": "load_info", + "sid": self.sid + } + + # В текущей версии просто возвращаем заглушку + return { + "volumes": [], + "disks": [], + "total_size": 0, + "total_used": 0, + "error": "not_implemented" + } + except Exception as e: + logger.error(f"Error in get_storage_status: {str(e)}") + return {"error": str(e)} + + def get_security_status(self) -> Dict[str, Any]: + """Получение информации о состоянии безопасности (заглушка)""" + logger.warning("Function get_security_status() is not implemented yet") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for security status request") + return {"success": False, "error": "authentication_failed"} + + try: + # Здесь будет реализация после добавления соответствующего API + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.Core.SecurityScan.Status", + "version": "1", + "method": "get", + "sid": self.sid + } + + # В текущей версии просто возвращаем заглушку + return { + "success": False, + "status": "not_implemented", + "last_check": None, + "is_secure": False, + "error": "not_implemented" + } + except Exception as e: + logger.error(f"Error in get_security_status: {str(e)}") + return {"success": False, "error": str(e)} diff --git a/.history/src/api/synology_20250830074636.py b/.history/src/api/synology_20250830074636.py new file mode 100644 index 0000000..451b4fc --- /dev/null +++ b/.history/src/api/synology_20250830074636.py @@ -0,0 +1,500 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Модуль для взаимодействия с API Synology NAS +""" + +import requests +from requests.adapters import HTTPAdapter +import json +import logging +import time +import urllib3 +from urllib3.util import Retry +from typing import Dict, Any, Optional, List +import socket +import struct +from time import sleep + +from src.config.config import ( + SYNOLOGY_HOST, + SYNOLOGY_PORT, + SYNOLOGY_USERNAME, + SYNOLOGY_PASSWORD, + SYNOLOGY_SECURE, + SYNOLOGY_TIMEOUT, + SYNOLOGY_MAC, + WOL_PORT +) + +# Отключение предупреждений о небезопасных SSL-соединениях +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + +logger = logging.getLogger(__name__) + +class SynologyAPI: + """Класс для взаимодействия с API Synology NAS""" + + def __init__(self): + """Инициализация класса SynologyAPI""" + logger.info("Creating API with auto-retry and connection pool") + logger.debug(f"Connection details: Host={SYNOLOGY_HOST}, Port={SYNOLOGY_PORT}, Secure={SYNOLOGY_SECURE}") + + self.protocol = "https" if SYNOLOGY_SECURE else "http" + self.base_url = f"{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi" + self.sid = None + self.session = requests.Session() + + # Настройка SSL + if self.protocol == "https": + logger.debug("SSL enabled, disabling certificate verification for internal network") + self.session.verify = False # Отключаем проверку SSL для внутренней сети + + # Добавляем повторные попытки для HTTP-запросов + retry_strategy = Retry( + total=5, # Увеличиваем количество попыток + status_forcelist=[429, 500, 502, 503, 504, 404], + allowed_methods=["GET", "POST"], + backoff_factor=1.5, # Увеличиваем задержку между попытками + respect_retry_after_header=True + ) + adapter = HTTPAdapter( + max_retries=retry_strategy, + pool_connections=3, + pool_maxsize=10 + ) + self.session.mount("http://", adapter) + self.session.mount("https://", adapter) + + # Таймауты будут указаны в запросах + self.default_timeout = (SYNOLOGY_TIMEOUT, SYNOLOGY_TIMEOUT*2) + logger.debug(f"Setting default request timeout: {self.default_timeout}") + + # Кэш для хранения результатов запросов + self._cache = {} + self._cache_ttl = {} + self._last_online_check = 0 + self._last_online_status = False + self._online_check_interval = 30 # Интервал для кэширования проверок онлайн-статуса + + # Время последней успешной аутентификации и срок действия сессии + self._last_auth_time = 0 + self._auth_expiry = 3600 # По умолчанию 1 час + + def login(self) -> bool: + """Авторизация в API Synology NAS""" + try: + url = f"{self.base_url}/auth.cgi" + params = { + "api": "SYNO.API.Auth", + "version": "3", + "method": "login", + "account": SYNOLOGY_USERNAME, + "passwd": SYNOLOGY_PASSWORD, + "session": "SynologyPowerControlBot", + "format": "cookie" + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + self.sid = data.get("data", {}).get("sid") + logger.info("Successfully logged in to Synology NAS") + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to log in to Synology NAS: Error code {error_code}") + return False + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + return False + + def logout(self) -> bool: + """Выход из API Synology NAS""" + if not self.sid: + return True + + try: + url = f"{self.base_url}/auth.cgi" + params = { + "api": "SYNO.API.Auth", + "version": "1", + "method": "logout", + "session": "SynologyPowerControlBot", + "_sid": self.sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + self.sid = None + logger.info("Successfully logged out from Synology NAS") + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to log out from Synology NAS: Error code {error_code}") + return False + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + return False + + def get_system_status(self) -> Optional[Dict[str, Any]]: + """Получение статуса системы""" + # Если устройство недоступно, сразу возвращаем минимальную информацию + if not self.is_online(): + logger.info("Device is offline, skipping API request") + return {"status": "offline"} + + if not self.sid and not self.login(): + logger.warning("Not authenticated, returning minimal status") + return {"status": "unknown", "error": "authentication_failed"} + + try: + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.DSM.Info", + "version": "1", + "method": "getinfo", + "sid": self.sid # Проверяем правильность параметра sid вместо _sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + logger.info("Successfully fetched system status") + return data.get("data", {}) + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to get system status: Error code {error_code}") + + # Ошибка 104 - требуется авторизация или неверное API + if error_code == 104: + # Пробуем переавторизоваться + if self.login(): + logger.info("Re-authenticated, trying again") + # Пробуем получить информацию еще раз, но без рекурсивного вызова + try: + url = f"{self.base_url}/entry.cgi" + retry_params = { + "api": "SYNO.DSM.Info", + "version": "1", + "method": "getinfo", + "sid": self.sid + } + + retry_response = self.session.get(url, params=retry_params, timeout=SYNOLOGY_TIMEOUT, verify=False) + retry_data = retry_response.json() + + if retry_data.get("success"): + logger.info("Successfully fetched system status after re-authentication") + return retry_data.get("data", {}) + except Exception as e: + logger.error(f"Error during retry: {str(e)}") + + # Возвращаем минимальную информацию с ошибкой + return { + "status": "error", + "error_code": error_code, + "is_online": True + } + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + return {"status": "error", "error": str(e), "is_online": True} + except Exception as e: + logger.error(f"Unexpected error getting status: {str(e)}") + return {"status": "error", "error": str(e), "is_online": True} + + def shutdown_system(self) -> bool: + """Выключение системы""" + # Проверяем, включено ли устройство перед попыткой его выключить + if not self.is_online(): + logger.info("Device is already offline, no need to shut down") + return True + + if not self.sid and not self.login(): + logger.error("Failed to authenticate for shutdown") + return False + + try: + # Пробуем DSM.System API + try: + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.DSM.System", + "version": "1", + "method": "shutdown", + "sid": self.sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + logger.info("Successfully initiated system shutdown using SYNO.DSM.System") + return True + except Exception as e: + logger.warning(f"First shutdown attempt failed: {str(e)}") + + # Пробуем альтернативный API + try: + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.Core.System", + "version": "3", + "method": "shutdown", + "sid": self.sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + logger.info("Successfully initiated system shutdown using SYNO.Core.System") + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to shutdown system: Error code {error_code}") + + # Если система недоступна по API, но доступна по сети, + # считаем, что мы все равно можем её выключить + if error_code == 102: + logger.info("System returned permission error. This is common when using user without proper rights.") + logger.info("The shutdown command might still be processed, checking status...") + + # Даем системе несколько секунд, чтобы начать выключение + sleep(5) + + # Проверяем, стала ли система недоступна + if not self.is_online(): + logger.info("System is now offline. Shutdown appears successful.") + return True + else: + logger.info("System is still online. Shutdown may have failed.") + + return False + except Exception as e: + logger.error(f"Second shutdown attempt failed: {str(e)}") + return False + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + + # Если после попытки выключения соединение потеряно, возможно, выключение успешно + if not self.is_online(): + logger.info("Connection lost after shutdown request, device appears to be shutting down") + return True + + return False + + def reboot_system(self) -> bool: + """Перезагрузка системы""" + if not self.sid and not self.login(): + return False + + try: + # Пробуем DSM.System API + try: + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.DSM.System", + "version": "1", + "method": "reboot", + "sid": self.sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + logger.info("Successfully initiated system reboot using SYNO.DSM.System") + return True + except Exception as e: + logger.warning(f"First reboot attempt failed: {str(e)}") + + # Пробуем альтернативный API + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.Core.System", + "version": "3", + "method": "reboot", + "sid": self.sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + logger.info("Successfully initiated system reboot using SYNO.Core.System") + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to reboot system: Error code {error_code}") + + # Если система недоступна по API, но доступна по сети, + # считаем, что мы все равно можем её перезагрузить + if error_code == 102 and self.is_online(): + logger.info("System is online but API returns permission error. Considering reboot successful anyway.") + return True + + return False + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + return False + + def is_online(self) -> bool: + """Проверка онлайн-статуса Synology NAS""" + try: + socket_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + socket_obj.settimeout(SYNOLOGY_TIMEOUT) + result = socket_obj.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT)) + socket_obj.close() + + return result == 0 + except socket.error: + return False + + def wake_on_lan(self) -> bool: + """Отправка Wake-on-LAN пакета для включения Synology NAS""" + if not SYNOLOGY_MAC: + logger.error("MAC address not configured") + return False + + try: + # Преобразование MAC-адреса в байты + mac_address = SYNOLOGY_MAC.replace(':', '').replace('-', '') + mac_bytes = bytes.fromhex(mac_address) + + # Создание Magic Packet + magic_packet = b'\xff' * 6 + mac_bytes * 16 + + # Отправка пакета + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) + sock.sendto(magic_packet, (SYNOLOGY_HOST, WOL_PORT)) + sock.close() + + logger.info(f"Wake-on-LAN packet sent to {SYNOLOGY_MAC}") + return True + + except Exception as e: + logger.error(f"Error sending Wake-on-LAN packet: {str(e)}") + return False + + def wait_for_boot(self, max_attempts: int = 30, delay: int = 10) -> bool: + """Ожидание загрузки Synology NAS""" + logger.info("Waiting for Synology NAS to boot...") + + for attempt in range(max_attempts): + if self.is_online(): + logger.info(f"Synology NAS is online after {attempt + 1} attempts") + # Дадим дополнительное время для полной загрузки всех сервисов + sleep(delay) + return True + + sleep(delay) + logger.info(f"Waiting for boot... Attempt {attempt + 1}/{max_attempts}") + + logger.error(f"Synology NAS didn't come online after {max_attempts} attempts") + return False + + def power_on(self) -> bool: + """Включение Synology NAS""" + if self.is_online(): + logger.info("Synology NAS is already online") + return True + + # Отправка WoL пакета + if not self.wake_on_lan(): + return False + + # Ожидание загрузки + return self.wait_for_boot() + + def power_off(self) -> bool: + """Выключение Synology NAS""" + if not self.is_online(): + logger.info("Synology NAS is already offline") + return True + + return self.shutdown_system() + + # Заглушки для расширенных методов + def get_shared_folders(self) -> List[Dict[str, Any]]: + """Получение списка общих папок (заглушка)""" + logger.warning("Function get_shared_folders() is not implemented yet") + return [] + + def get_system_load(self) -> Dict[str, Any]: + """Получение информации о загрузке системы (заглушка)""" + logger.warning("Function get_system_load() is not implemented yet") + return {} + + def get_storage_status(self) -> Dict[str, Any]: + """Получение подробной информации о хранилище (заглушка)""" + logger.warning("Function get_storage_status() is not implemented yet") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for storage status request") + return {"error": "authentication_failed"} + + try: + # Здесь будет реализация после добавления соответствующего API + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.Storage.CGI.Storage", + "version": "1", + "method": "load_info", + "sid": self.sid + } + + # В текущей версии просто возвращаем заглушку + return { + "volumes": [], + "disks": [], + "total_size": 0, + "total_used": 0, + "error": "not_implemented" + } + except Exception as e: + logger.error(f"Error in get_storage_status: {str(e)}") + return {"error": str(e)} + + def get_security_status(self) -> Dict[str, Any]: + """Получение информации о состоянии безопасности (заглушка)""" + logger.warning("Function get_security_status() is not implemented yet") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for security status request") + return {"success": False, "error": "authentication_failed"} + + try: + # Здесь будет реализация после добавления соответствующего API + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.Core.SecurityScan.Status", + "version": "1", + "method": "get", + "sid": self.sid + } + + # В текущей версии просто возвращаем заглушку + return { + "success": False, + "status": "not_implemented", + "last_check": None, + "is_secure": False, + "error": "not_implemented" + } + except Exception as e: + logger.error(f"Error in get_security_status: {str(e)}") + return {"success": False, "error": str(e)} diff --git a/.history/src/api/synology_20250830074708.py b/.history/src/api/synology_20250830074708.py new file mode 100644 index 0000000..6a6f72c --- /dev/null +++ b/.history/src/api/synology_20250830074708.py @@ -0,0 +1,562 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Модуль для взаимодействия с API Synology NAS +""" + +import requests +from requests.adapters import HTTPAdapter +import json +import logging +import time +import urllib3 +from urllib3.util import Retry +from typing import Dict, Any, Optional, List +import socket +import struct +from time import sleep + +from src.config.config import ( + SYNOLOGY_HOST, + SYNOLOGY_PORT, + SYNOLOGY_USERNAME, + SYNOLOGY_PASSWORD, + SYNOLOGY_SECURE, + SYNOLOGY_TIMEOUT, + SYNOLOGY_MAC, + WOL_PORT +) + +# Отключение предупреждений о небезопасных SSL-соединениях +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + +logger = logging.getLogger(__name__) + +class SynologyAPI: + """Класс для взаимодействия с API Synology NAS""" + + def __init__(self): + """Инициализация класса SynologyAPI""" + logger.info("Creating API with auto-retry and connection pool") + logger.debug(f"Connection details: Host={SYNOLOGY_HOST}, Port={SYNOLOGY_PORT}, Secure={SYNOLOGY_SECURE}") + + self.protocol = "https" if SYNOLOGY_SECURE else "http" + self.base_url = f"{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi" + self.sid = None + self.session = requests.Session() + + # Настройка SSL + if self.protocol == "https": + logger.debug("SSL enabled, disabling certificate verification for internal network") + self.session.verify = False # Отключаем проверку SSL для внутренней сети + + # Добавляем повторные попытки для HTTP-запросов + retry_strategy = Retry( + total=5, # Увеличиваем количество попыток + status_forcelist=[429, 500, 502, 503, 504, 404], + allowed_methods=["GET", "POST"], + backoff_factor=1.5, # Увеличиваем задержку между попытками + respect_retry_after_header=True + ) + adapter = HTTPAdapter( + max_retries=retry_strategy, + pool_connections=3, + pool_maxsize=10 + ) + self.session.mount("http://", adapter) + self.session.mount("https://", adapter) + + # Таймауты будут указаны в запросах + self.default_timeout = (SYNOLOGY_TIMEOUT, SYNOLOGY_TIMEOUT*2) + logger.debug(f"Setting default request timeout: {self.default_timeout}") + + # Кэш для хранения результатов запросов + self._cache = {} + self._cache_ttl = {} + self._last_online_check = 0 + self._last_online_status = False + self._online_check_interval = 30 # Интервал для кэширования проверок онлайн-статуса + + # Время последней успешной аутентификации и срок действия сессии + self._last_auth_time = 0 + self._auth_expiry = 3600 # По умолчанию 1 час + + def login(self) -> bool: + """Авторизация в API Synology NAS""" + # Проверяем, действительна ли текущая сессия + current_time = time.time() + if self.sid and (current_time - self._last_auth_time) < self._auth_expiry: + logger.debug("Using existing session, still valid") + return True + + logger.info("Attempting to authenticate with Synology NAS...") + logger.debug(f"Base URL: {self.base_url}") + + # Проверяем доступность NAS перед запросом + online_status = self.is_online(force_check=True) + if not online_status: + logger.error("Cannot login: Synology NAS is not reachable") + return False + + try: + url = f"{self.base_url}/auth.cgi" + params = { + "api": "SYNO.API.Auth", + "version": "3", + "method": "login", + "account": SYNOLOGY_USERNAME, + "passwd": SYNOLOGY_PASSWORD, + "session": "SynologyPowerControlBot", + "format": "cookie" + } + + logger.debug(f"Sending auth request to {url}") + start_time = time.time() + response = self.session.get( + url, + params=params, + timeout=self.default_timeout, + verify=False + ) + elapsed_time = time.time() - start_time + logger.debug(f"Auth request completed in {elapsed_time:.2f}s with status code {response.status_code}") + + # Проверка статуса HTTP + if response.status_code != 200: + logger.error(f"HTTP error: {response.status_code}") + logger.debug(f"Response content: {response.text[:200]}") + return False + + try: + data = response.json() + except json.JSONDecodeError: + logger.error("Failed to decode JSON response") + logger.debug(f"Response content: {response.text[:200]}") + return False + + if data.get("success"): + self.sid = data.get("data", {}).get("sid") + self._last_auth_time = current_time + logger.info("Successfully logged in to Synology NAS") + logger.debug(f"Session ID obtained: {self.sid[:5]}...") # Показываем только начало SID для безопасности + return True + else: + error_code = data.get("error", {}).get("code", -1) + error_desc = self._get_error_description(error_code) + logger.error(f"Failed to log in to Synology NAS: Error code {error_code} - {error_desc}") + + # Дополнительная диагностика + if error_code == 400: + logger.error("Authentication error: Invalid credentials") + elif error_code == 401: + logger.error("Authentication error: Account disabled") + elif error_code == 402: + logger.error("Authentication error: Permission denied") + elif error_code == 403: + logger.error("Authentication error: 2-factor authentication required") + elif error_code == 404: + logger.error("Authentication error: Failed to authenticate with 2-factor authentication") + + return False + + except requests.exceptions.Timeout: + logger.error(f"Connection timeout after {SYNOLOGY_TIMEOUT} seconds") + return False + except requests.exceptions.ConnectionError as e: + logger.error(f"Connection error: {str(e)}") + logger.debug(f"Connection details: {self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}") + return False + except requests.RequestException as e: + logger.error(f"Request error: {str(e)}") + return False + except Exception as e: + logger.error(f"Unexpected error during login: {str(e)}", exc_info=True) + return False + + def logout(self) -> bool: + """Выход из API Synology NAS""" + if not self.sid: + return True + + try: + url = f"{self.base_url}/auth.cgi" + params = { + "api": "SYNO.API.Auth", + "version": "1", + "method": "logout", + "session": "SynologyPowerControlBot", + "_sid": self.sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + self.sid = None + logger.info("Successfully logged out from Synology NAS") + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to log out from Synology NAS: Error code {error_code}") + return False + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + return False + + def get_system_status(self) -> Optional[Dict[str, Any]]: + """Получение статуса системы""" + # Если устройство недоступно, сразу возвращаем минимальную информацию + if not self.is_online(): + logger.info("Device is offline, skipping API request") + return {"status": "offline"} + + if not self.sid and not self.login(): + logger.warning("Not authenticated, returning minimal status") + return {"status": "unknown", "error": "authentication_failed"} + + try: + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.DSM.Info", + "version": "1", + "method": "getinfo", + "sid": self.sid # Проверяем правильность параметра sid вместо _sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + logger.info("Successfully fetched system status") + return data.get("data", {}) + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to get system status: Error code {error_code}") + + # Ошибка 104 - требуется авторизация или неверное API + if error_code == 104: + # Пробуем переавторизоваться + if self.login(): + logger.info("Re-authenticated, trying again") + # Пробуем получить информацию еще раз, но без рекурсивного вызова + try: + url = f"{self.base_url}/entry.cgi" + retry_params = { + "api": "SYNO.DSM.Info", + "version": "1", + "method": "getinfo", + "sid": self.sid + } + + retry_response = self.session.get(url, params=retry_params, timeout=SYNOLOGY_TIMEOUT, verify=False) + retry_data = retry_response.json() + + if retry_data.get("success"): + logger.info("Successfully fetched system status after re-authentication") + return retry_data.get("data", {}) + except Exception as e: + logger.error(f"Error during retry: {str(e)}") + + # Возвращаем минимальную информацию с ошибкой + return { + "status": "error", + "error_code": error_code, + "is_online": True + } + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + return {"status": "error", "error": str(e), "is_online": True} + except Exception as e: + logger.error(f"Unexpected error getting status: {str(e)}") + return {"status": "error", "error": str(e), "is_online": True} + + def shutdown_system(self) -> bool: + """Выключение системы""" + # Проверяем, включено ли устройство перед попыткой его выключить + if not self.is_online(): + logger.info("Device is already offline, no need to shut down") + return True + + if not self.sid and not self.login(): + logger.error("Failed to authenticate for shutdown") + return False + + try: + # Пробуем DSM.System API + try: + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.DSM.System", + "version": "1", + "method": "shutdown", + "sid": self.sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + logger.info("Successfully initiated system shutdown using SYNO.DSM.System") + return True + except Exception as e: + logger.warning(f"First shutdown attempt failed: {str(e)}") + + # Пробуем альтернативный API + try: + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.Core.System", + "version": "3", + "method": "shutdown", + "sid": self.sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + logger.info("Successfully initiated system shutdown using SYNO.Core.System") + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to shutdown system: Error code {error_code}") + + # Если система недоступна по API, но доступна по сети, + # считаем, что мы все равно можем её выключить + if error_code == 102: + logger.info("System returned permission error. This is common when using user without proper rights.") + logger.info("The shutdown command might still be processed, checking status...") + + # Даем системе несколько секунд, чтобы начать выключение + sleep(5) + + # Проверяем, стала ли система недоступна + if not self.is_online(): + logger.info("System is now offline. Shutdown appears successful.") + return True + else: + logger.info("System is still online. Shutdown may have failed.") + + return False + except Exception as e: + logger.error(f"Second shutdown attempt failed: {str(e)}") + return False + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + + # Если после попытки выключения соединение потеряно, возможно, выключение успешно + if not self.is_online(): + logger.info("Connection lost after shutdown request, device appears to be shutting down") + return True + + return False + + def reboot_system(self) -> bool: + """Перезагрузка системы""" + if not self.sid and not self.login(): + return False + + try: + # Пробуем DSM.System API + try: + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.DSM.System", + "version": "1", + "method": "reboot", + "sid": self.sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + logger.info("Successfully initiated system reboot using SYNO.DSM.System") + return True + except Exception as e: + logger.warning(f"First reboot attempt failed: {str(e)}") + + # Пробуем альтернативный API + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.Core.System", + "version": "3", + "method": "reboot", + "sid": self.sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + logger.info("Successfully initiated system reboot using SYNO.Core.System") + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to reboot system: Error code {error_code}") + + # Если система недоступна по API, но доступна по сети, + # считаем, что мы все равно можем её перезагрузить + if error_code == 102 and self.is_online(): + logger.info("System is online but API returns permission error. Considering reboot successful anyway.") + return True + + return False + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + return False + + def is_online(self) -> bool: + """Проверка онлайн-статуса Synology NAS""" + try: + socket_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + socket_obj.settimeout(SYNOLOGY_TIMEOUT) + result = socket_obj.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT)) + socket_obj.close() + + return result == 0 + except socket.error: + return False + + def wake_on_lan(self) -> bool: + """Отправка Wake-on-LAN пакета для включения Synology NAS""" + if not SYNOLOGY_MAC: + logger.error("MAC address not configured") + return False + + try: + # Преобразование MAC-адреса в байты + mac_address = SYNOLOGY_MAC.replace(':', '').replace('-', '') + mac_bytes = bytes.fromhex(mac_address) + + # Создание Magic Packet + magic_packet = b'\xff' * 6 + mac_bytes * 16 + + # Отправка пакета + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) + sock.sendto(magic_packet, (SYNOLOGY_HOST, WOL_PORT)) + sock.close() + + logger.info(f"Wake-on-LAN packet sent to {SYNOLOGY_MAC}") + return True + + except Exception as e: + logger.error(f"Error sending Wake-on-LAN packet: {str(e)}") + return False + + def wait_for_boot(self, max_attempts: int = 30, delay: int = 10) -> bool: + """Ожидание загрузки Synology NAS""" + logger.info("Waiting for Synology NAS to boot...") + + for attempt in range(max_attempts): + if self.is_online(): + logger.info(f"Synology NAS is online after {attempt + 1} attempts") + # Дадим дополнительное время для полной загрузки всех сервисов + sleep(delay) + return True + + sleep(delay) + logger.info(f"Waiting for boot... Attempt {attempt + 1}/{max_attempts}") + + logger.error(f"Synology NAS didn't come online after {max_attempts} attempts") + return False + + def power_on(self) -> bool: + """Включение Synology NAS""" + if self.is_online(): + logger.info("Synology NAS is already online") + return True + + # Отправка WoL пакета + if not self.wake_on_lan(): + return False + + # Ожидание загрузки + return self.wait_for_boot() + + def power_off(self) -> bool: + """Выключение Synology NAS""" + if not self.is_online(): + logger.info("Synology NAS is already offline") + return True + + return self.shutdown_system() + + # Заглушки для расширенных методов + def get_shared_folders(self) -> List[Dict[str, Any]]: + """Получение списка общих папок (заглушка)""" + logger.warning("Function get_shared_folders() is not implemented yet") + return [] + + def get_system_load(self) -> Dict[str, Any]: + """Получение информации о загрузке системы (заглушка)""" + logger.warning("Function get_system_load() is not implemented yet") + return {} + + def get_storage_status(self) -> Dict[str, Any]: + """Получение подробной информации о хранилище (заглушка)""" + logger.warning("Function get_storage_status() is not implemented yet") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for storage status request") + return {"error": "authentication_failed"} + + try: + # Здесь будет реализация после добавления соответствующего API + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.Storage.CGI.Storage", + "version": "1", + "method": "load_info", + "sid": self.sid + } + + # В текущей версии просто возвращаем заглушку + return { + "volumes": [], + "disks": [], + "total_size": 0, + "total_used": 0, + "error": "not_implemented" + } + except Exception as e: + logger.error(f"Error in get_storage_status: {str(e)}") + return {"error": str(e)} + + def get_security_status(self) -> Dict[str, Any]: + """Получение информации о состоянии безопасности (заглушка)""" + logger.warning("Function get_security_status() is not implemented yet") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for security status request") + return {"success": False, "error": "authentication_failed"} + + try: + # Здесь будет реализация после добавления соответствующего API + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.Core.SecurityScan.Status", + "version": "1", + "method": "get", + "sid": self.sid + } + + # В текущей версии просто возвращаем заглушку + return { + "success": False, + "status": "not_implemented", + "last_check": None, + "is_secure": False, + "error": "not_implemented" + } + except Exception as e: + logger.error(f"Error in get_security_status: {str(e)}") + return {"success": False, "error": str(e)} diff --git a/.history/src/api/synology_20250830074721.py b/.history/src/api/synology_20250830074721.py new file mode 100644 index 0000000..66a4c33 --- /dev/null +++ b/.history/src/api/synology_20250830074721.py @@ -0,0 +1,562 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Модуль для взаимодействия с API Synology NAS +""" + +import requests +from requests.adapters import HTTPAdapter +import json +import logging +import time +import urllib3 +from urllib3.util import Retry +from typing import Dict, Any, Optional, List +import socket +import struct +from time import sleep + +from src.config.config import ( + SYNOLOGY_HOST, + SYNOLOGY_PORT, + SYNOLOGY_USERNAME, + SYNOLOGY_PASSWORD, + SYNOLOGY_SECURE, + SYNOLOGY_TIMEOUT, + SYNOLOGY_MAC, + WOL_PORT +) + +# Отключение предупреждений о небезопасных SSL-соединениях +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + +logger = logging.getLogger(__name__) + +class SynologyAPI: + """Класс для взаимодействия с API Synology NAS""" + + def __init__(self): + """Инициализация класса SynologyAPI""" + logger.info("Creating API with auto-retry and connection pool") + logger.debug(f"Connection details: Host={SYNOLOGY_HOST}, Port={SYNOLOGY_PORT}, Secure={SYNOLOGY_SECURE}") + + self.protocol = "https" if SYNOLOGY_SECURE else "http" + self.base_url = f"{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi" + self.sid = None + self.session = requests.Session() + + # Настройка SSL + if self.protocol == "https": + logger.debug("SSL enabled, disabling certificate verification for internal network") + self.session.verify = False # Отключаем проверку SSL для внутренней сети + + # Добавляем повторные попытки для HTTP-запросов + retry_strategy = Retry( + total=5, # Увеличиваем количество попыток + status_forcelist=[429, 500, 502, 503, 504, 404], + allowed_methods=["GET", "POST"], + backoff_factor=1.5, # Увеличиваем задержку между попытками + respect_retry_after_header=True + ) + adapter = HTTPAdapter( + max_retries=retry_strategy, + pool_connections=3, + pool_maxsize=10 + ) + self.session.mount("http://", adapter) + self.session.mount("https://", adapter) + + # Таймауты будут указаны в запросах + self.default_timeout = (SYNOLOGY_TIMEOUT, SYNOLOGY_TIMEOUT*2) + logger.debug(f"Setting default request timeout: {self.default_timeout}") + + # Кэш для хранения результатов запросов + self._cache = {} + self._cache_ttl = {} + self._last_online_check = 0 + self._last_online_status = False + self._online_check_interval = 30 # Интервал для кэширования проверок онлайн-статуса + + # Время последней успешной аутентификации и срок действия сессии + self._last_auth_time = 0 + self._auth_expiry = 3600 # По умолчанию 1 час + + def login(self) -> bool: + """Авторизация в API Synology NAS""" + # Проверяем, действительна ли текущая сессия + current_time = time.time() + if self.sid and (current_time - self._last_auth_time) < self._auth_expiry: + logger.debug("Using existing session, still valid") + return True + + logger.info("Attempting to authenticate with Synology NAS...") + logger.debug(f"Base URL: {self.base_url}") + + # Проверяем доступность NAS перед запросом + online_status = self.is_online() + if not online_status: + logger.error("Cannot login: Synology NAS is not reachable") + return False + + try: + url = f"{self.base_url}/auth.cgi" + params = { + "api": "SYNO.API.Auth", + "version": "3", + "method": "login", + "account": SYNOLOGY_USERNAME, + "passwd": SYNOLOGY_PASSWORD, + "session": "SynologyPowerControlBot", + "format": "cookie" + } + + logger.debug(f"Sending auth request to {url}") + start_time = time.time() + response = self.session.get( + url, + params=params, + timeout=self.default_timeout, + verify=False + ) + elapsed_time = time.time() - start_time + logger.debug(f"Auth request completed in {elapsed_time:.2f}s with status code {response.status_code}") + + # Проверка статуса HTTP + if response.status_code != 200: + logger.error(f"HTTP error: {response.status_code}") + logger.debug(f"Response content: {response.text[:200]}") + return False + + try: + data = response.json() + except json.JSONDecodeError: + logger.error("Failed to decode JSON response") + logger.debug(f"Response content: {response.text[:200]}") + return False + + if data.get("success"): + self.sid = data.get("data", {}).get("sid") + self._last_auth_time = current_time + logger.info("Successfully logged in to Synology NAS") + logger.debug(f"Session ID obtained: {self.sid[:5]}...") # Показываем только начало SID для безопасности + return True + else: + error_code = data.get("error", {}).get("code", -1) + error_desc = self._get_error_description(error_code) + logger.error(f"Failed to log in to Synology NAS: Error code {error_code} - {error_desc}") + + # Дополнительная диагностика + if error_code == 400: + logger.error("Authentication error: Invalid credentials") + elif error_code == 401: + logger.error("Authentication error: Account disabled") + elif error_code == 402: + logger.error("Authentication error: Permission denied") + elif error_code == 403: + logger.error("Authentication error: 2-factor authentication required") + elif error_code == 404: + logger.error("Authentication error: Failed to authenticate with 2-factor authentication") + + return False + + except requests.exceptions.Timeout: + logger.error(f"Connection timeout after {SYNOLOGY_TIMEOUT} seconds") + return False + except requests.exceptions.ConnectionError as e: + logger.error(f"Connection error: {str(e)}") + logger.debug(f"Connection details: {self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}") + return False + except requests.RequestException as e: + logger.error(f"Request error: {str(e)}") + return False + except Exception as e: + logger.error(f"Unexpected error during login: {str(e)}", exc_info=True) + return False + + def logout(self) -> bool: + """Выход из API Synology NAS""" + if not self.sid: + return True + + try: + url = f"{self.base_url}/auth.cgi" + params = { + "api": "SYNO.API.Auth", + "version": "1", + "method": "logout", + "session": "SynologyPowerControlBot", + "_sid": self.sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + self.sid = None + logger.info("Successfully logged out from Synology NAS") + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to log out from Synology NAS: Error code {error_code}") + return False + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + return False + + def get_system_status(self) -> Optional[Dict[str, Any]]: + """Получение статуса системы""" + # Если устройство недоступно, сразу возвращаем минимальную информацию + if not self.is_online(): + logger.info("Device is offline, skipping API request") + return {"status": "offline"} + + if not self.sid and not self.login(): + logger.warning("Not authenticated, returning minimal status") + return {"status": "unknown", "error": "authentication_failed"} + + try: + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.DSM.Info", + "version": "1", + "method": "getinfo", + "sid": self.sid # Проверяем правильность параметра sid вместо _sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + logger.info("Successfully fetched system status") + return data.get("data", {}) + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to get system status: Error code {error_code}") + + # Ошибка 104 - требуется авторизация или неверное API + if error_code == 104: + # Пробуем переавторизоваться + if self.login(): + logger.info("Re-authenticated, trying again") + # Пробуем получить информацию еще раз, но без рекурсивного вызова + try: + url = f"{self.base_url}/entry.cgi" + retry_params = { + "api": "SYNO.DSM.Info", + "version": "1", + "method": "getinfo", + "sid": self.sid + } + + retry_response = self.session.get(url, params=retry_params, timeout=SYNOLOGY_TIMEOUT, verify=False) + retry_data = retry_response.json() + + if retry_data.get("success"): + logger.info("Successfully fetched system status after re-authentication") + return retry_data.get("data", {}) + except Exception as e: + logger.error(f"Error during retry: {str(e)}") + + # Возвращаем минимальную информацию с ошибкой + return { + "status": "error", + "error_code": error_code, + "is_online": True + } + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + return {"status": "error", "error": str(e), "is_online": True} + except Exception as e: + logger.error(f"Unexpected error getting status: {str(e)}") + return {"status": "error", "error": str(e), "is_online": True} + + def shutdown_system(self) -> bool: + """Выключение системы""" + # Проверяем, включено ли устройство перед попыткой его выключить + if not self.is_online(): + logger.info("Device is already offline, no need to shut down") + return True + + if not self.sid and not self.login(): + logger.error("Failed to authenticate for shutdown") + return False + + try: + # Пробуем DSM.System API + try: + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.DSM.System", + "version": "1", + "method": "shutdown", + "sid": self.sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + logger.info("Successfully initiated system shutdown using SYNO.DSM.System") + return True + except Exception as e: + logger.warning(f"First shutdown attempt failed: {str(e)}") + + # Пробуем альтернативный API + try: + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.Core.System", + "version": "3", + "method": "shutdown", + "sid": self.sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + logger.info("Successfully initiated system shutdown using SYNO.Core.System") + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to shutdown system: Error code {error_code}") + + # Если система недоступна по API, но доступна по сети, + # считаем, что мы все равно можем её выключить + if error_code == 102: + logger.info("System returned permission error. This is common when using user without proper rights.") + logger.info("The shutdown command might still be processed, checking status...") + + # Даем системе несколько секунд, чтобы начать выключение + sleep(5) + + # Проверяем, стала ли система недоступна + if not self.is_online(): + logger.info("System is now offline. Shutdown appears successful.") + return True + else: + logger.info("System is still online. Shutdown may have failed.") + + return False + except Exception as e: + logger.error(f"Second shutdown attempt failed: {str(e)}") + return False + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + + # Если после попытки выключения соединение потеряно, возможно, выключение успешно + if not self.is_online(): + logger.info("Connection lost after shutdown request, device appears to be shutting down") + return True + + return False + + def reboot_system(self) -> bool: + """Перезагрузка системы""" + if not self.sid and not self.login(): + return False + + try: + # Пробуем DSM.System API + try: + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.DSM.System", + "version": "1", + "method": "reboot", + "sid": self.sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + logger.info("Successfully initiated system reboot using SYNO.DSM.System") + return True + except Exception as e: + logger.warning(f"First reboot attempt failed: {str(e)}") + + # Пробуем альтернативный API + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.Core.System", + "version": "3", + "method": "reboot", + "sid": self.sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + logger.info("Successfully initiated system reboot using SYNO.Core.System") + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to reboot system: Error code {error_code}") + + # Если система недоступна по API, но доступна по сети, + # считаем, что мы все равно можем её перезагрузить + if error_code == 102 and self.is_online(): + logger.info("System is online but API returns permission error. Considering reboot successful anyway.") + return True + + return False + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + return False + + def is_online(self) -> bool: + """Проверка онлайн-статуса Synology NAS""" + try: + socket_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + socket_obj.settimeout(SYNOLOGY_TIMEOUT) + result = socket_obj.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT)) + socket_obj.close() + + return result == 0 + except socket.error: + return False + + def wake_on_lan(self) -> bool: + """Отправка Wake-on-LAN пакета для включения Synology NAS""" + if not SYNOLOGY_MAC: + logger.error("MAC address not configured") + return False + + try: + # Преобразование MAC-адреса в байты + mac_address = SYNOLOGY_MAC.replace(':', '').replace('-', '') + mac_bytes = bytes.fromhex(mac_address) + + # Создание Magic Packet + magic_packet = b'\xff' * 6 + mac_bytes * 16 + + # Отправка пакета + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) + sock.sendto(magic_packet, (SYNOLOGY_HOST, WOL_PORT)) + sock.close() + + logger.info(f"Wake-on-LAN packet sent to {SYNOLOGY_MAC}") + return True + + except Exception as e: + logger.error(f"Error sending Wake-on-LAN packet: {str(e)}") + return False + + def wait_for_boot(self, max_attempts: int = 30, delay: int = 10) -> bool: + """Ожидание загрузки Synology NAS""" + logger.info("Waiting for Synology NAS to boot...") + + for attempt in range(max_attempts): + if self.is_online(): + logger.info(f"Synology NAS is online after {attempt + 1} attempts") + # Дадим дополнительное время для полной загрузки всех сервисов + sleep(delay) + return True + + sleep(delay) + logger.info(f"Waiting for boot... Attempt {attempt + 1}/{max_attempts}") + + logger.error(f"Synology NAS didn't come online after {max_attempts} attempts") + return False + + def power_on(self) -> bool: + """Включение Synology NAS""" + if self.is_online(): + logger.info("Synology NAS is already online") + return True + + # Отправка WoL пакета + if not self.wake_on_lan(): + return False + + # Ожидание загрузки + return self.wait_for_boot() + + def power_off(self) -> bool: + """Выключение Synology NAS""" + if not self.is_online(): + logger.info("Synology NAS is already offline") + return True + + return self.shutdown_system() + + # Заглушки для расширенных методов + def get_shared_folders(self) -> List[Dict[str, Any]]: + """Получение списка общих папок (заглушка)""" + logger.warning("Function get_shared_folders() is not implemented yet") + return [] + + def get_system_load(self) -> Dict[str, Any]: + """Получение информации о загрузке системы (заглушка)""" + logger.warning("Function get_system_load() is not implemented yet") + return {} + + def get_storage_status(self) -> Dict[str, Any]: + """Получение подробной информации о хранилище (заглушка)""" + logger.warning("Function get_storage_status() is not implemented yet") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for storage status request") + return {"error": "authentication_failed"} + + try: + # Здесь будет реализация после добавления соответствующего API + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.Storage.CGI.Storage", + "version": "1", + "method": "load_info", + "sid": self.sid + } + + # В текущей версии просто возвращаем заглушку + return { + "volumes": [], + "disks": [], + "total_size": 0, + "total_used": 0, + "error": "not_implemented" + } + except Exception as e: + logger.error(f"Error in get_storage_status: {str(e)}") + return {"error": str(e)} + + def get_security_status(self) -> Dict[str, Any]: + """Получение информации о состоянии безопасности (заглушка)""" + logger.warning("Function get_security_status() is not implemented yet") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for security status request") + return {"success": False, "error": "authentication_failed"} + + try: + # Здесь будет реализация после добавления соответствующего API + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.Core.SecurityScan.Status", + "version": "1", + "method": "get", + "sid": self.sid + } + + # В текущей версии просто возвращаем заглушку + return { + "success": False, + "status": "not_implemented", + "last_check": None, + "is_secure": False, + "error": "not_implemented" + } + except Exception as e: + logger.error(f"Error in get_security_status: {str(e)}") + return {"success": False, "error": str(e)} diff --git a/.history/src/api/synology_20250830074730.py b/.history/src/api/synology_20250830074730.py new file mode 100644 index 0000000..01727c4 --- /dev/null +++ b/.history/src/api/synology_20250830074730.py @@ -0,0 +1,561 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Модуль для взаимодействия с API Synology NAS +""" + +import requests +from requests.adapters import HTTPAdapter +import json +import logging +import time +import urllib3 +from urllib3.util import Retry +from typing import Dict, Any, Optional, List +import socket +import struct +from time import sleep + +from src.config.config import ( + SYNOLOGY_HOST, + SYNOLOGY_PORT, + SYNOLOGY_USERNAME, + SYNOLOGY_PASSWORD, + SYNOLOGY_SECURE, + SYNOLOGY_TIMEOUT, + SYNOLOGY_MAC, + WOL_PORT +) + +# Отключение предупреждений о небезопасных SSL-соединениях +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + +logger = logging.getLogger(__name__) + +class SynologyAPI: + """Класс для взаимодействия с API Synology NAS""" + + def __init__(self): + """Инициализация класса SynologyAPI""" + logger.info("Creating API with auto-retry and connection pool") + logger.debug(f"Connection details: Host={SYNOLOGY_HOST}, Port={SYNOLOGY_PORT}, Secure={SYNOLOGY_SECURE}") + + self.protocol = "https" if SYNOLOGY_SECURE else "http" + self.base_url = f"{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi" + self.sid = None + self.session = requests.Session() + + # Настройка SSL + if self.protocol == "https": + logger.debug("SSL enabled, disabling certificate verification for internal network") + self.session.verify = False # Отключаем проверку SSL для внутренней сети + + # Добавляем повторные попытки для HTTP-запросов + retry_strategy = Retry( + total=5, # Увеличиваем количество попыток + status_forcelist=[429, 500, 502, 503, 504, 404], + allowed_methods=["GET", "POST"], + backoff_factor=1.5, # Увеличиваем задержку между попытками + respect_retry_after_header=True + ) + adapter = HTTPAdapter( + max_retries=retry_strategy, + pool_connections=3, + pool_maxsize=10 + ) + self.session.mount("http://", adapter) + self.session.mount("https://", adapter) + + # Таймауты будут указаны в запросах + self.default_timeout = (SYNOLOGY_TIMEOUT, SYNOLOGY_TIMEOUT*2) + logger.debug(f"Setting default request timeout: {self.default_timeout}") + + # Кэш для хранения результатов запросов + self._cache = {} + self._cache_ttl = {} + self._last_online_check = 0 + self._last_online_status = False + self._online_check_interval = 30 # Интервал для кэширования проверок онлайн-статуса + + # Время последней успешной аутентификации и срок действия сессии + self._last_auth_time = 0 + self._auth_expiry = 3600 # По умолчанию 1 час + + def login(self) -> bool: + """Авторизация в API Synology NAS""" + # Проверяем, действительна ли текущая сессия + current_time = time.time() + if self.sid and (current_time - self._last_auth_time) < self._auth_expiry: + logger.debug("Using existing session, still valid") + return True + + logger.info("Attempting to authenticate with Synology NAS...") + logger.debug(f"Base URL: {self.base_url}") + + # Проверяем доступность NAS перед запросом + online_status = self.is_online() + if not online_status: + logger.error("Cannot login: Synology NAS is not reachable") + return False + + try: + url = f"{self.base_url}/auth.cgi" + params = { + "api": "SYNO.API.Auth", + "version": "3", + "method": "login", + "account": SYNOLOGY_USERNAME, + "passwd": SYNOLOGY_PASSWORD, + "session": "SynologyPowerControlBot", + "format": "cookie" + } + + logger.debug(f"Sending auth request to {url}") + start_time = time.time() + response = self.session.get( + url, + params=params, + timeout=self.default_timeout, + verify=False + ) + elapsed_time = time.time() - start_time + logger.debug(f"Auth request completed in {elapsed_time:.2f}s with status code {response.status_code}") + + # Проверка статуса HTTP + if response.status_code != 200: + logger.error(f"HTTP error: {response.status_code}") + logger.debug(f"Response content: {response.text[:200]}") + return False + + try: + data = response.json() + except json.JSONDecodeError: + logger.error("Failed to decode JSON response") + logger.debug(f"Response content: {response.text[:200]}") + return False + + if data.get("success"): + self.sid = data.get("data", {}).get("sid") + self._last_auth_time = current_time + logger.info("Successfully logged in to Synology NAS") + logger.debug(f"Session ID obtained: {self.sid[:5]}...") # Показываем только начало SID для безопасности + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to log in to Synology NAS: Error code {error_code}") + + # Дополнительная диагностика + if error_code == 400: + logger.error("Authentication error: Invalid credentials") + elif error_code == 401: + logger.error("Authentication error: Account disabled") + elif error_code == 402: + logger.error("Authentication error: Permission denied") + elif error_code == 403: + logger.error("Authentication error: 2-factor authentication required") + elif error_code == 404: + logger.error("Authentication error: Failed to authenticate with 2-factor authentication") + + return False + + except requests.exceptions.Timeout: + logger.error(f"Connection timeout after {SYNOLOGY_TIMEOUT} seconds") + return False + except requests.exceptions.ConnectionError as e: + logger.error(f"Connection error: {str(e)}") + logger.debug(f"Connection details: {self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}") + return False + except requests.RequestException as e: + logger.error(f"Request error: {str(e)}") + return False + except Exception as e: + logger.error(f"Unexpected error during login: {str(e)}", exc_info=True) + return False + + def logout(self) -> bool: + """Выход из API Synology NAS""" + if not self.sid: + return True + + try: + url = f"{self.base_url}/auth.cgi" + params = { + "api": "SYNO.API.Auth", + "version": "1", + "method": "logout", + "session": "SynologyPowerControlBot", + "_sid": self.sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + self.sid = None + logger.info("Successfully logged out from Synology NAS") + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to log out from Synology NAS: Error code {error_code}") + return False + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + return False + + def get_system_status(self) -> Optional[Dict[str, Any]]: + """Получение статуса системы""" + # Если устройство недоступно, сразу возвращаем минимальную информацию + if not self.is_online(): + logger.info("Device is offline, skipping API request") + return {"status": "offline"} + + if not self.sid and not self.login(): + logger.warning("Not authenticated, returning minimal status") + return {"status": "unknown", "error": "authentication_failed"} + + try: + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.DSM.Info", + "version": "1", + "method": "getinfo", + "sid": self.sid # Проверяем правильность параметра sid вместо _sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + logger.info("Successfully fetched system status") + return data.get("data", {}) + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to get system status: Error code {error_code}") + + # Ошибка 104 - требуется авторизация или неверное API + if error_code == 104: + # Пробуем переавторизоваться + if self.login(): + logger.info("Re-authenticated, trying again") + # Пробуем получить информацию еще раз, но без рекурсивного вызова + try: + url = f"{self.base_url}/entry.cgi" + retry_params = { + "api": "SYNO.DSM.Info", + "version": "1", + "method": "getinfo", + "sid": self.sid + } + + retry_response = self.session.get(url, params=retry_params, timeout=SYNOLOGY_TIMEOUT, verify=False) + retry_data = retry_response.json() + + if retry_data.get("success"): + logger.info("Successfully fetched system status after re-authentication") + return retry_data.get("data", {}) + except Exception as e: + logger.error(f"Error during retry: {str(e)}") + + # Возвращаем минимальную информацию с ошибкой + return { + "status": "error", + "error_code": error_code, + "is_online": True + } + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + return {"status": "error", "error": str(e), "is_online": True} + except Exception as e: + logger.error(f"Unexpected error getting status: {str(e)}") + return {"status": "error", "error": str(e), "is_online": True} + + def shutdown_system(self) -> bool: + """Выключение системы""" + # Проверяем, включено ли устройство перед попыткой его выключить + if not self.is_online(): + logger.info("Device is already offline, no need to shut down") + return True + + if not self.sid and not self.login(): + logger.error("Failed to authenticate for shutdown") + return False + + try: + # Пробуем DSM.System API + try: + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.DSM.System", + "version": "1", + "method": "shutdown", + "sid": self.sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + logger.info("Successfully initiated system shutdown using SYNO.DSM.System") + return True + except Exception as e: + logger.warning(f"First shutdown attempt failed: {str(e)}") + + # Пробуем альтернативный API + try: + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.Core.System", + "version": "3", + "method": "shutdown", + "sid": self.sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + logger.info("Successfully initiated system shutdown using SYNO.Core.System") + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to shutdown system: Error code {error_code}") + + # Если система недоступна по API, но доступна по сети, + # считаем, что мы все равно можем её выключить + if error_code == 102: + logger.info("System returned permission error. This is common when using user without proper rights.") + logger.info("The shutdown command might still be processed, checking status...") + + # Даем системе несколько секунд, чтобы начать выключение + sleep(5) + + # Проверяем, стала ли система недоступна + if not self.is_online(): + logger.info("System is now offline. Shutdown appears successful.") + return True + else: + logger.info("System is still online. Shutdown may have failed.") + + return False + except Exception as e: + logger.error(f"Second shutdown attempt failed: {str(e)}") + return False + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + + # Если после попытки выключения соединение потеряно, возможно, выключение успешно + if not self.is_online(): + logger.info("Connection lost after shutdown request, device appears to be shutting down") + return True + + return False + + def reboot_system(self) -> bool: + """Перезагрузка системы""" + if not self.sid and not self.login(): + return False + + try: + # Пробуем DSM.System API + try: + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.DSM.System", + "version": "1", + "method": "reboot", + "sid": self.sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + logger.info("Successfully initiated system reboot using SYNO.DSM.System") + return True + except Exception as e: + logger.warning(f"First reboot attempt failed: {str(e)}") + + # Пробуем альтернативный API + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.Core.System", + "version": "3", + "method": "reboot", + "sid": self.sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + logger.info("Successfully initiated system reboot using SYNO.Core.System") + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to reboot system: Error code {error_code}") + + # Если система недоступна по API, но доступна по сети, + # считаем, что мы все равно можем её перезагрузить + if error_code == 102 and self.is_online(): + logger.info("System is online but API returns permission error. Considering reboot successful anyway.") + return True + + return False + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + return False + + def is_online(self) -> bool: + """Проверка онлайн-статуса Synology NAS""" + try: + socket_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + socket_obj.settimeout(SYNOLOGY_TIMEOUT) + result = socket_obj.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT)) + socket_obj.close() + + return result == 0 + except socket.error: + return False + + def wake_on_lan(self) -> bool: + """Отправка Wake-on-LAN пакета для включения Synology NAS""" + if not SYNOLOGY_MAC: + logger.error("MAC address not configured") + return False + + try: + # Преобразование MAC-адреса в байты + mac_address = SYNOLOGY_MAC.replace(':', '').replace('-', '') + mac_bytes = bytes.fromhex(mac_address) + + # Создание Magic Packet + magic_packet = b'\xff' * 6 + mac_bytes * 16 + + # Отправка пакета + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) + sock.sendto(magic_packet, (SYNOLOGY_HOST, WOL_PORT)) + sock.close() + + logger.info(f"Wake-on-LAN packet sent to {SYNOLOGY_MAC}") + return True + + except Exception as e: + logger.error(f"Error sending Wake-on-LAN packet: {str(e)}") + return False + + def wait_for_boot(self, max_attempts: int = 30, delay: int = 10) -> bool: + """Ожидание загрузки Synology NAS""" + logger.info("Waiting for Synology NAS to boot...") + + for attempt in range(max_attempts): + if self.is_online(): + logger.info(f"Synology NAS is online after {attempt + 1} attempts") + # Дадим дополнительное время для полной загрузки всех сервисов + sleep(delay) + return True + + sleep(delay) + logger.info(f"Waiting for boot... Attempt {attempt + 1}/{max_attempts}") + + logger.error(f"Synology NAS didn't come online after {max_attempts} attempts") + return False + + def power_on(self) -> bool: + """Включение Synology NAS""" + if self.is_online(): + logger.info("Synology NAS is already online") + return True + + # Отправка WoL пакета + if not self.wake_on_lan(): + return False + + # Ожидание загрузки + return self.wait_for_boot() + + def power_off(self) -> bool: + """Выключение Synology NAS""" + if not self.is_online(): + logger.info("Synology NAS is already offline") + return True + + return self.shutdown_system() + + # Заглушки для расширенных методов + def get_shared_folders(self) -> List[Dict[str, Any]]: + """Получение списка общих папок (заглушка)""" + logger.warning("Function get_shared_folders() is not implemented yet") + return [] + + def get_system_load(self) -> Dict[str, Any]: + """Получение информации о загрузке системы (заглушка)""" + logger.warning("Function get_system_load() is not implemented yet") + return {} + + def get_storage_status(self) -> Dict[str, Any]: + """Получение подробной информации о хранилище (заглушка)""" + logger.warning("Function get_storage_status() is not implemented yet") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for storage status request") + return {"error": "authentication_failed"} + + try: + # Здесь будет реализация после добавления соответствующего API + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.Storage.CGI.Storage", + "version": "1", + "method": "load_info", + "sid": self.sid + } + + # В текущей версии просто возвращаем заглушку + return { + "volumes": [], + "disks": [], + "total_size": 0, + "total_used": 0, + "error": "not_implemented" + } + except Exception as e: + logger.error(f"Error in get_storage_status: {str(e)}") + return {"error": str(e)} + + def get_security_status(self) -> Dict[str, Any]: + """Получение информации о состоянии безопасности (заглушка)""" + logger.warning("Function get_security_status() is not implemented yet") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for security status request") + return {"success": False, "error": "authentication_failed"} + + try: + # Здесь будет реализация после добавления соответствующего API + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.Core.SecurityScan.Status", + "version": "1", + "method": "get", + "sid": self.sid + } + + # В текущей версии просто возвращаем заглушку + return { + "success": False, + "status": "not_implemented", + "last_check": None, + "is_secure": False, + "error": "not_implemented" + } + except Exception as e: + logger.error(f"Error in get_security_status: {str(e)}") + return {"success": False, "error": str(e)} diff --git a/.history/src/api/synology_20250830074755.py b/.history/src/api/synology_20250830074755.py new file mode 100644 index 0000000..1967301 --- /dev/null +++ b/.history/src/api/synology_20250830074755.py @@ -0,0 +1,637 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Модуль для взаимодействия с API Synology NAS +""" + +import requests +from requests.adapters import HTTPAdapter +import json +import logging +import time +import urllib3 +from urllib3.util import Retry +from typing import Dict, Any, Optional, List +import socket +import struct +from time import sleep + +from src.config.config import ( + SYNOLOGY_HOST, + SYNOLOGY_PORT, + SYNOLOGY_USERNAME, + SYNOLOGY_PASSWORD, + SYNOLOGY_SECURE, + SYNOLOGY_TIMEOUT, + SYNOLOGY_MAC, + WOL_PORT +) + +# Отключение предупреждений о небезопасных SSL-соединениях +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + +logger = logging.getLogger(__name__) + +class SynologyAPI: + """Класс для взаимодействия с API Synology NAS""" + + def __init__(self): + """Инициализация класса SynologyAPI""" + logger.info("Creating API with auto-retry and connection pool") + logger.debug(f"Connection details: Host={SYNOLOGY_HOST}, Port={SYNOLOGY_PORT}, Secure={SYNOLOGY_SECURE}") + + self.protocol = "https" if SYNOLOGY_SECURE else "http" + self.base_url = f"{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi" + self.sid = None + self.session = requests.Session() + + # Настройка SSL + if self.protocol == "https": + logger.debug("SSL enabled, disabling certificate verification for internal network") + self.session.verify = False # Отключаем проверку SSL для внутренней сети + + # Добавляем повторные попытки для HTTP-запросов + retry_strategy = Retry( + total=5, # Увеличиваем количество попыток + status_forcelist=[429, 500, 502, 503, 504, 404], + allowed_methods=["GET", "POST"], + backoff_factor=1.5, # Увеличиваем задержку между попытками + respect_retry_after_header=True + ) + adapter = HTTPAdapter( + max_retries=retry_strategy, + pool_connections=3, + pool_maxsize=10 + ) + self.session.mount("http://", adapter) + self.session.mount("https://", adapter) + + # Таймауты будут указаны в запросах + self.default_timeout = (SYNOLOGY_TIMEOUT, SYNOLOGY_TIMEOUT*2) + logger.debug(f"Setting default request timeout: {self.default_timeout}") + + # Кэш для хранения результатов запросов + self._cache = {} + self._cache_ttl = {} + self._last_online_check = 0 + self._last_online_status = False + self._online_check_interval = 30 # Интервал для кэширования проверок онлайн-статуса + + # Время последней успешной аутентификации и срок действия сессии + self._last_auth_time = 0 + self._auth_expiry = 3600 # По умолчанию 1 час + + def login(self) -> bool: + """Авторизация в API Synology NAS""" + # Проверяем, действительна ли текущая сессия + current_time = time.time() + if self.sid and (current_time - self._last_auth_time) < self._auth_expiry: + logger.debug("Using existing session, still valid") + return True + + logger.info("Attempting to authenticate with Synology NAS...") + logger.debug(f"Base URL: {self.base_url}") + + # Проверяем доступность NAS перед запросом + online_status = self.is_online() + if not online_status: + logger.error("Cannot login: Synology NAS is not reachable") + return False + + try: + url = f"{self.base_url}/auth.cgi" + params = { + "api": "SYNO.API.Auth", + "version": "3", + "method": "login", + "account": SYNOLOGY_USERNAME, + "passwd": SYNOLOGY_PASSWORD, + "session": "SynologyPowerControlBot", + "format": "cookie" + } + + logger.debug(f"Sending auth request to {url}") + start_time = time.time() + response = self.session.get( + url, + params=params, + timeout=self.default_timeout, + verify=False + ) + elapsed_time = time.time() - start_time + logger.debug(f"Auth request completed in {elapsed_time:.2f}s with status code {response.status_code}") + + # Проверка статуса HTTP + if response.status_code != 200: + logger.error(f"HTTP error: {response.status_code}") + logger.debug(f"Response content: {response.text[:200]}") + return False + + try: + data = response.json() + except json.JSONDecodeError: + logger.error("Failed to decode JSON response") + logger.debug(f"Response content: {response.text[:200]}") + return False + + if data.get("success"): + self.sid = data.get("data", {}).get("sid") + self._last_auth_time = current_time + logger.info("Successfully logged in to Synology NAS") + logger.debug(f"Session ID obtained: {self.sid[:5]}...") # Показываем только начало SID для безопасности + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to log in to Synology NAS: Error code {error_code}") + + # Дополнительная диагностика + if error_code == 400: + logger.error("Authentication error: Invalid credentials") + elif error_code == 401: + logger.error("Authentication error: Account disabled") + elif error_code == 402: + logger.error("Authentication error: Permission denied") + elif error_code == 403: + logger.error("Authentication error: 2-factor authentication required") + elif error_code == 404: + logger.error("Authentication error: Failed to authenticate with 2-factor authentication") + + return False + + except requests.exceptions.Timeout: + logger.error(f"Connection timeout after {SYNOLOGY_TIMEOUT} seconds") + return False + except requests.exceptions.ConnectionError as e: + logger.error(f"Connection error: {str(e)}") + logger.debug(f"Connection details: {self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}") + return False + except requests.RequestException as e: + logger.error(f"Request error: {str(e)}") + return False + except Exception as e: + logger.error(f"Unexpected error during login: {str(e)}", exc_info=True) + return False + + def logout(self) -> bool: + """Выход из API Synology NAS""" + if not self.sid: + return True + + try: + url = f"{self.base_url}/auth.cgi" + params = { + "api": "SYNO.API.Auth", + "version": "1", + "method": "logout", + "session": "SynologyPowerControlBot", + "_sid": self.sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + self.sid = None + logger.info("Successfully logged out from Synology NAS") + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to log out from Synology NAS: Error code {error_code}") + return False + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + return False + + def get_system_status(self) -> Optional[Dict[str, Any]]: + """Получение статуса системы""" + # Если устройство недоступно, сразу возвращаем минимальную информацию + if not self.is_online(): + logger.info("Device is offline, skipping API request") + return {"status": "offline"} + + if not self.sid and not self.login(): + logger.warning("Not authenticated, returning minimal status") + return {"status": "unknown", "error": "authentication_failed"} + + try: + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.DSM.Info", + "version": "1", + "method": "getinfo", + "sid": self.sid # Проверяем правильность параметра sid вместо _sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + logger.info("Successfully fetched system status") + return data.get("data", {}) + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to get system status: Error code {error_code}") + + # Ошибка 104 - требуется авторизация или неверное API + if error_code == 104: + # Пробуем переавторизоваться + if self.login(): + logger.info("Re-authenticated, trying again") + # Пробуем получить информацию еще раз, но без рекурсивного вызова + try: + url = f"{self.base_url}/entry.cgi" + retry_params = { + "api": "SYNO.DSM.Info", + "version": "1", + "method": "getinfo", + "sid": self.sid + } + + retry_response = self.session.get(url, params=retry_params, timeout=SYNOLOGY_TIMEOUT, verify=False) + retry_data = retry_response.json() + + if retry_data.get("success"): + logger.info("Successfully fetched system status after re-authentication") + return retry_data.get("data", {}) + except Exception as e: + logger.error(f"Error during retry: {str(e)}") + + # Возвращаем минимальную информацию с ошибкой + return { + "status": "error", + "error_code": error_code, + "is_online": True + } + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + return {"status": "error", "error": str(e), "is_online": True} + except Exception as e: + logger.error(f"Unexpected error getting status: {str(e)}") + return {"status": "error", "error": str(e), "is_online": True} + + def shutdown_system(self) -> bool: + """Выключение системы""" + # Проверяем, включено ли устройство перед попыткой его выключить + if not self.is_online(): + logger.info("Device is already offline, no need to shut down") + return True + + if not self.sid and not self.login(): + logger.error("Failed to authenticate for shutdown") + return False + + try: + # Пробуем DSM.System API + try: + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.DSM.System", + "version": "1", + "method": "shutdown", + "sid": self.sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + logger.info("Successfully initiated system shutdown using SYNO.DSM.System") + return True + except Exception as e: + logger.warning(f"First shutdown attempt failed: {str(e)}") + + # Пробуем альтернативный API + try: + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.Core.System", + "version": "3", + "method": "shutdown", + "sid": self.sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + logger.info("Successfully initiated system shutdown using SYNO.Core.System") + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to shutdown system: Error code {error_code}") + + # Если система недоступна по API, но доступна по сети, + # считаем, что мы все равно можем её выключить + if error_code == 102: + logger.info("System returned permission error. This is common when using user without proper rights.") + logger.info("The shutdown command might still be processed, checking status...") + + # Даем системе несколько секунд, чтобы начать выключение + sleep(5) + + # Проверяем, стала ли система недоступна + if not self.is_online(): + logger.info("System is now offline. Shutdown appears successful.") + return True + else: + logger.info("System is still online. Shutdown may have failed.") + + return False + except Exception as e: + logger.error(f"Second shutdown attempt failed: {str(e)}") + return False + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + + # Если после попытки выключения соединение потеряно, возможно, выключение успешно + if not self.is_online(): + logger.info("Connection lost after shutdown request, device appears to be shutting down") + return True + + return False + + def reboot_system(self) -> bool: + """Перезагрузка системы""" + if not self.sid and not self.login(): + return False + + try: + # Пробуем DSM.System API + try: + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.DSM.System", + "version": "1", + "method": "reboot", + "sid": self.sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + logger.info("Successfully initiated system reboot using SYNO.DSM.System") + return True + except Exception as e: + logger.warning(f"First reboot attempt failed: {str(e)}") + + # Пробуем альтернативный API + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.Core.System", + "version": "3", + "method": "reboot", + "sid": self.sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + logger.info("Successfully initiated system reboot using SYNO.Core.System") + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to reboot system: Error code {error_code}") + + # Если система недоступна по API, но доступна по сети, + # считаем, что мы все равно можем её перезагрузить + if error_code == 102 and self.is_online(): + logger.info("System is online but API returns permission error. Considering reboot successful anyway.") + return True + + return False + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + return False + + def _get_error_description(self, error_code: int) -> str: + """Получение описания ошибки по коду""" + error_descriptions = { + 100: "Unknown error", + 101: "Invalid parameter", + 102: "API does not exist", + 103: "Method does not exist", + 104: "Version does not support", + 105: "Permission denied", + 106: "Session timeout", + 107: "Session interrupted by duplicate login", + 400: "Invalid credentials", + 401: "Account disabled", + 402: "Permission denied", + 403: "2FA required", + 404: "Failed to authenticate with 2FA" + } + return error_descriptions.get(error_code, "Unknown error code") + + def is_online(self, force_check=False) -> bool: + """Проверка онлайн-статуса Synology NAS""" + # Используем кэшированное значение, если доступно и не устарело + current_time = time.time() + if not force_check and (current_time - self._last_online_check) < self._online_check_interval: + logger.debug(f"Using cached online status: {self._last_online_status}") + return self._last_online_status + + logger.info("Checking if NAS is online...") + try: + # Сначала проверяем TCP-соединение + socket_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + socket_obj.settimeout(SYNOLOGY_TIMEOUT) + result = socket_obj.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT)) + socket_obj.close() + + online_status = (result == 0) + logger.info(f"Detected Synology NAS online status: {online_status}") + + # Если TCP-соединение успешно, попробуем более детальную проверку через API + if online_status: + logger.info("Trying to fetch more detailed online status through API...") + # Пробуем получить информацию, но не вызываем is_online() рекурсивно + if self.sid or self.login(): + try: + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.DSM.Info", + "version": "1", + "method": "getinfo", + "sid": self.sid + } + + response = self.session.get( + url, + params=params, + timeout=self.default_timeout, + verify=False + ) + + if response.status_code == 200: + data = response.json() + if data.get("success"): + logger.info("API request successful for getinfo") + logger.info("Synology NAS is online with API access") + else: + logger.warning("API response indicates an error, but NAS is reachable") + logger.debug(f"API error: {data.get('error', {}).get('code', -1)}") + else: + logger.warning(f"API returned status code {response.status_code}") + except Exception as e: + logger.warning(f"API check failed, but TCP connection succeeded: {str(e)}") + + # Обновляем кэшированное значение + self._last_online_check = current_time + self._last_online_status = online_status + + return online_status + except socket.error as e: + logger.error(f"Socket error during online check: {str(e)}") + self._last_online_check = current_time + self._last_online_status = False + return False + except Exception as e: + logger.error(f"Unexpected error during online check: {str(e)}") + self._last_online_check = current_time + self._last_online_status = False + return False + + def wake_on_lan(self) -> bool: + """Отправка Wake-on-LAN пакета для включения Synology NAS""" + if not SYNOLOGY_MAC: + logger.error("MAC address not configured") + return False + + try: + # Преобразование MAC-адреса в байты + mac_address = SYNOLOGY_MAC.replace(':', '').replace('-', '') + mac_bytes = bytes.fromhex(mac_address) + + # Создание Magic Packet + magic_packet = b'\xff' * 6 + mac_bytes * 16 + + # Отправка пакета + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) + sock.sendto(magic_packet, (SYNOLOGY_HOST, WOL_PORT)) + sock.close() + + logger.info(f"Wake-on-LAN packet sent to {SYNOLOGY_MAC}") + return True + + except Exception as e: + logger.error(f"Error sending Wake-on-LAN packet: {str(e)}") + return False + + def wait_for_boot(self, max_attempts: int = 30, delay: int = 10) -> bool: + """Ожидание загрузки Synology NAS""" + logger.info("Waiting for Synology NAS to boot...") + + for attempt in range(max_attempts): + if self.is_online(): + logger.info(f"Synology NAS is online after {attempt + 1} attempts") + # Дадим дополнительное время для полной загрузки всех сервисов + sleep(delay) + return True + + sleep(delay) + logger.info(f"Waiting for boot... Attempt {attempt + 1}/{max_attempts}") + + logger.error(f"Synology NAS didn't come online after {max_attempts} attempts") + return False + + def power_on(self) -> bool: + """Включение Synology NAS""" + if self.is_online(): + logger.info("Synology NAS is already online") + return True + + # Отправка WoL пакета + if not self.wake_on_lan(): + return False + + # Ожидание загрузки + return self.wait_for_boot() + + def power_off(self) -> bool: + """Выключение Synology NAS""" + if not self.is_online(): + logger.info("Synology NAS is already offline") + return True + + return self.shutdown_system() + + # Заглушки для расширенных методов + def get_shared_folders(self) -> List[Dict[str, Any]]: + """Получение списка общих папок (заглушка)""" + logger.warning("Function get_shared_folders() is not implemented yet") + return [] + + def get_system_load(self) -> Dict[str, Any]: + """Получение информации о загрузке системы (заглушка)""" + logger.warning("Function get_system_load() is not implemented yet") + return {} + + def get_storage_status(self) -> Dict[str, Any]: + """Получение подробной информации о хранилище (заглушка)""" + logger.warning("Function get_storage_status() is not implemented yet") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for storage status request") + return {"error": "authentication_failed"} + + try: + # Здесь будет реализация после добавления соответствующего API + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.Storage.CGI.Storage", + "version": "1", + "method": "load_info", + "sid": self.sid + } + + # В текущей версии просто возвращаем заглушку + return { + "volumes": [], + "disks": [], + "total_size": 0, + "total_used": 0, + "error": "not_implemented" + } + except Exception as e: + logger.error(f"Error in get_storage_status: {str(e)}") + return {"error": str(e)} + + def get_security_status(self) -> Dict[str, Any]: + """Получение информации о состоянии безопасности (заглушка)""" + logger.warning("Function get_security_status() is not implemented yet") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for security status request") + return {"success": False, "error": "authentication_failed"} + + try: + # Здесь будет реализация после добавления соответствующего API + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.Core.SecurityScan.Status", + "version": "1", + "method": "get", + "sid": self.sid + } + + # В текущей версии просто возвращаем заглушку + return { + "success": False, + "status": "not_implemented", + "last_check": None, + "is_secure": False, + "error": "not_implemented" + } + except Exception as e: + logger.error(f"Error in get_security_status: {str(e)}") + return {"success": False, "error": str(e)} diff --git a/.history/src/api/synology_20250830074838.py b/.history/src/api/synology_20250830074838.py new file mode 100644 index 0000000..4558a9a --- /dev/null +++ b/.history/src/api/synology_20250830074838.py @@ -0,0 +1,686 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Модуль для взаимодействия с API Synology NAS +""" + +import requests +from requests.adapters import HTTPAdapter +import json +import logging +import time +import urllib3 +from urllib3.util import Retry +from typing import Dict, Any, Optional, List +import socket +import struct +from time import sleep + +from src.config.config import ( + SYNOLOGY_HOST, + SYNOLOGY_PORT, + SYNOLOGY_USERNAME, + SYNOLOGY_PASSWORD, + SYNOLOGY_SECURE, + SYNOLOGY_TIMEOUT, + SYNOLOGY_MAC, + WOL_PORT +) + +# Отключение предупреждений о небезопасных SSL-соединениях +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + +logger = logging.getLogger(__name__) + +class SynologyAPI: + """Класс для взаимодействия с API Synology NAS""" + + def __init__(self): + """Инициализация класса SynologyAPI""" + logger.info("Creating API with auto-retry and connection pool") + logger.debug(f"Connection details: Host={SYNOLOGY_HOST}, Port={SYNOLOGY_PORT}, Secure={SYNOLOGY_SECURE}") + + self.protocol = "https" if SYNOLOGY_SECURE else "http" + self.base_url = f"{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi" + self.sid = None + self.session = requests.Session() + + # Настройка SSL + if self.protocol == "https": + logger.debug("SSL enabled, disabling certificate verification for internal network") + self.session.verify = False # Отключаем проверку SSL для внутренней сети + + # Добавляем повторные попытки для HTTP-запросов + retry_strategy = Retry( + total=5, # Увеличиваем количество попыток + status_forcelist=[429, 500, 502, 503, 504, 404], + allowed_methods=["GET", "POST"], + backoff_factor=1.5, # Увеличиваем задержку между попытками + respect_retry_after_header=True + ) + adapter = HTTPAdapter( + max_retries=retry_strategy, + pool_connections=3, + pool_maxsize=10 + ) + self.session.mount("http://", adapter) + self.session.mount("https://", adapter) + + # Таймауты будут указаны в запросах + self.default_timeout = (SYNOLOGY_TIMEOUT, SYNOLOGY_TIMEOUT*2) + logger.debug(f"Setting default request timeout: {self.default_timeout}") + + # Кэш для хранения результатов запросов + self._cache = {} + self._cache_ttl = {} + self._last_online_check = 0 + self._last_online_status = False + self._online_check_interval = 30 # Интервал для кэширования проверок онлайн-статуса + + # Время последней успешной аутентификации и срок действия сессии + self._last_auth_time = 0 + self._auth_expiry = 3600 # По умолчанию 1 час + + def login(self) -> bool: + """Авторизация в API Synology NAS""" + # Проверяем, действительна ли текущая сессия + current_time = time.time() + if self.sid and (current_time - self._last_auth_time) < self._auth_expiry: + logger.debug("Using existing session, still valid") + return True + + logger.info("Attempting to authenticate with Synology NAS...") + logger.debug(f"Base URL: {self.base_url}") + + # Проверяем доступность NAS перед запросом + online_status = self.is_online() + if not online_status: + logger.error("Cannot login: Synology NAS is not reachable") + return False + + try: + url = f"{self.base_url}/auth.cgi" + params = { + "api": "SYNO.API.Auth", + "version": "3", + "method": "login", + "account": SYNOLOGY_USERNAME, + "passwd": SYNOLOGY_PASSWORD, + "session": "SynologyPowerControlBot", + "format": "cookie" + } + + logger.debug(f"Sending auth request to {url}") + start_time = time.time() + response = self.session.get( + url, + params=params, + timeout=self.default_timeout, + verify=False + ) + elapsed_time = time.time() - start_time + logger.debug(f"Auth request completed in {elapsed_time:.2f}s with status code {response.status_code}") + + # Проверка статуса HTTP + if response.status_code != 200: + logger.error(f"HTTP error: {response.status_code}") + logger.debug(f"Response content: {response.text[:200]}") + return False + + try: + data = response.json() + except json.JSONDecodeError: + logger.error("Failed to decode JSON response") + logger.debug(f"Response content: {response.text[:200]}") + return False + + if data.get("success"): + self.sid = data.get("data", {}).get("sid") + self._last_auth_time = current_time + logger.info("Successfully logged in to Synology NAS") + logger.debug(f"Session ID obtained: {self.sid[:5]}...") # Показываем только начало SID для безопасности + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to log in to Synology NAS: Error code {error_code}") + + # Дополнительная диагностика + if error_code == 400: + logger.error("Authentication error: Invalid credentials") + elif error_code == 401: + logger.error("Authentication error: Account disabled") + elif error_code == 402: + logger.error("Authentication error: Permission denied") + elif error_code == 403: + logger.error("Authentication error: 2-factor authentication required") + elif error_code == 404: + logger.error("Authentication error: Failed to authenticate with 2-factor authentication") + + return False + + except requests.exceptions.Timeout: + logger.error(f"Connection timeout after {SYNOLOGY_TIMEOUT} seconds") + return False + except requests.exceptions.ConnectionError as e: + logger.error(f"Connection error: {str(e)}") + logger.debug(f"Connection details: {self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}") + return False + except requests.RequestException as e: + logger.error(f"Request error: {str(e)}") + return False + except Exception as e: + logger.error(f"Unexpected error during login: {str(e)}", exc_info=True) + return False + + def logout(self) -> bool: + """Выход из API Synology NAS""" + if not self.sid: + return True + + try: + url = f"{self.base_url}/auth.cgi" + params = { + "api": "SYNO.API.Auth", + "version": "1", + "method": "logout", + "session": "SynologyPowerControlBot", + "_sid": self.sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + self.sid = None + logger.info("Successfully logged out from Synology NAS") + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to log out from Synology NAS: Error code {error_code}") + return False + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + return False + + def _make_api_request(self, api_name: str, method: str, version: int = 1, + params: Dict[str, Any] = None, retry_auth: bool = True) -> Optional[Dict[str, Any]]: + """Обобщенный метод для выполнения API запросов с обработкой ошибок""" + # Проверка наличия авторизации + if not self.sid and not self.login(): + logger.error(f"Not authenticated for API request: {api_name}.{method}") + return None + + # Подготовка базовых параметров запроса + base_params = { + "api": api_name, + "version": str(version), + "method": method, + "sid": self.sid + } + + # Добавление дополнительных параметров, если они заданы + if params: + base_params.update(params) + + url = f"{self.base_url}/entry.cgi" + logger.debug(f"API request: {api_name}.{method} v{version}") + + try: + start_time = time.time() + response = self.session.get( + url, + params=base_params, + timeout=self.default_timeout, + verify=False + ) + elapsed_time = time.time() - start_time + logger.debug(f"API request completed in {elapsed_time:.2f}s with status {response.status_code}") + + # Проверка статуса HTTP + if response.status_code != 200: + logger.error(f"HTTP error: {response.status_code} for {api_name}.{method}") + return None + + try: + data = response.json() + except json.JSONDecodeError: + logger.error(f"Failed to decode JSON response for {api_name}.{method}") + logger.debug(f"Response content: {response.text[:200]}") + return None + + if data.get("success"): + logger.info(f"API request successful for {method}") + return data.get("data", {}) + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"API error for {api_name}.{method}: {error_code}") + + # Если ошибка связана с авторизацией и нам разрешено повторить попытку + if error_code in [106, 107, 119] and retry_auth: + logger.info("Session may have expired, re-authenticating...") + self.sid = None # Сбрасываем SID + if self.login(): + logger.info("Re-authenticated, retrying API request...") + # Рекурсивный вызов, но без повторной авторизации во избежание зацикливания + return self._make_api_request(api_name, method, version, params, False) + + return None + + except requests.exceptions.Timeout: + logger.error(f"Request timeout for {api_name}.{method}") + return None + except requests.exceptions.ConnectionError as e: + logger.error(f"Connection error for {api_name}.{method}: {str(e)}") + return None + except Exception as e: + logger.error(f"Unexpected error during {api_name}.{method}: {str(e)}") + return None + + def get_system_status(self) -> Dict[str, Any]: + """Получение статуса системы""" + # Проверяем доступность системы + if not self.is_online(): + logger.info("Device is offline, skipping API request") + return {"status": "offline"} + + # Проверяем, есть ли кэшированный результат + cache_key = "system_status" + current_time = time.time() + if cache_key in self._cache and current_time - self._cache_ttl.get(cache_key, 0) < 60: + logger.debug("Using cached system status") + return self._cache[cache_key] + + # Делаем API запрос + result = self._make_api_request("SYNO.DSM.Info", "getinfo") + + if result: + # Формируем расширенный ответ с дополнительной информацией + system_info = { + "status": "online", + "hostname": result.get("hostname", "unknown"), + "model": result.get("model", "unknown"), + "version": result.get("version", "unknown"), + "uptime": result.get("uptime", 0), + "time": current_time, + "is_online": True + } + + # Сохраняем в кэше + self._cache[cache_key] = system_info + self._cache_ttl[cache_key] = current_time + + return system_info + else: + # Если запрос не удался, но система онлайн + return { + "status": "error", + "error": "Failed to fetch system information", + "is_online": True, + "time": current_time + } + + def shutdown_system(self) -> bool: + """Выключение системы""" + # Проверяем, включено ли устройство перед попыткой его выключить + if not self.is_online(): + logger.info("Device is already offline, no need to shut down") + return True + + if not self.sid and not self.login(): + logger.error("Failed to authenticate for shutdown") + return False + + try: + # Пробуем DSM.System API + try: + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.DSM.System", + "version": "1", + "method": "shutdown", + "sid": self.sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + logger.info("Successfully initiated system shutdown using SYNO.DSM.System") + return True + except Exception as e: + logger.warning(f"First shutdown attempt failed: {str(e)}") + + # Пробуем альтернативный API + try: + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.Core.System", + "version": "3", + "method": "shutdown", + "sid": self.sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + logger.info("Successfully initiated system shutdown using SYNO.Core.System") + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to shutdown system: Error code {error_code}") + + # Если система недоступна по API, но доступна по сети, + # считаем, что мы все равно можем её выключить + if error_code == 102: + logger.info("System returned permission error. This is common when using user without proper rights.") + logger.info("The shutdown command might still be processed, checking status...") + + # Даем системе несколько секунд, чтобы начать выключение + sleep(5) + + # Проверяем, стала ли система недоступна + if not self.is_online(): + logger.info("System is now offline. Shutdown appears successful.") + return True + else: + logger.info("System is still online. Shutdown may have failed.") + + return False + except Exception as e: + logger.error(f"Second shutdown attempt failed: {str(e)}") + return False + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + + # Если после попытки выключения соединение потеряно, возможно, выключение успешно + if not self.is_online(): + logger.info("Connection lost after shutdown request, device appears to be shutting down") + return True + + return False + + def reboot_system(self) -> bool: + """Перезагрузка системы""" + if not self.sid and not self.login(): + return False + + try: + # Пробуем DSM.System API + try: + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.DSM.System", + "version": "1", + "method": "reboot", + "sid": self.sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + logger.info("Successfully initiated system reboot using SYNO.DSM.System") + return True + except Exception as e: + logger.warning(f"First reboot attempt failed: {str(e)}") + + # Пробуем альтернативный API + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.Core.System", + "version": "3", + "method": "reboot", + "sid": self.sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + logger.info("Successfully initiated system reboot using SYNO.Core.System") + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to reboot system: Error code {error_code}") + + # Если система недоступна по API, но доступна по сети, + # считаем, что мы все равно можем её перезагрузить + if error_code == 102 and self.is_online(): + logger.info("System is online but API returns permission error. Considering reboot successful anyway.") + return True + + return False + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + return False + + def _get_error_description(self, error_code: int) -> str: + """Получение описания ошибки по коду""" + error_descriptions = { + 100: "Unknown error", + 101: "Invalid parameter", + 102: "API does not exist", + 103: "Method does not exist", + 104: "Version does not support", + 105: "Permission denied", + 106: "Session timeout", + 107: "Session interrupted by duplicate login", + 400: "Invalid credentials", + 401: "Account disabled", + 402: "Permission denied", + 403: "2FA required", + 404: "Failed to authenticate with 2FA" + } + return error_descriptions.get(error_code, "Unknown error code") + + def is_online(self, force_check=False) -> bool: + """Проверка онлайн-статуса Synology NAS""" + # Используем кэшированное значение, если доступно и не устарело + current_time = time.time() + if not force_check and (current_time - self._last_online_check) < self._online_check_interval: + logger.debug(f"Using cached online status: {self._last_online_status}") + return self._last_online_status + + logger.info("Checking if NAS is online...") + try: + # Сначала проверяем TCP-соединение + socket_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + socket_obj.settimeout(SYNOLOGY_TIMEOUT) + result = socket_obj.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT)) + socket_obj.close() + + online_status = (result == 0) + logger.info(f"Detected Synology NAS online status: {online_status}") + + # Если TCP-соединение успешно, попробуем более детальную проверку через API + if online_status: + logger.info("Trying to fetch more detailed online status through API...") + # Пробуем получить информацию, но не вызываем is_online() рекурсивно + if self.sid or self.login(): + try: + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.DSM.Info", + "version": "1", + "method": "getinfo", + "sid": self.sid + } + + response = self.session.get( + url, + params=params, + timeout=self.default_timeout, + verify=False + ) + + if response.status_code == 200: + data = response.json() + if data.get("success"): + logger.info("API request successful for getinfo") + logger.info("Synology NAS is online with API access") + else: + logger.warning("API response indicates an error, but NAS is reachable") + logger.debug(f"API error: {data.get('error', {}).get('code', -1)}") + else: + logger.warning(f"API returned status code {response.status_code}") + except Exception as e: + logger.warning(f"API check failed, but TCP connection succeeded: {str(e)}") + + # Обновляем кэшированное значение + self._last_online_check = current_time + self._last_online_status = online_status + + return online_status + except socket.error as e: + logger.error(f"Socket error during online check: {str(e)}") + self._last_online_check = current_time + self._last_online_status = False + return False + except Exception as e: + logger.error(f"Unexpected error during online check: {str(e)}") + self._last_online_check = current_time + self._last_online_status = False + return False + + def wake_on_lan(self) -> bool: + """Отправка Wake-on-LAN пакета для включения Synology NAS""" + if not SYNOLOGY_MAC: + logger.error("MAC address not configured") + return False + + try: + # Преобразование MAC-адреса в байты + mac_address = SYNOLOGY_MAC.replace(':', '').replace('-', '') + mac_bytes = bytes.fromhex(mac_address) + + # Создание Magic Packet + magic_packet = b'\xff' * 6 + mac_bytes * 16 + + # Отправка пакета + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) + sock.sendto(magic_packet, (SYNOLOGY_HOST, WOL_PORT)) + sock.close() + + logger.info(f"Wake-on-LAN packet sent to {SYNOLOGY_MAC}") + return True + + except Exception as e: + logger.error(f"Error sending Wake-on-LAN packet: {str(e)}") + return False + + def wait_for_boot(self, max_attempts: int = 30, delay: int = 10) -> bool: + """Ожидание загрузки Synology NAS""" + logger.info("Waiting for Synology NAS to boot...") + + for attempt in range(max_attempts): + if self.is_online(): + logger.info(f"Synology NAS is online after {attempt + 1} attempts") + # Дадим дополнительное время для полной загрузки всех сервисов + sleep(delay) + return True + + sleep(delay) + logger.info(f"Waiting for boot... Attempt {attempt + 1}/{max_attempts}") + + logger.error(f"Synology NAS didn't come online after {max_attempts} attempts") + return False + + def power_on(self) -> bool: + """Включение Synology NAS""" + if self.is_online(): + logger.info("Synology NAS is already online") + return True + + # Отправка WoL пакета + if not self.wake_on_lan(): + return False + + # Ожидание загрузки + return self.wait_for_boot() + + def power_off(self) -> bool: + """Выключение Synology NAS""" + if not self.is_online(): + logger.info("Synology NAS is already offline") + return True + + return self.shutdown_system() + + # Заглушки для расширенных методов + def get_shared_folders(self) -> List[Dict[str, Any]]: + """Получение списка общих папок (заглушка)""" + logger.warning("Function get_shared_folders() is not implemented yet") + return [] + + def get_system_load(self) -> Dict[str, Any]: + """Получение информации о загрузке системы (заглушка)""" + logger.warning("Function get_system_load() is not implemented yet") + return {} + + def get_storage_status(self) -> Dict[str, Any]: + """Получение подробной информации о хранилище (заглушка)""" + logger.warning("Function get_storage_status() is not implemented yet") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for storage status request") + return {"error": "authentication_failed"} + + try: + # Здесь будет реализация после добавления соответствующего API + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.Storage.CGI.Storage", + "version": "1", + "method": "load_info", + "sid": self.sid + } + + # В текущей версии просто возвращаем заглушку + return { + "volumes": [], + "disks": [], + "total_size": 0, + "total_used": 0, + "error": "not_implemented" + } + except Exception as e: + logger.error(f"Error in get_storage_status: {str(e)}") + return {"error": str(e)} + + def get_security_status(self) -> Dict[str, Any]: + """Получение информации о состоянии безопасности (заглушка)""" + logger.warning("Function get_security_status() is not implemented yet") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for security status request") + return {"success": False, "error": "authentication_failed"} + + try: + # Здесь будет реализация после добавления соответствующего API + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.Core.SecurityScan.Status", + "version": "1", + "method": "get", + "sid": self.sid + } + + # В текущей версии просто возвращаем заглушку + return { + "success": False, + "status": "not_implemented", + "last_check": None, + "is_secure": False, + "error": "not_implemented" + } + except Exception as e: + logger.error(f"Error in get_security_status: {str(e)}") + return {"success": False, "error": str(e)} diff --git a/.history/src/api/synology_20250830074850.py b/.history/src/api/synology_20250830074850.py new file mode 100644 index 0000000..0f2ebda --- /dev/null +++ b/.history/src/api/synology_20250830074850.py @@ -0,0 +1,686 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Модуль для взаимодействия с API Synology NAS +""" + +import requests +from requests.adapters import HTTPAdapter +import json +import logging +import time +import urllib3 +from urllib3.util import Retry +from typing import Dict, Any, Optional, List +import socket +import struct +from time import sleep + +from src.config.config import ( + SYNOLOGY_HOST, + SYNOLOGY_PORT, + SYNOLOGY_USERNAME, + SYNOLOGY_PASSWORD, + SYNOLOGY_SECURE, + SYNOLOGY_TIMEOUT, + SYNOLOGY_MAC, + WOL_PORT +) + +# Отключение предупреждений о небезопасных SSL-соединениях +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + +logger = logging.getLogger(__name__) + +class SynologyAPI: + """Класс для взаимодействия с API Synology NAS""" + + def __init__(self): + """Инициализация класса SynologyAPI""" + logger.info("Creating API with auto-retry and connection pool") + logger.debug(f"Connection details: Host={SYNOLOGY_HOST}, Port={SYNOLOGY_PORT}, Secure={SYNOLOGY_SECURE}") + + self.protocol = "https" if SYNOLOGY_SECURE else "http" + self.base_url = f"{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi" + self.sid = None + self.session = requests.Session() + + # Настройка SSL + if self.protocol == "https": + logger.debug("SSL enabled, disabling certificate verification for internal network") + self.session.verify = False # Отключаем проверку SSL для внутренней сети + + # Добавляем повторные попытки для HTTP-запросов + retry_strategy = Retry( + total=5, # Увеличиваем количество попыток + status_forcelist=[429, 500, 502, 503, 504, 404], + allowed_methods=["GET", "POST"], + backoff_factor=1.5, # Увеличиваем задержку между попытками + respect_retry_after_header=True + ) + adapter = HTTPAdapter( + max_retries=retry_strategy, + pool_connections=3, + pool_maxsize=10 + ) + self.session.mount("http://", adapter) + self.session.mount("https://", adapter) + + # Таймауты будут указаны в запросах + self.default_timeout = (SYNOLOGY_TIMEOUT, SYNOLOGY_TIMEOUT*2) + logger.debug(f"Setting default request timeout: {self.default_timeout}") + + # Кэш для хранения результатов запросов + self._cache = {} + self._cache_ttl = {} + self._last_online_check = 0 + self._last_online_status = False + self._online_check_interval = 30 # Интервал для кэширования проверок онлайн-статуса + + # Время последней успешной аутентификации и срок действия сессии + self._last_auth_time = 0 + self._auth_expiry = 3600 # По умолчанию 1 час + + def login(self) -> bool: + """Авторизация в API Synology NAS""" + # Проверяем, действительна ли текущая сессия + current_time = time.time() + if self.sid and (current_time - self._last_auth_time) < self._auth_expiry: + logger.debug("Using existing session, still valid") + return True + + logger.info("Attempting to authenticate with Synology NAS...") + logger.debug(f"Base URL: {self.base_url}") + + # Проверяем доступность NAS перед запросом + online_status = self.is_online() + if not online_status: + logger.error("Cannot login: Synology NAS is not reachable") + return False + + try: + url = f"{self.base_url}/auth.cgi" + params = { + "api": "SYNO.API.Auth", + "version": "3", + "method": "login", + "account": SYNOLOGY_USERNAME, + "passwd": SYNOLOGY_PASSWORD, + "session": "SynologyPowerControlBot", + "format": "cookie" + } + + logger.debug(f"Sending auth request to {url}") + start_time = time.time() + response = self.session.get( + url, + params=params, + timeout=self.default_timeout, + verify=False + ) + elapsed_time = time.time() - start_time + logger.debug(f"Auth request completed in {elapsed_time:.2f}s with status code {response.status_code}") + + # Проверка статуса HTTP + if response.status_code != 200: + logger.error(f"HTTP error: {response.status_code}") + logger.debug(f"Response content: {response.text[:200]}") + return False + + try: + data = response.json() + except json.JSONDecodeError: + logger.error("Failed to decode JSON response") + logger.debug(f"Response content: {response.text[:200]}") + return False + + if data.get("success"): + self.sid = data.get("data", {}).get("sid") + self._last_auth_time = current_time + logger.info("Successfully logged in to Synology NAS") + logger.debug(f"Session ID obtained: {self.sid[:5]}...") # Показываем только начало SID для безопасности + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to log in to Synology NAS: Error code {error_code}") + + # Дополнительная диагностика + if error_code == 400: + logger.error("Authentication error: Invalid credentials") + elif error_code == 401: + logger.error("Authentication error: Account disabled") + elif error_code == 402: + logger.error("Authentication error: Permission denied") + elif error_code == 403: + logger.error("Authentication error: 2-factor authentication required") + elif error_code == 404: + logger.error("Authentication error: Failed to authenticate with 2-factor authentication") + + return False + + except requests.exceptions.Timeout: + logger.error(f"Connection timeout after {SYNOLOGY_TIMEOUT} seconds") + return False + except requests.exceptions.ConnectionError as e: + logger.error(f"Connection error: {str(e)}") + logger.debug(f"Connection details: {self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}") + return False + except requests.RequestException as e: + logger.error(f"Request error: {str(e)}") + return False + except Exception as e: + logger.error(f"Unexpected error during login: {str(e)}", exc_info=True) + return False + + def logout(self) -> bool: + """Выход из API Synology NAS""" + if not self.sid: + return True + + try: + url = f"{self.base_url}/auth.cgi" + params = { + "api": "SYNO.API.Auth", + "version": "1", + "method": "logout", + "session": "SynologyPowerControlBot", + "_sid": self.sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + self.sid = None + logger.info("Successfully logged out from Synology NAS") + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to log out from Synology NAS: Error code {error_code}") + return False + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + return False + + def _make_api_request(self, api_name: str, method: str, version: int = 1, + params: Optional[Dict[str, Any]] = None, retry_auth: bool = True) -> Optional[Dict[str, Any]]: + """Обобщенный метод для выполнения API запросов с обработкой ошибок""" + # Проверка наличия авторизации + if not self.sid and not self.login(): + logger.error(f"Not authenticated for API request: {api_name}.{method}") + return None + + # Подготовка базовых параметров запроса + base_params = { + "api": api_name, + "version": str(version), + "method": method, + "sid": self.sid + } + + # Добавление дополнительных параметров, если они заданы + if params: + base_params.update(params) + + url = f"{self.base_url}/entry.cgi" + logger.debug(f"API request: {api_name}.{method} v{version}") + + try: + start_time = time.time() + response = self.session.get( + url, + params=base_params, + timeout=self.default_timeout, + verify=False + ) + elapsed_time = time.time() - start_time + logger.debug(f"API request completed in {elapsed_time:.2f}s with status {response.status_code}") + + # Проверка статуса HTTP + if response.status_code != 200: + logger.error(f"HTTP error: {response.status_code} for {api_name}.{method}") + return None + + try: + data = response.json() + except json.JSONDecodeError: + logger.error(f"Failed to decode JSON response for {api_name}.{method}") + logger.debug(f"Response content: {response.text[:200]}") + return None + + if data.get("success"): + logger.info(f"API request successful for {method}") + return data.get("data", {}) + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"API error for {api_name}.{method}: {error_code}") + + # Если ошибка связана с авторизацией и нам разрешено повторить попытку + if error_code in [106, 107, 119] and retry_auth: + logger.info("Session may have expired, re-authenticating...") + self.sid = None # Сбрасываем SID + if self.login(): + logger.info("Re-authenticated, retrying API request...") + # Рекурсивный вызов, но без повторной авторизации во избежание зацикливания + return self._make_api_request(api_name, method, version, params, False) + + return None + + except requests.exceptions.Timeout: + logger.error(f"Request timeout for {api_name}.{method}") + return None + except requests.exceptions.ConnectionError as e: + logger.error(f"Connection error for {api_name}.{method}: {str(e)}") + return None + except Exception as e: + logger.error(f"Unexpected error during {api_name}.{method}: {str(e)}") + return None + + def get_system_status(self) -> Dict[str, Any]: + """Получение статуса системы""" + # Проверяем доступность системы + if not self.is_online(): + logger.info("Device is offline, skipping API request") + return {"status": "offline"} + + # Проверяем, есть ли кэшированный результат + cache_key = "system_status" + current_time = time.time() + if cache_key in self._cache and current_time - self._cache_ttl.get(cache_key, 0) < 60: + logger.debug("Using cached system status") + return self._cache[cache_key] + + # Делаем API запрос + result = self._make_api_request("SYNO.DSM.Info", "getinfo") + + if result: + # Формируем расширенный ответ с дополнительной информацией + system_info = { + "status": "online", + "hostname": result.get("hostname", "unknown"), + "model": result.get("model", "unknown"), + "version": result.get("version", "unknown"), + "uptime": result.get("uptime", 0), + "time": current_time, + "is_online": True + } + + # Сохраняем в кэше + self._cache[cache_key] = system_info + self._cache_ttl[cache_key] = current_time + + return system_info + else: + # Если запрос не удался, но система онлайн + return { + "status": "error", + "error": "Failed to fetch system information", + "is_online": True, + "time": current_time + } + + def shutdown_system(self) -> bool: + """Выключение системы""" + # Проверяем, включено ли устройство перед попыткой его выключить + if not self.is_online(): + logger.info("Device is already offline, no need to shut down") + return True + + if not self.sid and not self.login(): + logger.error("Failed to authenticate for shutdown") + return False + + try: + # Пробуем DSM.System API + try: + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.DSM.System", + "version": "1", + "method": "shutdown", + "sid": self.sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + logger.info("Successfully initiated system shutdown using SYNO.DSM.System") + return True + except Exception as e: + logger.warning(f"First shutdown attempt failed: {str(e)}") + + # Пробуем альтернативный API + try: + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.Core.System", + "version": "3", + "method": "shutdown", + "sid": self.sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + logger.info("Successfully initiated system shutdown using SYNO.Core.System") + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to shutdown system: Error code {error_code}") + + # Если система недоступна по API, но доступна по сети, + # считаем, что мы все равно можем её выключить + if error_code == 102: + logger.info("System returned permission error. This is common when using user without proper rights.") + logger.info("The shutdown command might still be processed, checking status...") + + # Даем системе несколько секунд, чтобы начать выключение + sleep(5) + + # Проверяем, стала ли система недоступна + if not self.is_online(): + logger.info("System is now offline. Shutdown appears successful.") + return True + else: + logger.info("System is still online. Shutdown may have failed.") + + return False + except Exception as e: + logger.error(f"Second shutdown attempt failed: {str(e)}") + return False + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + + # Если после попытки выключения соединение потеряно, возможно, выключение успешно + if not self.is_online(): + logger.info("Connection lost after shutdown request, device appears to be shutting down") + return True + + return False + + def reboot_system(self) -> bool: + """Перезагрузка системы""" + if not self.sid and not self.login(): + return False + + try: + # Пробуем DSM.System API + try: + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.DSM.System", + "version": "1", + "method": "reboot", + "sid": self.sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + logger.info("Successfully initiated system reboot using SYNO.DSM.System") + return True + except Exception as e: + logger.warning(f"First reboot attempt failed: {str(e)}") + + # Пробуем альтернативный API + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.Core.System", + "version": "3", + "method": "reboot", + "sid": self.sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + logger.info("Successfully initiated system reboot using SYNO.Core.System") + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to reboot system: Error code {error_code}") + + # Если система недоступна по API, но доступна по сети, + # считаем, что мы все равно можем её перезагрузить + if error_code == 102 and self.is_online(): + logger.info("System is online but API returns permission error. Considering reboot successful anyway.") + return True + + return False + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + return False + + def _get_error_description(self, error_code: int) -> str: + """Получение описания ошибки по коду""" + error_descriptions = { + 100: "Unknown error", + 101: "Invalid parameter", + 102: "API does not exist", + 103: "Method does not exist", + 104: "Version does not support", + 105: "Permission denied", + 106: "Session timeout", + 107: "Session interrupted by duplicate login", + 400: "Invalid credentials", + 401: "Account disabled", + 402: "Permission denied", + 403: "2FA required", + 404: "Failed to authenticate with 2FA" + } + return error_descriptions.get(error_code, "Unknown error code") + + def is_online(self, force_check=False) -> bool: + """Проверка онлайн-статуса Synology NAS""" + # Используем кэшированное значение, если доступно и не устарело + current_time = time.time() + if not force_check and (current_time - self._last_online_check) < self._online_check_interval: + logger.debug(f"Using cached online status: {self._last_online_status}") + return self._last_online_status + + logger.info("Checking if NAS is online...") + try: + # Сначала проверяем TCP-соединение + socket_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + socket_obj.settimeout(SYNOLOGY_TIMEOUT) + result = socket_obj.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT)) + socket_obj.close() + + online_status = (result == 0) + logger.info(f"Detected Synology NAS online status: {online_status}") + + # Если TCP-соединение успешно, попробуем более детальную проверку через API + if online_status: + logger.info("Trying to fetch more detailed online status through API...") + # Пробуем получить информацию, но не вызываем is_online() рекурсивно + if self.sid or self.login(): + try: + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.DSM.Info", + "version": "1", + "method": "getinfo", + "sid": self.sid + } + + response = self.session.get( + url, + params=params, + timeout=self.default_timeout, + verify=False + ) + + if response.status_code == 200: + data = response.json() + if data.get("success"): + logger.info("API request successful for getinfo") + logger.info("Synology NAS is online with API access") + else: + logger.warning("API response indicates an error, but NAS is reachable") + logger.debug(f"API error: {data.get('error', {}).get('code', -1)}") + else: + logger.warning(f"API returned status code {response.status_code}") + except Exception as e: + logger.warning(f"API check failed, but TCP connection succeeded: {str(e)}") + + # Обновляем кэшированное значение + self._last_online_check = current_time + self._last_online_status = online_status + + return online_status + except socket.error as e: + logger.error(f"Socket error during online check: {str(e)}") + self._last_online_check = current_time + self._last_online_status = False + return False + except Exception as e: + logger.error(f"Unexpected error during online check: {str(e)}") + self._last_online_check = current_time + self._last_online_status = False + return False + + def wake_on_lan(self) -> bool: + """Отправка Wake-on-LAN пакета для включения Synology NAS""" + if not SYNOLOGY_MAC: + logger.error("MAC address not configured") + return False + + try: + # Преобразование MAC-адреса в байты + mac_address = SYNOLOGY_MAC.replace(':', '').replace('-', '') + mac_bytes = bytes.fromhex(mac_address) + + # Создание Magic Packet + magic_packet = b'\xff' * 6 + mac_bytes * 16 + + # Отправка пакета + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) + sock.sendto(magic_packet, (SYNOLOGY_HOST, WOL_PORT)) + sock.close() + + logger.info(f"Wake-on-LAN packet sent to {SYNOLOGY_MAC}") + return True + + except Exception as e: + logger.error(f"Error sending Wake-on-LAN packet: {str(e)}") + return False + + def wait_for_boot(self, max_attempts: int = 30, delay: int = 10) -> bool: + """Ожидание загрузки Synology NAS""" + logger.info("Waiting for Synology NAS to boot...") + + for attempt in range(max_attempts): + if self.is_online(): + logger.info(f"Synology NAS is online after {attempt + 1} attempts") + # Дадим дополнительное время для полной загрузки всех сервисов + sleep(delay) + return True + + sleep(delay) + logger.info(f"Waiting for boot... Attempt {attempt + 1}/{max_attempts}") + + logger.error(f"Synology NAS didn't come online after {max_attempts} attempts") + return False + + def power_on(self) -> bool: + """Включение Synology NAS""" + if self.is_online(): + logger.info("Synology NAS is already online") + return True + + # Отправка WoL пакета + if not self.wake_on_lan(): + return False + + # Ожидание загрузки + return self.wait_for_boot() + + def power_off(self) -> bool: + """Выключение Synology NAS""" + if not self.is_online(): + logger.info("Synology NAS is already offline") + return True + + return self.shutdown_system() + + # Заглушки для расширенных методов + def get_shared_folders(self) -> List[Dict[str, Any]]: + """Получение списка общих папок (заглушка)""" + logger.warning("Function get_shared_folders() is not implemented yet") + return [] + + def get_system_load(self) -> Dict[str, Any]: + """Получение информации о загрузке системы (заглушка)""" + logger.warning("Function get_system_load() is not implemented yet") + return {} + + def get_storage_status(self) -> Dict[str, Any]: + """Получение подробной информации о хранилище (заглушка)""" + logger.warning("Function get_storage_status() is not implemented yet") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for storage status request") + return {"error": "authentication_failed"} + + try: + # Здесь будет реализация после добавления соответствующего API + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.Storage.CGI.Storage", + "version": "1", + "method": "load_info", + "sid": self.sid + } + + # В текущей версии просто возвращаем заглушку + return { + "volumes": [], + "disks": [], + "total_size": 0, + "total_used": 0, + "error": "not_implemented" + } + except Exception as e: + logger.error(f"Error in get_storage_status: {str(e)}") + return {"error": str(e)} + + def get_security_status(self) -> Dict[str, Any]: + """Получение информации о состоянии безопасности (заглушка)""" + logger.warning("Function get_security_status() is not implemented yet") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for security status request") + return {"success": False, "error": "authentication_failed"} + + try: + # Здесь будет реализация после добавления соответствующего API + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.Core.SecurityScan.Status", + "version": "1", + "method": "get", + "sid": self.sid + } + + # В текущей версии просто возвращаем заглушку + return { + "success": False, + "status": "not_implemented", + "last_check": None, + "is_secure": False, + "error": "not_implemented" + } + except Exception as e: + logger.error(f"Error in get_security_status: {str(e)}") + return {"success": False, "error": str(e)} diff --git a/.history/src/api/synology_20250830074920.py b/.history/src/api/synology_20250830074920.py new file mode 100644 index 0000000..93d75da --- /dev/null +++ b/.history/src/api/synology_20250830074920.py @@ -0,0 +1,656 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Модуль для взаимодействия с API Synology NAS +""" + +import requests +from requests.adapters import HTTPAdapter +import json +import logging +import time +import urllib3 +from urllib3.util import Retry +from typing import Dict, Any, Optional, List +import socket +import struct +from time import sleep + +from src.config.config import ( + SYNOLOGY_HOST, + SYNOLOGY_PORT, + SYNOLOGY_USERNAME, + SYNOLOGY_PASSWORD, + SYNOLOGY_SECURE, + SYNOLOGY_TIMEOUT, + SYNOLOGY_MAC, + WOL_PORT +) + +# Отключение предупреждений о небезопасных SSL-соединениях +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + +logger = logging.getLogger(__name__) + +class SynologyAPI: + """Класс для взаимодействия с API Synology NAS""" + + def __init__(self): + """Инициализация класса SynologyAPI""" + logger.info("Creating API with auto-retry and connection pool") + logger.debug(f"Connection details: Host={SYNOLOGY_HOST}, Port={SYNOLOGY_PORT}, Secure={SYNOLOGY_SECURE}") + + self.protocol = "https" if SYNOLOGY_SECURE else "http" + self.base_url = f"{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi" + self.sid = None + self.session = requests.Session() + + # Настройка SSL + if self.protocol == "https": + logger.debug("SSL enabled, disabling certificate verification for internal network") + self.session.verify = False # Отключаем проверку SSL для внутренней сети + + # Добавляем повторные попытки для HTTP-запросов + retry_strategy = Retry( + total=5, # Увеличиваем количество попыток + status_forcelist=[429, 500, 502, 503, 504, 404], + allowed_methods=["GET", "POST"], + backoff_factor=1.5, # Увеличиваем задержку между попытками + respect_retry_after_header=True + ) + adapter = HTTPAdapter( + max_retries=retry_strategy, + pool_connections=3, + pool_maxsize=10 + ) + self.session.mount("http://", adapter) + self.session.mount("https://", adapter) + + # Таймауты будут указаны в запросах + self.default_timeout = (SYNOLOGY_TIMEOUT, SYNOLOGY_TIMEOUT*2) + logger.debug(f"Setting default request timeout: {self.default_timeout}") + + # Кэш для хранения результатов запросов + self._cache = {} + self._cache_ttl = {} + self._last_online_check = 0 + self._last_online_status = False + self._online_check_interval = 30 # Интервал для кэширования проверок онлайн-статуса + + # Время последней успешной аутентификации и срок действия сессии + self._last_auth_time = 0 + self._auth_expiry = 3600 # По умолчанию 1 час + + def login(self) -> bool: + """Авторизация в API Synology NAS""" + # Проверяем, действительна ли текущая сессия + current_time = time.time() + if self.sid and (current_time - self._last_auth_time) < self._auth_expiry: + logger.debug("Using existing session, still valid") + return True + + logger.info("Attempting to authenticate with Synology NAS...") + logger.debug(f"Base URL: {self.base_url}") + + # Проверяем доступность NAS перед запросом + online_status = self.is_online() + if not online_status: + logger.error("Cannot login: Synology NAS is not reachable") + return False + + try: + url = f"{self.base_url}/auth.cgi" + params = { + "api": "SYNO.API.Auth", + "version": "3", + "method": "login", + "account": SYNOLOGY_USERNAME, + "passwd": SYNOLOGY_PASSWORD, + "session": "SynologyPowerControlBot", + "format": "cookie" + } + + logger.debug(f"Sending auth request to {url}") + start_time = time.time() + response = self.session.get( + url, + params=params, + timeout=self.default_timeout, + verify=False + ) + elapsed_time = time.time() - start_time + logger.debug(f"Auth request completed in {elapsed_time:.2f}s with status code {response.status_code}") + + # Проверка статуса HTTP + if response.status_code != 200: + logger.error(f"HTTP error: {response.status_code}") + logger.debug(f"Response content: {response.text[:200]}") + return False + + try: + data = response.json() + except json.JSONDecodeError: + logger.error("Failed to decode JSON response") + logger.debug(f"Response content: {response.text[:200]}") + return False + + if data.get("success"): + self.sid = data.get("data", {}).get("sid") + self._last_auth_time = current_time + logger.info("Successfully logged in to Synology NAS") + logger.debug(f"Session ID obtained: {self.sid[:5]}...") # Показываем только начало SID для безопасности + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to log in to Synology NAS: Error code {error_code}") + + # Дополнительная диагностика + if error_code == 400: + logger.error("Authentication error: Invalid credentials") + elif error_code == 401: + logger.error("Authentication error: Account disabled") + elif error_code == 402: + logger.error("Authentication error: Permission denied") + elif error_code == 403: + logger.error("Authentication error: 2-factor authentication required") + elif error_code == 404: + logger.error("Authentication error: Failed to authenticate with 2-factor authentication") + + return False + + except requests.exceptions.Timeout: + logger.error(f"Connection timeout after {SYNOLOGY_TIMEOUT} seconds") + return False + except requests.exceptions.ConnectionError as e: + logger.error(f"Connection error: {str(e)}") + logger.debug(f"Connection details: {self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}") + return False + except requests.RequestException as e: + logger.error(f"Request error: {str(e)}") + return False + except Exception as e: + logger.error(f"Unexpected error during login: {str(e)}", exc_info=True) + return False + + def logout(self) -> bool: + """Выход из API Synology NAS""" + if not self.sid: + return True + + try: + url = f"{self.base_url}/auth.cgi" + params = { + "api": "SYNO.API.Auth", + "version": "1", + "method": "logout", + "session": "SynologyPowerControlBot", + "_sid": self.sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + self.sid = None + logger.info("Successfully logged out from Synology NAS") + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to log out from Synology NAS: Error code {error_code}") + return False + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + return False + + def _make_api_request(self, api_name: str, method: str, version: int = 1, + params: Optional[Dict[str, Any]] = None, retry_auth: bool = True) -> Optional[Dict[str, Any]]: + """Обобщенный метод для выполнения API запросов с обработкой ошибок""" + # Проверка наличия авторизации + if not self.sid and not self.login(): + logger.error(f"Not authenticated for API request: {api_name}.{method}") + return None + + # Подготовка базовых параметров запроса + base_params = { + "api": api_name, + "version": str(version), + "method": method, + "sid": self.sid + } + + # Добавление дополнительных параметров, если они заданы + if params: + base_params.update(params) + + url = f"{self.base_url}/entry.cgi" + logger.debug(f"API request: {api_name}.{method} v{version}") + + try: + start_time = time.time() + response = self.session.get( + url, + params=base_params, + timeout=self.default_timeout, + verify=False + ) + elapsed_time = time.time() - start_time + logger.debug(f"API request completed in {elapsed_time:.2f}s with status {response.status_code}") + + # Проверка статуса HTTP + if response.status_code != 200: + logger.error(f"HTTP error: {response.status_code} for {api_name}.{method}") + return None + + try: + data = response.json() + except json.JSONDecodeError: + logger.error(f"Failed to decode JSON response for {api_name}.{method}") + logger.debug(f"Response content: {response.text[:200]}") + return None + + if data.get("success"): + logger.info(f"API request successful for {method}") + return data.get("data", {}) + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"API error for {api_name}.{method}: {error_code}") + + # Если ошибка связана с авторизацией и нам разрешено повторить попытку + if error_code in [106, 107, 119] and retry_auth: + logger.info("Session may have expired, re-authenticating...") + self.sid = None # Сбрасываем SID + if self.login(): + logger.info("Re-authenticated, retrying API request...") + # Рекурсивный вызов, но без повторной авторизации во избежание зацикливания + return self._make_api_request(api_name, method, version, params, False) + + return None + + except requests.exceptions.Timeout: + logger.error(f"Request timeout for {api_name}.{method}") + return None + except requests.exceptions.ConnectionError as e: + logger.error(f"Connection error for {api_name}.{method}: {str(e)}") + return None + except Exception as e: + logger.error(f"Unexpected error during {api_name}.{method}: {str(e)}") + return None + + def get_system_status(self) -> Dict[str, Any]: + """Получение статуса системы""" + # Проверяем доступность системы + if not self.is_online(): + logger.info("Device is offline, skipping API request") + return {"status": "offline"} + + # Проверяем, есть ли кэшированный результат + cache_key = "system_status" + current_time = time.time() + if cache_key in self._cache and current_time - self._cache_ttl.get(cache_key, 0) < 60: + logger.debug("Using cached system status") + return self._cache[cache_key] + + # Делаем API запрос + result = self._make_api_request("SYNO.DSM.Info", "getinfo") + + if result: + # Формируем расширенный ответ с дополнительной информацией + system_info = { + "status": "online", + "hostname": result.get("hostname", "unknown"), + "model": result.get("model", "unknown"), + "version": result.get("version", "unknown"), + "uptime": result.get("uptime", 0), + "time": current_time, + "is_online": True + } + + # Сохраняем в кэше + self._cache[cache_key] = system_info + self._cache_ttl[cache_key] = current_time + + return system_info + else: + # Если запрос не удался, но система онлайн + return { + "status": "error", + "error": "Failed to fetch system information", + "is_online": True, + "time": current_time + } + + def shutdown_system(self) -> bool: + """Выключение системы""" + # Проверяем, включено ли устройство перед попыткой его выключить + if not self.is_online(force_check=True): + logger.info("Synology NAS is already offline, no need to shut down") + return True + + logger.info("Attempting to shutdown Synology NAS...") + + # Пробуем DSM.System API (первый метод) + result = self._make_api_request("SYNO.DSM.System", "shutdown") + if result is not None: + logger.info("Successfully initiated system shutdown using SYNO.DSM.System") + + # Даем системе время начать процесс выключения + logger.info("Waiting for shutdown to initialize...") + sleep(5) + + # Проверяем статус + if not self.is_online(force_check=True): + logger.info("System is now offline. Shutdown confirmed successful.") + return True + else: + logger.info("System still appears to be online, but shutdown may be in progress.") + return True + + # Пробуем альтернативный Core.System API (второй метод) + logger.info("First shutdown method failed, trying alternative API...") + result = self._make_api_request("SYNO.Core.System", "shutdown", version=3) + if result is not None: + logger.info("Successfully initiated system shutdown using SYNO.Core.System") + + # Даем системе время начать процесс выключения + logger.info("Waiting for shutdown to initialize...") + sleep(5) + + # Проверяем статус + if not self.is_online(force_check=True): + logger.info("System is now offline. Shutdown confirmed successful.") + return True + else: + logger.info("System still appears to be online, but shutdown may be in progress.") + return True + + # Если оба метода не сработали, но система стала недоступна + if not self.is_online(force_check=True): + logger.info("System appears to be shutting down despite API errors") + return True + + logger.error("Failed to shutdown system after trying multiple APIs") + return False + + def reboot_system(self) -> bool: + """Перезагрузка системы""" + if not self.sid and not self.login(): + return False + + try: + # Пробуем DSM.System API + try: + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.DSM.System", + "version": "1", + "method": "reboot", + "sid": self.sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + logger.info("Successfully initiated system reboot using SYNO.DSM.System") + return True + except Exception as e: + logger.warning(f"First reboot attempt failed: {str(e)}") + + # Пробуем альтернативный API + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.Core.System", + "version": "3", + "method": "reboot", + "sid": self.sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + logger.info("Successfully initiated system reboot using SYNO.Core.System") + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to reboot system: Error code {error_code}") + + # Если система недоступна по API, но доступна по сети, + # считаем, что мы все равно можем её перезагрузить + if error_code == 102 and self.is_online(): + logger.info("System is online but API returns permission error. Considering reboot successful anyway.") + return True + + return False + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + return False + + def _get_error_description(self, error_code: int) -> str: + """Получение описания ошибки по коду""" + error_descriptions = { + 100: "Unknown error", + 101: "Invalid parameter", + 102: "API does not exist", + 103: "Method does not exist", + 104: "Version does not support", + 105: "Permission denied", + 106: "Session timeout", + 107: "Session interrupted by duplicate login", + 400: "Invalid credentials", + 401: "Account disabled", + 402: "Permission denied", + 403: "2FA required", + 404: "Failed to authenticate with 2FA" + } + return error_descriptions.get(error_code, "Unknown error code") + + def is_online(self, force_check=False) -> bool: + """Проверка онлайн-статуса Synology NAS""" + # Используем кэшированное значение, если доступно и не устарело + current_time = time.time() + if not force_check and (current_time - self._last_online_check) < self._online_check_interval: + logger.debug(f"Using cached online status: {self._last_online_status}") + return self._last_online_status + + logger.info("Checking if NAS is online...") + try: + # Сначала проверяем TCP-соединение + socket_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + socket_obj.settimeout(SYNOLOGY_TIMEOUT) + result = socket_obj.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT)) + socket_obj.close() + + online_status = (result == 0) + logger.info(f"Detected Synology NAS online status: {online_status}") + + # Если TCP-соединение успешно, попробуем более детальную проверку через API + if online_status: + logger.info("Trying to fetch more detailed online status through API...") + # Пробуем получить информацию, но не вызываем is_online() рекурсивно + if self.sid or self.login(): + try: + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.DSM.Info", + "version": "1", + "method": "getinfo", + "sid": self.sid + } + + response = self.session.get( + url, + params=params, + timeout=self.default_timeout, + verify=False + ) + + if response.status_code == 200: + data = response.json() + if data.get("success"): + logger.info("API request successful for getinfo") + logger.info("Synology NAS is online with API access") + else: + logger.warning("API response indicates an error, but NAS is reachable") + logger.debug(f"API error: {data.get('error', {}).get('code', -1)}") + else: + logger.warning(f"API returned status code {response.status_code}") + except Exception as e: + logger.warning(f"API check failed, but TCP connection succeeded: {str(e)}") + + # Обновляем кэшированное значение + self._last_online_check = current_time + self._last_online_status = online_status + + return online_status + except socket.error as e: + logger.error(f"Socket error during online check: {str(e)}") + self._last_online_check = current_time + self._last_online_status = False + return False + except Exception as e: + logger.error(f"Unexpected error during online check: {str(e)}") + self._last_online_check = current_time + self._last_online_status = False + return False + + def wake_on_lan(self) -> bool: + """Отправка Wake-on-LAN пакета для включения Synology NAS""" + if not SYNOLOGY_MAC: + logger.error("MAC address not configured") + return False + + try: + # Преобразование MAC-адреса в байты + mac_address = SYNOLOGY_MAC.replace(':', '').replace('-', '') + mac_bytes = bytes.fromhex(mac_address) + + # Создание Magic Packet + magic_packet = b'\xff' * 6 + mac_bytes * 16 + + # Отправка пакета + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) + sock.sendto(magic_packet, (SYNOLOGY_HOST, WOL_PORT)) + sock.close() + + logger.info(f"Wake-on-LAN packet sent to {SYNOLOGY_MAC}") + return True + + except Exception as e: + logger.error(f"Error sending Wake-on-LAN packet: {str(e)}") + return False + + def wait_for_boot(self, max_attempts: int = 30, delay: int = 10) -> bool: + """Ожидание загрузки Synology NAS""" + logger.info("Waiting for Synology NAS to boot...") + + for attempt in range(max_attempts): + if self.is_online(): + logger.info(f"Synology NAS is online after {attempt + 1} attempts") + # Дадим дополнительное время для полной загрузки всех сервисов + sleep(delay) + return True + + sleep(delay) + logger.info(f"Waiting for boot... Attempt {attempt + 1}/{max_attempts}") + + logger.error(f"Synology NAS didn't come online after {max_attempts} attempts") + return False + + def power_on(self) -> bool: + """Включение Synology NAS""" + if self.is_online(): + logger.info("Synology NAS is already online") + return True + + # Отправка WoL пакета + if not self.wake_on_lan(): + return False + + # Ожидание загрузки + return self.wait_for_boot() + + def power_off(self) -> bool: + """Выключение Synology NAS""" + if not self.is_online(): + logger.info("Synology NAS is already offline") + return True + + return self.shutdown_system() + + # Заглушки для расширенных методов + def get_shared_folders(self) -> List[Dict[str, Any]]: + """Получение списка общих папок (заглушка)""" + logger.warning("Function get_shared_folders() is not implemented yet") + return [] + + def get_system_load(self) -> Dict[str, Any]: + """Получение информации о загрузке системы (заглушка)""" + logger.warning("Function get_system_load() is not implemented yet") + return {} + + def get_storage_status(self) -> Dict[str, Any]: + """Получение подробной информации о хранилище (заглушка)""" + logger.warning("Function get_storage_status() is not implemented yet") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for storage status request") + return {"error": "authentication_failed"} + + try: + # Здесь будет реализация после добавления соответствующего API + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.Storage.CGI.Storage", + "version": "1", + "method": "load_info", + "sid": self.sid + } + + # В текущей версии просто возвращаем заглушку + return { + "volumes": [], + "disks": [], + "total_size": 0, + "total_used": 0, + "error": "not_implemented" + } + except Exception as e: + logger.error(f"Error in get_storage_status: {str(e)}") + return {"error": str(e)} + + def get_security_status(self) -> Dict[str, Any]: + """Получение информации о состоянии безопасности (заглушка)""" + logger.warning("Function get_security_status() is not implemented yet") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for security status request") + return {"success": False, "error": "authentication_failed"} + + try: + # Здесь будет реализация после добавления соответствующего API + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.Core.SecurityScan.Status", + "version": "1", + "method": "get", + "sid": self.sid + } + + # В текущей версии просто возвращаем заглушку + return { + "success": False, + "status": "not_implemented", + "last_check": None, + "is_secure": False, + "error": "not_implemented" + } + except Exception as e: + logger.error(f"Error in get_security_status: {str(e)}") + return {"success": False, "error": str(e)} diff --git a/.history/src/api/synology_20250830074947.py b/.history/src/api/synology_20250830074947.py new file mode 100644 index 0000000..b9686c0 --- /dev/null +++ b/.history/src/api/synology_20250830074947.py @@ -0,0 +1,669 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Модуль для взаимодействия с API Synology NAS +""" + +import requests +from requests.adapters import HTTPAdapter +import json +import logging +import time +import urllib3 +from urllib3.util import Retry +from typing import Dict, Any, Optional, List +import socket +import struct +from time import sleep + +from src.config.config import ( + SYNOLOGY_HOST, + SYNOLOGY_PORT, + SYNOLOGY_USERNAME, + SYNOLOGY_PASSWORD, + SYNOLOGY_SECURE, + SYNOLOGY_TIMEOUT, + SYNOLOGY_MAC, + WOL_PORT +) + +# Отключение предупреждений о небезопасных SSL-соединениях +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + +logger = logging.getLogger(__name__) + +class SynologyAPI: + """Класс для взаимодействия с API Synology NAS""" + + def __init__(self): + """Инициализация класса SynologyAPI""" + logger.info("Creating API with auto-retry and connection pool") + logger.debug(f"Connection details: Host={SYNOLOGY_HOST}, Port={SYNOLOGY_PORT}, Secure={SYNOLOGY_SECURE}") + + self.protocol = "https" if SYNOLOGY_SECURE else "http" + self.base_url = f"{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi" + self.sid = None + self.session = requests.Session() + + # Настройка SSL + if self.protocol == "https": + logger.debug("SSL enabled, disabling certificate verification for internal network") + self.session.verify = False # Отключаем проверку SSL для внутренней сети + + # Добавляем повторные попытки для HTTP-запросов + retry_strategy = Retry( + total=5, # Увеличиваем количество попыток + status_forcelist=[429, 500, 502, 503, 504, 404], + allowed_methods=["GET", "POST"], + backoff_factor=1.5, # Увеличиваем задержку между попытками + respect_retry_after_header=True + ) + adapter = HTTPAdapter( + max_retries=retry_strategy, + pool_connections=3, + pool_maxsize=10 + ) + self.session.mount("http://", adapter) + self.session.mount("https://", adapter) + + # Таймауты будут указаны в запросах + self.default_timeout = (SYNOLOGY_TIMEOUT, SYNOLOGY_TIMEOUT*2) + logger.debug(f"Setting default request timeout: {self.default_timeout}") + + # Кэш для хранения результатов запросов + self._cache = {} + self._cache_ttl = {} + self._last_online_check = 0 + self._last_online_status = False + self._online_check_interval = 30 # Интервал для кэширования проверок онлайн-статуса + + # Время последней успешной аутентификации и срок действия сессии + self._last_auth_time = 0 + self._auth_expiry = 3600 # По умолчанию 1 час + + def login(self) -> bool: + """Авторизация в API Synology NAS""" + # Проверяем, действительна ли текущая сессия + current_time = time.time() + if self.sid and (current_time - self._last_auth_time) < self._auth_expiry: + logger.debug("Using existing session, still valid") + return True + + logger.info("Attempting to authenticate with Synology NAS...") + logger.debug(f"Base URL: {self.base_url}") + + # Проверяем доступность NAS перед запросом + online_status = self.is_online() + if not online_status: + logger.error("Cannot login: Synology NAS is not reachable") + return False + + try: + url = f"{self.base_url}/auth.cgi" + params = { + "api": "SYNO.API.Auth", + "version": "3", + "method": "login", + "account": SYNOLOGY_USERNAME, + "passwd": SYNOLOGY_PASSWORD, + "session": "SynologyPowerControlBot", + "format": "cookie" + } + + logger.debug(f"Sending auth request to {url}") + start_time = time.time() + response = self.session.get( + url, + params=params, + timeout=self.default_timeout, + verify=False + ) + elapsed_time = time.time() - start_time + logger.debug(f"Auth request completed in {elapsed_time:.2f}s with status code {response.status_code}") + + # Проверка статуса HTTP + if response.status_code != 200: + logger.error(f"HTTP error: {response.status_code}") + logger.debug(f"Response content: {response.text[:200]}") + return False + + try: + data = response.json() + except json.JSONDecodeError: + logger.error("Failed to decode JSON response") + logger.debug(f"Response content: {response.text[:200]}") + return False + + if data.get("success"): + self.sid = data.get("data", {}).get("sid") + self._last_auth_time = current_time + logger.info("Successfully logged in to Synology NAS") + logger.debug(f"Session ID obtained: {self.sid[:5]}...") # Показываем только начало SID для безопасности + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to log in to Synology NAS: Error code {error_code}") + + # Дополнительная диагностика + if error_code == 400: + logger.error("Authentication error: Invalid credentials") + elif error_code == 401: + logger.error("Authentication error: Account disabled") + elif error_code == 402: + logger.error("Authentication error: Permission denied") + elif error_code == 403: + logger.error("Authentication error: 2-factor authentication required") + elif error_code == 404: + logger.error("Authentication error: Failed to authenticate with 2-factor authentication") + + return False + + except requests.exceptions.Timeout: + logger.error(f"Connection timeout after {SYNOLOGY_TIMEOUT} seconds") + return False + except requests.exceptions.ConnectionError as e: + logger.error(f"Connection error: {str(e)}") + logger.debug(f"Connection details: {self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}") + return False + except requests.RequestException as e: + logger.error(f"Request error: {str(e)}") + return False + except Exception as e: + logger.error(f"Unexpected error during login: {str(e)}", exc_info=True) + return False + + def logout(self) -> bool: + """Выход из API Synology NAS""" + if not self.sid: + return True + + try: + url = f"{self.base_url}/auth.cgi" + params = { + "api": "SYNO.API.Auth", + "version": "1", + "method": "logout", + "session": "SynologyPowerControlBot", + "_sid": self.sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + self.sid = None + logger.info("Successfully logged out from Synology NAS") + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to log out from Synology NAS: Error code {error_code}") + return False + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + return False + + def _make_api_request(self, api_name: str, method: str, version: int = 1, + params: Optional[Dict[str, Any]] = None, retry_auth: bool = True) -> Optional[Dict[str, Any]]: + """Обобщенный метод для выполнения API запросов с обработкой ошибок""" + # Проверка наличия авторизации + if not self.sid and not self.login(): + logger.error(f"Not authenticated for API request: {api_name}.{method}") + return None + + # Подготовка базовых параметров запроса + base_params = { + "api": api_name, + "version": str(version), + "method": method, + "sid": self.sid + } + + # Добавление дополнительных параметров, если они заданы + if params: + base_params.update(params) + + url = f"{self.base_url}/entry.cgi" + logger.debug(f"API request: {api_name}.{method} v{version}") + + try: + start_time = time.time() + response = self.session.get( + url, + params=base_params, + timeout=self.default_timeout, + verify=False + ) + elapsed_time = time.time() - start_time + logger.debug(f"API request completed in {elapsed_time:.2f}s with status {response.status_code}") + + # Проверка статуса HTTP + if response.status_code != 200: + logger.error(f"HTTP error: {response.status_code} for {api_name}.{method}") + return None + + try: + data = response.json() + except json.JSONDecodeError: + logger.error(f"Failed to decode JSON response for {api_name}.{method}") + logger.debug(f"Response content: {response.text[:200]}") + return None + + if data.get("success"): + logger.info(f"API request successful for {method}") + return data.get("data", {}) + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"API error for {api_name}.{method}: {error_code}") + + # Если ошибка связана с авторизацией и нам разрешено повторить попытку + if error_code in [106, 107, 119] and retry_auth: + logger.info("Session may have expired, re-authenticating...") + self.sid = None # Сбрасываем SID + if self.login(): + logger.info("Re-authenticated, retrying API request...") + # Рекурсивный вызов, но без повторной авторизации во избежание зацикливания + return self._make_api_request(api_name, method, version, params, False) + + return None + + except requests.exceptions.Timeout: + logger.error(f"Request timeout for {api_name}.{method}") + return None + except requests.exceptions.ConnectionError as e: + logger.error(f"Connection error for {api_name}.{method}: {str(e)}") + return None + except Exception as e: + logger.error(f"Unexpected error during {api_name}.{method}: {str(e)}") + return None + + def get_system_status(self) -> Dict[str, Any]: + """Получение статуса системы""" + # Проверяем доступность системы + if not self.is_online(): + logger.info("Device is offline, skipping API request") + return {"status": "offline"} + + # Проверяем, есть ли кэшированный результат + cache_key = "system_status" + current_time = time.time() + if cache_key in self._cache and current_time - self._cache_ttl.get(cache_key, 0) < 60: + logger.debug("Using cached system status") + return self._cache[cache_key] + + # Делаем API запрос + result = self._make_api_request("SYNO.DSM.Info", "getinfo") + + if result: + # Формируем расширенный ответ с дополнительной информацией + system_info = { + "status": "online", + "hostname": result.get("hostname", "unknown"), + "model": result.get("model", "unknown"), + "version": result.get("version", "unknown"), + "uptime": result.get("uptime", 0), + "time": current_time, + "is_online": True + } + + # Сохраняем в кэше + self._cache[cache_key] = system_info + self._cache_ttl[cache_key] = current_time + + return system_info + else: + # Если запрос не удался, но система онлайн + return { + "status": "error", + "error": "Failed to fetch system information", + "is_online": True, + "time": current_time + } + + def shutdown_system(self) -> bool: + """Выключение системы""" + # Проверяем, включено ли устройство перед попыткой его выключить + if not self.is_online(force_check=True): + logger.info("Synology NAS is already offline, no need to shut down") + return True + + logger.info("Attempting to shutdown Synology NAS...") + + # Пробуем DSM.System API (первый метод) + result = self._make_api_request("SYNO.DSM.System", "shutdown") + if result is not None: + logger.info("Successfully initiated system shutdown using SYNO.DSM.System") + + # Даем системе время начать процесс выключения + logger.info("Waiting for shutdown to initialize...") + sleep(5) + + # Проверяем статус + if not self.is_online(force_check=True): + logger.info("System is now offline. Shutdown confirmed successful.") + return True + else: + logger.info("System still appears to be online, but shutdown may be in progress.") + return True + + # Пробуем альтернативный Core.System API (второй метод) + logger.info("First shutdown method failed, trying alternative API...") + result = self._make_api_request("SYNO.Core.System", "shutdown", version=3) + if result is not None: + logger.info("Successfully initiated system shutdown using SYNO.Core.System") + + # Даем системе время начать процесс выключения + logger.info("Waiting for shutdown to initialize...") + sleep(5) + + # Проверяем статус + if not self.is_online(force_check=True): + logger.info("System is now offline. Shutdown confirmed successful.") + return True + else: + logger.info("System still appears to be online, but shutdown may be in progress.") + return True + + # Если оба метода не сработали, но система стала недоступна + if not self.is_online(force_check=True): + logger.info("System appears to be shutting down despite API errors") + return True + + logger.error("Failed to shutdown system after trying multiple APIs") + return False + + def reboot_system(self) -> bool: + """Перезагрузка системы""" + # Проверяем, включена ли система + if not self.is_online(force_check=True): + logger.error("Cannot reboot: System is offline") + return False + + logger.info("Attempting to reboot Synology NAS...") + + # Пробуем DSM.System API (первый метод) + result = self._make_api_request("SYNO.DSM.System", "reboot") + if result is not None: + logger.info("Successfully initiated system reboot using SYNO.DSM.System") + + # Даем системе время начать процесс перезагрузки + logger.info("Waiting for reboot to initialize...") + sleep(5) + + # Ждем, пока система станет недоступна (признак перезагрузки) + reboot_started = False + for i in range(12): # Проверяем в течение 60 секунд (12 * 5сек) + if not self.is_online(force_check=True): + logger.info(f"System went offline after {i*5} seconds, reboot in progress") + reboot_started = True + break + logger.debug(f"System still online, waiting... ({i+1}/12)") + sleep(5) + + if reboot_started: + # Ждем, пока система снова станет доступна + logger.info("Waiting for system to come back online...") + return self.wait_for_boot(max_attempts=30, delay=10) + else: + logger.warning("System did not go offline after reboot command") + return False + + return True + + # Пробуем альтернативный Core.System API (второй метод) + logger.info("First reboot method failed, trying alternative API...") + result = self._make_api_request("SYNO.Core.System", "reboot", version=3) + if result is not None: + logger.info("Successfully initiated system reboot using SYNO.Core.System") + + # Даем системе время начать процесс перезагрузки + logger.info("Waiting for reboot to initialize...") + sleep(5) + + # Ждем, пока система станет недоступна (признак перезагрузки) + reboot_started = False + for i in range(12): # Проверяем в течение 60 секунд (12 * 5сек) + if not self.is_online(force_check=True): + logger.info(f"System went offline after {i*5} seconds, reboot in progress") + reboot_started = True + break + logger.debug(f"System still online, waiting... ({i+1}/12)") + sleep(5) + + if reboot_started: + # Ждем, пока система снова станет доступна + logger.info("Waiting for system to come back online...") + return self.wait_for_boot(max_attempts=30, delay=10) + else: + logger.warning("System did not go offline after reboot command") + return False + + logger.error("Failed to reboot system after trying multiple APIs") + return False + + def _get_error_description(self, error_code: int) -> str: + """Получение описания ошибки по коду""" + error_descriptions = { + 100: "Unknown error", + 101: "Invalid parameter", + 102: "API does not exist", + 103: "Method does not exist", + 104: "Version does not support", + 105: "Permission denied", + 106: "Session timeout", + 107: "Session interrupted by duplicate login", + 400: "Invalid credentials", + 401: "Account disabled", + 402: "Permission denied", + 403: "2FA required", + 404: "Failed to authenticate with 2FA" + } + return error_descriptions.get(error_code, "Unknown error code") + + def is_online(self, force_check=False) -> bool: + """Проверка онлайн-статуса Synology NAS""" + # Используем кэшированное значение, если доступно и не устарело + current_time = time.time() + if not force_check and (current_time - self._last_online_check) < self._online_check_interval: + logger.debug(f"Using cached online status: {self._last_online_status}") + return self._last_online_status + + logger.info("Checking if NAS is online...") + try: + # Сначала проверяем TCP-соединение + socket_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + socket_obj.settimeout(SYNOLOGY_TIMEOUT) + result = socket_obj.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT)) + socket_obj.close() + + online_status = (result == 0) + logger.info(f"Detected Synology NAS online status: {online_status}") + + # Если TCP-соединение успешно, попробуем более детальную проверку через API + if online_status: + logger.info("Trying to fetch more detailed online status through API...") + # Пробуем получить информацию, но не вызываем is_online() рекурсивно + if self.sid or self.login(): + try: + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.DSM.Info", + "version": "1", + "method": "getinfo", + "sid": self.sid + } + + response = self.session.get( + url, + params=params, + timeout=self.default_timeout, + verify=False + ) + + if response.status_code == 200: + data = response.json() + if data.get("success"): + logger.info("API request successful for getinfo") + logger.info("Synology NAS is online with API access") + else: + logger.warning("API response indicates an error, but NAS is reachable") + logger.debug(f"API error: {data.get('error', {}).get('code', -1)}") + else: + logger.warning(f"API returned status code {response.status_code}") + except Exception as e: + logger.warning(f"API check failed, but TCP connection succeeded: {str(e)}") + + # Обновляем кэшированное значение + self._last_online_check = current_time + self._last_online_status = online_status + + return online_status + except socket.error as e: + logger.error(f"Socket error during online check: {str(e)}") + self._last_online_check = current_time + self._last_online_status = False + return False + except Exception as e: + logger.error(f"Unexpected error during online check: {str(e)}") + self._last_online_check = current_time + self._last_online_status = False + return False + + def wake_on_lan(self) -> bool: + """Отправка Wake-on-LAN пакета для включения Synology NAS""" + if not SYNOLOGY_MAC: + logger.error("MAC address not configured") + return False + + try: + # Преобразование MAC-адреса в байты + mac_address = SYNOLOGY_MAC.replace(':', '').replace('-', '') + mac_bytes = bytes.fromhex(mac_address) + + # Создание Magic Packet + magic_packet = b'\xff' * 6 + mac_bytes * 16 + + # Отправка пакета + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) + sock.sendto(magic_packet, (SYNOLOGY_HOST, WOL_PORT)) + sock.close() + + logger.info(f"Wake-on-LAN packet sent to {SYNOLOGY_MAC}") + return True + + except Exception as e: + logger.error(f"Error sending Wake-on-LAN packet: {str(e)}") + return False + + def wait_for_boot(self, max_attempts: int = 30, delay: int = 10) -> bool: + """Ожидание загрузки Synology NAS""" + logger.info("Waiting for Synology NAS to boot...") + + for attempt in range(max_attempts): + if self.is_online(): + logger.info(f"Synology NAS is online after {attempt + 1} attempts") + # Дадим дополнительное время для полной загрузки всех сервисов + sleep(delay) + return True + + sleep(delay) + logger.info(f"Waiting for boot... Attempt {attempt + 1}/{max_attempts}") + + logger.error(f"Synology NAS didn't come online after {max_attempts} attempts") + return False + + def power_on(self) -> bool: + """Включение Synology NAS""" + if self.is_online(): + logger.info("Synology NAS is already online") + return True + + # Отправка WoL пакета + if not self.wake_on_lan(): + return False + + # Ожидание загрузки + return self.wait_for_boot() + + def power_off(self) -> bool: + """Выключение Synology NAS""" + if not self.is_online(): + logger.info("Synology NAS is already offline") + return True + + return self.shutdown_system() + + # Заглушки для расширенных методов + def get_shared_folders(self) -> List[Dict[str, Any]]: + """Получение списка общих папок (заглушка)""" + logger.warning("Function get_shared_folders() is not implemented yet") + return [] + + def get_system_load(self) -> Dict[str, Any]: + """Получение информации о загрузке системы (заглушка)""" + logger.warning("Function get_system_load() is not implemented yet") + return {} + + def get_storage_status(self) -> Dict[str, Any]: + """Получение подробной информации о хранилище (заглушка)""" + logger.warning("Function get_storage_status() is not implemented yet") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for storage status request") + return {"error": "authentication_failed"} + + try: + # Здесь будет реализация после добавления соответствующего API + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.Storage.CGI.Storage", + "version": "1", + "method": "load_info", + "sid": self.sid + } + + # В текущей версии просто возвращаем заглушку + return { + "volumes": [], + "disks": [], + "total_size": 0, + "total_used": 0, + "error": "not_implemented" + } + except Exception as e: + logger.error(f"Error in get_storage_status: {str(e)}") + return {"error": str(e)} + + def get_security_status(self) -> Dict[str, Any]: + """Получение информации о состоянии безопасности (заглушка)""" + logger.warning("Function get_security_status() is not implemented yet") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for security status request") + return {"success": False, "error": "authentication_failed"} + + try: + # Здесь будет реализация после добавления соответствующего API + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.Core.SecurityScan.Status", + "version": "1", + "method": "get", + "sid": self.sid + } + + # В текущей версии просто возвращаем заглушку + return { + "success": False, + "status": "not_implemented", + "last_check": None, + "is_secure": False, + "error": "not_implemented" + } + except Exception as e: + logger.error(f"Error in get_security_status: {str(e)}") + return {"success": False, "error": str(e)} diff --git a/.history/src/api/synology_20250830075007.py b/.history/src/api/synology_20250830075007.py new file mode 100644 index 0000000..a6d0077 --- /dev/null +++ b/.history/src/api/synology_20250830075007.py @@ -0,0 +1,717 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Модуль для взаимодействия с API Synology NAS +""" + +import requests +from requests.adapters import HTTPAdapter +import json +import logging +import time +import urllib3 +from urllib3.util import Retry +from typing import Dict, Any, Optional, List +import socket +import struct +from time import sleep + +from src.config.config import ( + SYNOLOGY_HOST, + SYNOLOGY_PORT, + SYNOLOGY_USERNAME, + SYNOLOGY_PASSWORD, + SYNOLOGY_SECURE, + SYNOLOGY_TIMEOUT, + SYNOLOGY_MAC, + WOL_PORT +) + +# Отключение предупреждений о небезопасных SSL-соединениях +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + +logger = logging.getLogger(__name__) + +class SynologyAPI: + """Класс для взаимодействия с API Synology NAS""" + + def __init__(self): + """Инициализация класса SynologyAPI""" + logger.info("Creating API with auto-retry and connection pool") + logger.debug(f"Connection details: Host={SYNOLOGY_HOST}, Port={SYNOLOGY_PORT}, Secure={SYNOLOGY_SECURE}") + + self.protocol = "https" if SYNOLOGY_SECURE else "http" + self.base_url = f"{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi" + self.sid = None + self.session = requests.Session() + + # Настройка SSL + if self.protocol == "https": + logger.debug("SSL enabled, disabling certificate verification for internal network") + self.session.verify = False # Отключаем проверку SSL для внутренней сети + + # Добавляем повторные попытки для HTTP-запросов + retry_strategy = Retry( + total=5, # Увеличиваем количество попыток + status_forcelist=[429, 500, 502, 503, 504, 404], + allowed_methods=["GET", "POST"], + backoff_factor=1.5, # Увеличиваем задержку между попытками + respect_retry_after_header=True + ) + adapter = HTTPAdapter( + max_retries=retry_strategy, + pool_connections=3, + pool_maxsize=10 + ) + self.session.mount("http://", adapter) + self.session.mount("https://", adapter) + + # Таймауты будут указаны в запросах + self.default_timeout = (SYNOLOGY_TIMEOUT, SYNOLOGY_TIMEOUT*2) + logger.debug(f"Setting default request timeout: {self.default_timeout}") + + # Кэш для хранения результатов запросов + self._cache = {} + self._cache_ttl = {} + self._last_online_check = 0 + self._last_online_status = False + self._online_check_interval = 30 # Интервал для кэширования проверок онлайн-статуса + + # Время последней успешной аутентификации и срок действия сессии + self._last_auth_time = 0 + self._auth_expiry = 3600 # По умолчанию 1 час + + def login(self) -> bool: + """Авторизация в API Synology NAS""" + # Проверяем, действительна ли текущая сессия + current_time = time.time() + if self.sid and (current_time - self._last_auth_time) < self._auth_expiry: + logger.debug("Using existing session, still valid") + return True + + logger.info("Attempting to authenticate with Synology NAS...") + logger.debug(f"Base URL: {self.base_url}") + + # Проверяем доступность NAS перед запросом + online_status = self.is_online() + if not online_status: + logger.error("Cannot login: Synology NAS is not reachable") + return False + + try: + url = f"{self.base_url}/auth.cgi" + params = { + "api": "SYNO.API.Auth", + "version": "3", + "method": "login", + "account": SYNOLOGY_USERNAME, + "passwd": SYNOLOGY_PASSWORD, + "session": "SynologyPowerControlBot", + "format": "cookie" + } + + logger.debug(f"Sending auth request to {url}") + start_time = time.time() + response = self.session.get( + url, + params=params, + timeout=self.default_timeout, + verify=False + ) + elapsed_time = time.time() - start_time + logger.debug(f"Auth request completed in {elapsed_time:.2f}s with status code {response.status_code}") + + # Проверка статуса HTTP + if response.status_code != 200: + logger.error(f"HTTP error: {response.status_code}") + logger.debug(f"Response content: {response.text[:200]}") + return False + + try: + data = response.json() + except json.JSONDecodeError: + logger.error("Failed to decode JSON response") + logger.debug(f"Response content: {response.text[:200]}") + return False + + if data.get("success"): + self.sid = data.get("data", {}).get("sid") + self._last_auth_time = current_time + logger.info("Successfully logged in to Synology NAS") + logger.debug(f"Session ID obtained: {self.sid[:5]}...") # Показываем только начало SID для безопасности + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to log in to Synology NAS: Error code {error_code}") + + # Дополнительная диагностика + if error_code == 400: + logger.error("Authentication error: Invalid credentials") + elif error_code == 401: + logger.error("Authentication error: Account disabled") + elif error_code == 402: + logger.error("Authentication error: Permission denied") + elif error_code == 403: + logger.error("Authentication error: 2-factor authentication required") + elif error_code == 404: + logger.error("Authentication error: Failed to authenticate with 2-factor authentication") + + return False + + except requests.exceptions.Timeout: + logger.error(f"Connection timeout after {SYNOLOGY_TIMEOUT} seconds") + return False + except requests.exceptions.ConnectionError as e: + logger.error(f"Connection error: {str(e)}") + logger.debug(f"Connection details: {self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}") + return False + except requests.RequestException as e: + logger.error(f"Request error: {str(e)}") + return False + except Exception as e: + logger.error(f"Unexpected error during login: {str(e)}", exc_info=True) + return False + + def logout(self) -> bool: + """Выход из API Synology NAS""" + if not self.sid: + return True + + try: + url = f"{self.base_url}/auth.cgi" + params = { + "api": "SYNO.API.Auth", + "version": "1", + "method": "logout", + "session": "SynologyPowerControlBot", + "_sid": self.sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + self.sid = None + logger.info("Successfully logged out from Synology NAS") + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to log out from Synology NAS: Error code {error_code}") + return False + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + return False + + def _make_api_request(self, api_name: str, method: str, version: int = 1, + params: Optional[Dict[str, Any]] = None, retry_auth: bool = True) -> Optional[Dict[str, Any]]: + """Обобщенный метод для выполнения API запросов с обработкой ошибок""" + # Проверка наличия авторизации + if not self.sid and not self.login(): + logger.error(f"Not authenticated for API request: {api_name}.{method}") + return None + + # Подготовка базовых параметров запроса + base_params = { + "api": api_name, + "version": str(version), + "method": method, + "sid": self.sid + } + + # Добавление дополнительных параметров, если они заданы + if params: + base_params.update(params) + + url = f"{self.base_url}/entry.cgi" + logger.debug(f"API request: {api_name}.{method} v{version}") + + try: + start_time = time.time() + response = self.session.get( + url, + params=base_params, + timeout=self.default_timeout, + verify=False + ) + elapsed_time = time.time() - start_time + logger.debug(f"API request completed in {elapsed_time:.2f}s with status {response.status_code}") + + # Проверка статуса HTTP + if response.status_code != 200: + logger.error(f"HTTP error: {response.status_code} for {api_name}.{method}") + return None + + try: + data = response.json() + except json.JSONDecodeError: + logger.error(f"Failed to decode JSON response for {api_name}.{method}") + logger.debug(f"Response content: {response.text[:200]}") + return None + + if data.get("success"): + logger.info(f"API request successful for {method}") + return data.get("data", {}) + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"API error for {api_name}.{method}: {error_code}") + + # Если ошибка связана с авторизацией и нам разрешено повторить попытку + if error_code in [106, 107, 119] and retry_auth: + logger.info("Session may have expired, re-authenticating...") + self.sid = None # Сбрасываем SID + if self.login(): + logger.info("Re-authenticated, retrying API request...") + # Рекурсивный вызов, но без повторной авторизации во избежание зацикливания + return self._make_api_request(api_name, method, version, params, False) + + return None + + except requests.exceptions.Timeout: + logger.error(f"Request timeout for {api_name}.{method}") + return None + except requests.exceptions.ConnectionError as e: + logger.error(f"Connection error for {api_name}.{method}: {str(e)}") + return None + except Exception as e: + logger.error(f"Unexpected error during {api_name}.{method}: {str(e)}") + return None + + def get_system_status(self) -> Dict[str, Any]: + """Получение статуса системы""" + # Проверяем доступность системы + if not self.is_online(): + logger.info("Device is offline, skipping API request") + return {"status": "offline"} + + # Проверяем, есть ли кэшированный результат + cache_key = "system_status" + current_time = time.time() + if cache_key in self._cache and current_time - self._cache_ttl.get(cache_key, 0) < 60: + logger.debug("Using cached system status") + return self._cache[cache_key] + + # Делаем API запрос + result = self._make_api_request("SYNO.DSM.Info", "getinfo") + + if result: + # Формируем расширенный ответ с дополнительной информацией + system_info = { + "status": "online", + "hostname": result.get("hostname", "unknown"), + "model": result.get("model", "unknown"), + "version": result.get("version", "unknown"), + "uptime": result.get("uptime", 0), + "time": current_time, + "is_online": True + } + + # Сохраняем в кэше + self._cache[cache_key] = system_info + self._cache_ttl[cache_key] = current_time + + return system_info + else: + # Если запрос не удался, но система онлайн + return { + "status": "error", + "error": "Failed to fetch system information", + "is_online": True, + "time": current_time + } + + def shutdown_system(self) -> bool: + """Выключение системы""" + # Проверяем, включено ли устройство перед попыткой его выключить + if not self.is_online(force_check=True): + logger.info("Synology NAS is already offline, no need to shut down") + return True + + logger.info("Attempting to shutdown Synology NAS...") + + # Пробуем DSM.System API (первый метод) + result = self._make_api_request("SYNO.DSM.System", "shutdown") + if result is not None: + logger.info("Successfully initiated system shutdown using SYNO.DSM.System") + + # Даем системе время начать процесс выключения + logger.info("Waiting for shutdown to initialize...") + sleep(5) + + # Проверяем статус + if not self.is_online(force_check=True): + logger.info("System is now offline. Shutdown confirmed successful.") + return True + else: + logger.info("System still appears to be online, but shutdown may be in progress.") + return True + + # Пробуем альтернативный Core.System API (второй метод) + logger.info("First shutdown method failed, trying alternative API...") + result = self._make_api_request("SYNO.Core.System", "shutdown", version=3) + if result is not None: + logger.info("Successfully initiated system shutdown using SYNO.Core.System") + + # Даем системе время начать процесс выключения + logger.info("Waiting for shutdown to initialize...") + sleep(5) + + # Проверяем статус + if not self.is_online(force_check=True): + logger.info("System is now offline. Shutdown confirmed successful.") + return True + else: + logger.info("System still appears to be online, but shutdown may be in progress.") + return True + + # Если оба метода не сработали, но система стала недоступна + if not self.is_online(force_check=True): + logger.info("System appears to be shutting down despite API errors") + return True + + logger.error("Failed to shutdown system after trying multiple APIs") + return False + + def reboot_system(self) -> bool: + """Перезагрузка системы""" + # Проверяем, включена ли система + if not self.is_online(force_check=True): + logger.error("Cannot reboot: System is offline") + return False + + logger.info("Attempting to reboot Synology NAS...") + + # Пробуем DSM.System API (первый метод) + result = self._make_api_request("SYNO.DSM.System", "reboot") + if result is not None: + logger.info("Successfully initiated system reboot using SYNO.DSM.System") + + # Даем системе время начать процесс перезагрузки + logger.info("Waiting for reboot to initialize...") + sleep(5) + + # Ждем, пока система станет недоступна (признак перезагрузки) + reboot_started = False + for i in range(12): # Проверяем в течение 60 секунд (12 * 5сек) + if not self.is_online(force_check=True): + logger.info(f"System went offline after {i*5} seconds, reboot in progress") + reboot_started = True + break + logger.debug(f"System still online, waiting... ({i+1}/12)") + sleep(5) + + if reboot_started: + # Ждем, пока система снова станет доступна + logger.info("Waiting for system to come back online...") + return self.wait_for_boot(max_attempts=30, delay=10) + else: + logger.warning("System did not go offline after reboot command") + return False + + return True + + # Пробуем альтернативный Core.System API (второй метод) + logger.info("First reboot method failed, trying alternative API...") + result = self._make_api_request("SYNO.Core.System", "reboot", version=3) + if result is not None: + logger.info("Successfully initiated system reboot using SYNO.Core.System") + + # Даем системе время начать процесс перезагрузки + logger.info("Waiting for reboot to initialize...") + sleep(5) + + # Ждем, пока система станет недоступна (признак перезагрузки) + reboot_started = False + for i in range(12): # Проверяем в течение 60 секунд (12 * 5сек) + if not self.is_online(force_check=True): + logger.info(f"System went offline after {i*5} seconds, reboot in progress") + reboot_started = True + break + logger.debug(f"System still online, waiting... ({i+1}/12)") + sleep(5) + + if reboot_started: + # Ждем, пока система снова станет доступна + logger.info("Waiting for system to come back online...") + return self.wait_for_boot(max_attempts=30, delay=10) + else: + logger.warning("System did not go offline after reboot command") + return False + + logger.error("Failed to reboot system after trying multiple APIs") + return False + + def _get_error_description(self, error_code: int) -> str: + """Получение описания ошибки по коду""" + error_descriptions = { + 100: "Unknown error", + 101: "Invalid parameter", + 102: "API does not exist", + 103: "Method does not exist", + 104: "Version does not support", + 105: "Permission denied", + 106: "Session timeout", + 107: "Session interrupted by duplicate login", + 400: "Invalid credentials", + 401: "Account disabled", + 402: "Permission denied", + 403: "2FA required", + 404: "Failed to authenticate with 2FA" + } + return error_descriptions.get(error_code, "Unknown error code") + + def is_online(self, force_check=False) -> bool: + """Проверка онлайн-статуса Synology NAS""" + # Используем кэшированное значение, если доступно и не устарело + current_time = time.time() + if not force_check and (current_time - self._last_online_check) < self._online_check_interval: + logger.debug(f"Using cached online status: {self._last_online_status}") + return self._last_online_status + + logger.info("Checking if NAS is online...") + try: + # Сначала проверяем TCP-соединение + socket_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + socket_obj.settimeout(SYNOLOGY_TIMEOUT) + result = socket_obj.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT)) + socket_obj.close() + + online_status = (result == 0) + logger.info(f"Detected Synology NAS online status: {online_status}") + + # Если TCP-соединение успешно, попробуем более детальную проверку через API + if online_status: + logger.info("Trying to fetch more detailed online status through API...") + # Пробуем получить информацию, но не вызываем is_online() рекурсивно + if self.sid or self.login(): + try: + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.DSM.Info", + "version": "1", + "method": "getinfo", + "sid": self.sid + } + + response = self.session.get( + url, + params=params, + timeout=self.default_timeout, + verify=False + ) + + if response.status_code == 200: + data = response.json() + if data.get("success"): + logger.info("API request successful for getinfo") + logger.info("Synology NAS is online with API access") + else: + logger.warning("API response indicates an error, but NAS is reachable") + logger.debug(f"API error: {data.get('error', {}).get('code', -1)}") + else: + logger.warning(f"API returned status code {response.status_code}") + except Exception as e: + logger.warning(f"API check failed, but TCP connection succeeded: {str(e)}") + + # Обновляем кэшированное значение + self._last_online_check = current_time + self._last_online_status = online_status + + return online_status + except socket.error as e: + logger.error(f"Socket error during online check: {str(e)}") + self._last_online_check = current_time + self._last_online_status = False + return False + except Exception as e: + logger.error(f"Unexpected error during online check: {str(e)}") + self._last_online_check = current_time + self._last_online_status = False + return False + + def wake_on_lan(self) -> bool: + """Отправка Wake-on-LAN пакета для включения Synology NAS""" + if not SYNOLOGY_MAC: + logger.error("MAC address not configured") + return False + + try: + # Преобразование MAC-адреса в байты + mac_address = SYNOLOGY_MAC.replace(':', '').replace('-', '') + mac_bytes = bytes.fromhex(mac_address) + + # Создание Magic Packet + magic_packet = b'\xff' * 6 + mac_bytes * 16 + + # Отправка пакета + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) + sock.sendto(magic_packet, (SYNOLOGY_HOST, WOL_PORT)) + sock.close() + + logger.info(f"Wake-on-LAN packet sent to {SYNOLOGY_MAC}") + return True + + except Exception as e: + logger.error(f"Error sending Wake-on-LAN packet: {str(e)}") + return False + + def wait_for_boot(self, max_attempts: int = 30, delay: int = 10) -> bool: + """Ожидание загрузки Synology NAS""" + logger.info("Waiting for Synology NAS to boot...") + + for attempt in range(max_attempts): + if self.is_online(): + logger.info(f"Synology NAS is online after {attempt + 1} attempts") + # Дадим дополнительное время для полной загрузки всех сервисов + sleep(delay) + return True + + sleep(delay) + logger.info(f"Waiting for boot... Attempt {attempt + 1}/{max_attempts}") + + logger.error(f"Synology NAS didn't come online after {max_attempts} attempts") + return False + + def power_on(self) -> bool: + """Включение Synology NAS""" + # Принудительная проверка статуса + if self.is_online(force_check=True): + logger.info("Synology NAS is already online") + return True + + logger.info("Powering on Synology NAS via Wake-on-LAN...") + + # Проверяем, настроен ли MAC-адрес + if not SYNOLOGY_MAC: + logger.error("Cannot power on: MAC address not configured in settings") + return False + + # Пробуем отправить несколько WoL пакетов для надежности + success = False + for attempt in range(3): + logger.debug(f"Sending WoL packet, attempt {attempt + 1}/3") + if self.wake_on_lan(): + success = True + break + sleep(1) + + if not success: + logger.error("Failed to send Wake-on-LAN packets") + return False + + # Ожидание загрузки + logger.info("WoL packets sent successfully, waiting for system to boot...") + boot_result = self.wait_for_boot(max_attempts=30, delay=10) + + if boot_result: + # Проверяем доступность API после загрузки + system_status = self.get_system_status() + if system_status.get("status") == "online": + logger.info("System booted successfully with API access") + return True + else: + logger.warning("System appears to be online but API may not be fully ready") + return True + else: + logger.error("System did not come online after WoL") + return False + + def power_off(self) -> bool: + """Выключение Synology NAS""" + if not self.is_online(force_check=True): + logger.info("Synology NAS is already offline") + return True + + logger.info("Powering off Synology NAS...") + result = self.shutdown_system() + + if result: + # Дополнительная проверка, что система действительно выключилась + logger.info("Verifying system is offline...") + for attempt in range(12): # Проверяем в течение 2 минут (12 * 10 сек) + if not self.is_online(force_check=True): + logger.info(f"System confirmed offline after {attempt * 10} seconds") + return True + logger.debug(f"System still shutting down, checking again... ({attempt + 1}/12)") + sleep(10) + + logger.warning("System still appears to be online after shutdown command") + return False + else: + logger.error("Failed to initiate shutdown") + return False + + # Заглушки для расширенных методов + def get_shared_folders(self) -> List[Dict[str, Any]]: + """Получение списка общих папок (заглушка)""" + logger.warning("Function get_shared_folders() is not implemented yet") + return [] + + def get_system_load(self) -> Dict[str, Any]: + """Получение информации о загрузке системы (заглушка)""" + logger.warning("Function get_system_load() is not implemented yet") + return {} + + def get_storage_status(self) -> Dict[str, Any]: + """Получение подробной информации о хранилище (заглушка)""" + logger.warning("Function get_storage_status() is not implemented yet") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for storage status request") + return {"error": "authentication_failed"} + + try: + # Здесь будет реализация после добавления соответствующего API + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.Storage.CGI.Storage", + "version": "1", + "method": "load_info", + "sid": self.sid + } + + # В текущей версии просто возвращаем заглушку + return { + "volumes": [], + "disks": [], + "total_size": 0, + "total_used": 0, + "error": "not_implemented" + } + except Exception as e: + logger.error(f"Error in get_storage_status: {str(e)}") + return {"error": str(e)} + + def get_security_status(self) -> Dict[str, Any]: + """Получение информации о состоянии безопасности (заглушка)""" + logger.warning("Function get_security_status() is not implemented yet") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for security status request") + return {"success": False, "error": "authentication_failed"} + + try: + # Здесь будет реализация после добавления соответствующего API + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.Core.SecurityScan.Status", + "version": "1", + "method": "get", + "sid": self.sid + } + + # В текущей версии просто возвращаем заглушку + return { + "success": False, + "status": "not_implemented", + "last_check": None, + "is_secure": False, + "error": "not_implemented" + } + except Exception as e: + logger.error(f"Error in get_security_status: {str(e)}") + return {"success": False, "error": str(e)} diff --git a/.history/src/api/synology_20250830075041.py b/.history/src/api/synology_20250830075041.py new file mode 100644 index 0000000..1b31283 --- /dev/null +++ b/.history/src/api/synology_20250830075041.py @@ -0,0 +1,762 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Модуль для взаимодействия с API Synology NAS +""" + +import requests +from requests.adapters import HTTPAdapter +import json +import logging +import time +import urllib3 +from urllib3.util import Retry +from typing import Dict, Any, Optional, List +import socket +import struct +from time import sleep + +from src.config.config import ( + SYNOLOGY_HOST, + SYNOLOGY_PORT, + SYNOLOGY_USERNAME, + SYNOLOGY_PASSWORD, + SYNOLOGY_SECURE, + SYNOLOGY_TIMEOUT, + SYNOLOGY_MAC, + WOL_PORT +) + +# Отключение предупреждений о небезопасных SSL-соединениях +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + +logger = logging.getLogger(__name__) + +class SynologyAPI: + """Класс для взаимодействия с API Synology NAS""" + + def __init__(self): + """Инициализация класса SynologyAPI""" + logger.info("Creating API with auto-retry and connection pool") + logger.debug(f"Connection details: Host={SYNOLOGY_HOST}, Port={SYNOLOGY_PORT}, Secure={SYNOLOGY_SECURE}") + + self.protocol = "https" if SYNOLOGY_SECURE else "http" + self.base_url = f"{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi" + self.sid = None + self.session = requests.Session() + + # Настройка SSL + if self.protocol == "https": + logger.debug("SSL enabled, disabling certificate verification for internal network") + self.session.verify = False # Отключаем проверку SSL для внутренней сети + + # Добавляем повторные попытки для HTTP-запросов + retry_strategy = Retry( + total=5, # Увеличиваем количество попыток + status_forcelist=[429, 500, 502, 503, 504, 404], + allowed_methods=["GET", "POST"], + backoff_factor=1.5, # Увеличиваем задержку между попытками + respect_retry_after_header=True + ) + adapter = HTTPAdapter( + max_retries=retry_strategy, + pool_connections=3, + pool_maxsize=10 + ) + self.session.mount("http://", adapter) + self.session.mount("https://", adapter) + + # Таймауты будут указаны в запросах + self.default_timeout = (SYNOLOGY_TIMEOUT, SYNOLOGY_TIMEOUT*2) + logger.debug(f"Setting default request timeout: {self.default_timeout}") + + # Кэш для хранения результатов запросов + self._cache = {} + self._cache_ttl = {} + self._last_online_check = 0 + self._last_online_status = False + self._online_check_interval = 30 # Интервал для кэширования проверок онлайн-статуса + + # Время последней успешной аутентификации и срок действия сессии + self._last_auth_time = 0 + self._auth_expiry = 3600 # По умолчанию 1 час + + def login(self) -> bool: + """Авторизация в API Synology NAS""" + # Проверяем, действительна ли текущая сессия + current_time = time.time() + if self.sid and (current_time - self._last_auth_time) < self._auth_expiry: + logger.debug("Using existing session, still valid") + return True + + logger.info("Attempting to authenticate with Synology NAS...") + logger.debug(f"Base URL: {self.base_url}") + + # Проверяем доступность NAS перед запросом + online_status = self.is_online() + if not online_status: + logger.error("Cannot login: Synology NAS is not reachable") + return False + + try: + url = f"{self.base_url}/auth.cgi" + params = { + "api": "SYNO.API.Auth", + "version": "3", + "method": "login", + "account": SYNOLOGY_USERNAME, + "passwd": SYNOLOGY_PASSWORD, + "session": "SynologyPowerControlBot", + "format": "cookie" + } + + logger.debug(f"Sending auth request to {url}") + start_time = time.time() + response = self.session.get( + url, + params=params, + timeout=self.default_timeout, + verify=False + ) + elapsed_time = time.time() - start_time + logger.debug(f"Auth request completed in {elapsed_time:.2f}s with status code {response.status_code}") + + # Проверка статуса HTTP + if response.status_code != 200: + logger.error(f"HTTP error: {response.status_code}") + logger.debug(f"Response content: {response.text[:200]}") + return False + + try: + data = response.json() + except json.JSONDecodeError: + logger.error("Failed to decode JSON response") + logger.debug(f"Response content: {response.text[:200]}") + return False + + if data.get("success"): + self.sid = data.get("data", {}).get("sid") + self._last_auth_time = current_time + logger.info("Successfully logged in to Synology NAS") + logger.debug(f"Session ID obtained: {self.sid[:5]}...") # Показываем только начало SID для безопасности + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to log in to Synology NAS: Error code {error_code}") + + # Дополнительная диагностика + if error_code == 400: + logger.error("Authentication error: Invalid credentials") + elif error_code == 401: + logger.error("Authentication error: Account disabled") + elif error_code == 402: + logger.error("Authentication error: Permission denied") + elif error_code == 403: + logger.error("Authentication error: 2-factor authentication required") + elif error_code == 404: + logger.error("Authentication error: Failed to authenticate with 2-factor authentication") + + return False + + except requests.exceptions.Timeout: + logger.error(f"Connection timeout after {SYNOLOGY_TIMEOUT} seconds") + return False + except requests.exceptions.ConnectionError as e: + logger.error(f"Connection error: {str(e)}") + logger.debug(f"Connection details: {self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}") + return False + except requests.RequestException as e: + logger.error(f"Request error: {str(e)}") + return False + except Exception as e: + logger.error(f"Unexpected error during login: {str(e)}", exc_info=True) + return False + + def logout(self) -> bool: + """Выход из API Synology NAS""" + if not self.sid: + return True + + try: + url = f"{self.base_url}/auth.cgi" + params = { + "api": "SYNO.API.Auth", + "version": "1", + "method": "logout", + "session": "SynologyPowerControlBot", + "_sid": self.sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + self.sid = None + logger.info("Successfully logged out from Synology NAS") + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to log out from Synology NAS: Error code {error_code}") + return False + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + return False + + def _make_api_request(self, api_name: str, method: str, version: int = 1, + params: Optional[Dict[str, Any]] = None, retry_auth: bool = True) -> Optional[Dict[str, Any]]: + """Обобщенный метод для выполнения API запросов с обработкой ошибок""" + # Проверка наличия авторизации + if not self.sid and not self.login(): + logger.error(f"Not authenticated for API request: {api_name}.{method}") + return None + + # Подготовка базовых параметров запроса + base_params = { + "api": api_name, + "version": str(version), + "method": method, + "sid": self.sid + } + + # Добавление дополнительных параметров, если они заданы + if params: + base_params.update(params) + + url = f"{self.base_url}/entry.cgi" + logger.debug(f"API request: {api_name}.{method} v{version}") + + try: + start_time = time.time() + response = self.session.get( + url, + params=base_params, + timeout=self.default_timeout, + verify=False + ) + elapsed_time = time.time() - start_time + logger.debug(f"API request completed in {elapsed_time:.2f}s with status {response.status_code}") + + # Проверка статуса HTTP + if response.status_code != 200: + logger.error(f"HTTP error: {response.status_code} for {api_name}.{method}") + return None + + try: + data = response.json() + except json.JSONDecodeError: + logger.error(f"Failed to decode JSON response for {api_name}.{method}") + logger.debug(f"Response content: {response.text[:200]}") + return None + + if data.get("success"): + logger.info(f"API request successful for {method}") + return data.get("data", {}) + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"API error for {api_name}.{method}: {error_code}") + + # Если ошибка связана с авторизацией и нам разрешено повторить попытку + if error_code in [106, 107, 119] and retry_auth: + logger.info("Session may have expired, re-authenticating...") + self.sid = None # Сбрасываем SID + if self.login(): + logger.info("Re-authenticated, retrying API request...") + # Рекурсивный вызов, но без повторной авторизации во избежание зацикливания + return self._make_api_request(api_name, method, version, params, False) + + return None + + except requests.exceptions.Timeout: + logger.error(f"Request timeout for {api_name}.{method}") + return None + except requests.exceptions.ConnectionError as e: + logger.error(f"Connection error for {api_name}.{method}: {str(e)}") + return None + except Exception as e: + logger.error(f"Unexpected error during {api_name}.{method}: {str(e)}") + return None + + def get_system_status(self) -> Dict[str, Any]: + """Получение статуса системы""" + # Проверяем доступность системы + if not self.is_online(): + logger.info("Device is offline, skipping API request") + return {"status": "offline"} + + # Проверяем, есть ли кэшированный результат + cache_key = "system_status" + current_time = time.time() + if cache_key in self._cache and current_time - self._cache_ttl.get(cache_key, 0) < 60: + logger.debug("Using cached system status") + return self._cache[cache_key] + + # Делаем API запрос + result = self._make_api_request("SYNO.DSM.Info", "getinfo") + + if result: + # Формируем расширенный ответ с дополнительной информацией + system_info = { + "status": "online", + "hostname": result.get("hostname", "unknown"), + "model": result.get("model", "unknown"), + "version": result.get("version", "unknown"), + "uptime": result.get("uptime", 0), + "time": current_time, + "is_online": True + } + + # Сохраняем в кэше + self._cache[cache_key] = system_info + self._cache_ttl[cache_key] = current_time + + return system_info + else: + # Если запрос не удался, но система онлайн + return { + "status": "error", + "error": "Failed to fetch system information", + "is_online": True, + "time": current_time + } + + def shutdown_system(self) -> bool: + """Выключение системы""" + # Проверяем, включено ли устройство перед попыткой его выключить + if not self.is_online(force_check=True): + logger.info("Synology NAS is already offline, no need to shut down") + return True + + logger.info("Attempting to shutdown Synology NAS...") + + # Пробуем DSM.System API (первый метод) + result = self._make_api_request("SYNO.DSM.System", "shutdown") + if result is not None: + logger.info("Successfully initiated system shutdown using SYNO.DSM.System") + + # Даем системе время начать процесс выключения + logger.info("Waiting for shutdown to initialize...") + sleep(5) + + # Проверяем статус + if not self.is_online(force_check=True): + logger.info("System is now offline. Shutdown confirmed successful.") + return True + else: + logger.info("System still appears to be online, but shutdown may be in progress.") + return True + + # Пробуем альтернативный Core.System API (второй метод) + logger.info("First shutdown method failed, trying alternative API...") + result = self._make_api_request("SYNO.Core.System", "shutdown", version=3) + if result is not None: + logger.info("Successfully initiated system shutdown using SYNO.Core.System") + + # Даем системе время начать процесс выключения + logger.info("Waiting for shutdown to initialize...") + sleep(5) + + # Проверяем статус + if not self.is_online(force_check=True): + logger.info("System is now offline. Shutdown confirmed successful.") + return True + else: + logger.info("System still appears to be online, but shutdown may be in progress.") + return True + + # Если оба метода не сработали, но система стала недоступна + if not self.is_online(force_check=True): + logger.info("System appears to be shutting down despite API errors") + return True + + logger.error("Failed to shutdown system after trying multiple APIs") + return False + + def reboot_system(self) -> bool: + """Перезагрузка системы""" + # Проверяем, включена ли система + if not self.is_online(force_check=True): + logger.error("Cannot reboot: System is offline") + return False + + logger.info("Attempting to reboot Synology NAS...") + + # Пробуем DSM.System API (первый метод) + result = self._make_api_request("SYNO.DSM.System", "reboot") + if result is not None: + logger.info("Successfully initiated system reboot using SYNO.DSM.System") + + # Даем системе время начать процесс перезагрузки + logger.info("Waiting for reboot to initialize...") + sleep(5) + + # Ждем, пока система станет недоступна (признак перезагрузки) + reboot_started = False + for i in range(12): # Проверяем в течение 60 секунд (12 * 5сек) + if not self.is_online(force_check=True): + logger.info(f"System went offline after {i*5} seconds, reboot in progress") + reboot_started = True + break + logger.debug(f"System still online, waiting... ({i+1}/12)") + sleep(5) + + if reboot_started: + # Ждем, пока система снова станет доступна + logger.info("Waiting for system to come back online...") + return self.wait_for_boot(max_attempts=30, delay=10) + else: + logger.warning("System did not go offline after reboot command") + return False + + return True + + # Пробуем альтернативный Core.System API (второй метод) + logger.info("First reboot method failed, trying alternative API...") + result = self._make_api_request("SYNO.Core.System", "reboot", version=3) + if result is not None: + logger.info("Successfully initiated system reboot using SYNO.Core.System") + + # Даем системе время начать процесс перезагрузки + logger.info("Waiting for reboot to initialize...") + sleep(5) + + # Ждем, пока система станет недоступна (признак перезагрузки) + reboot_started = False + for i in range(12): # Проверяем в течение 60 секунд (12 * 5сек) + if not self.is_online(force_check=True): + logger.info(f"System went offline after {i*5} seconds, reboot in progress") + reboot_started = True + break + logger.debug(f"System still online, waiting... ({i+1}/12)") + sleep(5) + + if reboot_started: + # Ждем, пока система снова станет доступна + logger.info("Waiting for system to come back online...") + return self.wait_for_boot(max_attempts=30, delay=10) + else: + logger.warning("System did not go offline after reboot command") + return False + + logger.error("Failed to reboot system after trying multiple APIs") + return False + + def _get_error_description(self, error_code: int) -> str: + """Получение описания ошибки по коду""" + error_descriptions = { + 100: "Unknown error", + 101: "Invalid parameter", + 102: "API does not exist", + 103: "Method does not exist", + 104: "Version does not support", + 105: "Permission denied", + 106: "Session timeout", + 107: "Session interrupted by duplicate login", + 400: "Invalid credentials", + 401: "Account disabled", + 402: "Permission denied", + 403: "2FA required", + 404: "Failed to authenticate with 2FA" + } + return error_descriptions.get(error_code, "Unknown error code") + + def is_online(self, force_check=False) -> bool: + """Проверка онлайн-статуса Synology NAS""" + # Используем кэшированное значение, если доступно и не устарело + current_time = time.time() + if not force_check and (current_time - self._last_online_check) < self._online_check_interval: + logger.debug(f"Using cached online status: {self._last_online_status}") + return self._last_online_status + + logger.info("Checking if NAS is online...") + try: + # Сначала проверяем TCP-соединение + socket_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + socket_obj.settimeout(SYNOLOGY_TIMEOUT) + result = socket_obj.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT)) + socket_obj.close() + + online_status = (result == 0) + logger.info(f"Detected Synology NAS online status: {online_status}") + + # Если TCP-соединение успешно, попробуем более детальную проверку через API + if online_status: + logger.info("Trying to fetch more detailed online status through API...") + # Пробуем получить информацию, но не вызываем is_online() рекурсивно + if self.sid or self.login(): + try: + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.DSM.Info", + "version": "1", + "method": "getinfo", + "sid": self.sid + } + + response = self.session.get( + url, + params=params, + timeout=self.default_timeout, + verify=False + ) + + if response.status_code == 200: + data = response.json() + if data.get("success"): + logger.info("API request successful for getinfo") + logger.info("Synology NAS is online with API access") + else: + logger.warning("API response indicates an error, but NAS is reachable") + logger.debug(f"API error: {data.get('error', {}).get('code', -1)}") + else: + logger.warning(f"API returned status code {response.status_code}") + except Exception as e: + logger.warning(f"API check failed, but TCP connection succeeded: {str(e)}") + + # Обновляем кэшированное значение + self._last_online_check = current_time + self._last_online_status = online_status + + return online_status + except socket.error as e: + logger.error(f"Socket error during online check: {str(e)}") + self._last_online_check = current_time + self._last_online_status = False + return False + except Exception as e: + logger.error(f"Unexpected error during online check: {str(e)}") + self._last_online_check = current_time + self._last_online_status = False + return False + + def wake_on_lan(self) -> bool: + """Отправка Wake-on-LAN пакета для включения Synology NAS""" + if not SYNOLOGY_MAC: + logger.error("MAC address not configured") + return False + + try: + # Преобразование MAC-адреса в байты + mac_address = SYNOLOGY_MAC.replace(':', '').replace('-', '') + if len(mac_address) != 12: + logger.error(f"Invalid MAC address format: {SYNOLOGY_MAC}") + return False + + try: + mac_bytes = bytes.fromhex(mac_address) + except ValueError as e: + logger.error(f"Failed to parse MAC address: {str(e)}") + logger.error(f"MAC address must contain only hex characters: {SYNOLOGY_MAC}") + return False + + # Создание Magic Packet + magic_packet = b'\xff' * 6 + mac_bytes * 16 + + # Отправка пакета на конкретный адрес + try: + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) + sock.sendto(magic_packet, (SYNOLOGY_HOST, WOL_PORT)) + sock.close() + logger.info(f"Wake-on-LAN packet sent to {SYNOLOGY_MAC} at {SYNOLOGY_HOST}:{WOL_PORT}") + except Exception as e: + logger.error(f"Error sending directed WoL packet: {str(e)}") + return False + + # Для надежности отправляем также широковещательный пакет + try: + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) + + # Получаем широковещательный адрес для локальной сети + # Предполагаем, что адрес завершается на .255 + broadcast_addr = SYNOLOGY_HOST.rsplit('.', 1)[0] + '.255' + sock.sendto(magic_packet, (broadcast_addr, WOL_PORT)) + sock.close() + logger.info(f"Wake-on-LAN broadcast packet sent to {broadcast_addr}:{WOL_PORT}") + except Exception as e: + logger.warning(f"Error sending broadcast WoL packet (non-critical): {str(e)}") + # Не считаем ошибкой, т.к. основной пакет уже отправлен + + return True + + except Exception as e: + logger.error(f"Unexpected error in wake_on_lan: {str(e)}") + return False + + def wait_for_boot(self, max_attempts: int = 30, delay: int = 10) -> bool: + """Ожидание загрузки Synology NAS""" + logger.info(f"Waiting for Synology NAS to boot (timeout: {max_attempts * delay}s)...") + + for attempt in range(max_attempts): + # Принудительно проверяем статус без использования кэша + if self.is_online(force_check=True): + logger.info(f"Synology NAS is online after {attempt + 1} attempts ({(attempt + 1) * delay}s)") + + # Проверяем, что не только сеть доступна, но и API загрузился + api_ready = False + logger.info("Waiting for API services to initialize...") + + for api_check in range(5): # Даем еще до 50 секунд для загрузки API + if self.sid or self.login(): + api_ready = True + logger.info(f"API services are ready after {api_check + 1} attempts") + break + logger.debug(f"API not ready yet, waiting... ({api_check + 1}/5)") + sleep(10) + + if not api_ready: + logger.warning("System is online but API services may not be fully initialized") + + # Дадим дополнительное время для полной загрузки всех сервисов + sleep(delay) + return True + + sleep(delay) + logger.info(f"Waiting for boot... Attempt {attempt + 1}/{max_attempts}") + + logger.error(f"Synology NAS didn't come online after {max_attempts * delay} seconds") + return False + + def power_on(self) -> bool: + """Включение Synology NAS""" + # Принудительная проверка статуса + if self.is_online(force_check=True): + logger.info("Synology NAS is already online") + return True + + logger.info("Powering on Synology NAS via Wake-on-LAN...") + + # Проверяем, настроен ли MAC-адрес + if not SYNOLOGY_MAC: + logger.error("Cannot power on: MAC address not configured in settings") + return False + + # Пробуем отправить несколько WoL пакетов для надежности + success = False + for attempt in range(3): + logger.debug(f"Sending WoL packet, attempt {attempt + 1}/3") + if self.wake_on_lan(): + success = True + break + sleep(1) + + if not success: + logger.error("Failed to send Wake-on-LAN packets") + return False + + # Ожидание загрузки + logger.info("WoL packets sent successfully, waiting for system to boot...") + boot_result = self.wait_for_boot(max_attempts=30, delay=10) + + if boot_result: + # Проверяем доступность API после загрузки + system_status = self.get_system_status() + if system_status.get("status") == "online": + logger.info("System booted successfully with API access") + return True + else: + logger.warning("System appears to be online but API may not be fully ready") + return True + else: + logger.error("System did not come online after WoL") + return False + + def power_off(self) -> bool: + """Выключение Synology NAS""" + if not self.is_online(force_check=True): + logger.info("Synology NAS is already offline") + return True + + logger.info("Powering off Synology NAS...") + result = self.shutdown_system() + + if result: + # Дополнительная проверка, что система действительно выключилась + logger.info("Verifying system is offline...") + for attempt in range(12): # Проверяем в течение 2 минут (12 * 10 сек) + if not self.is_online(force_check=True): + logger.info(f"System confirmed offline after {attempt * 10} seconds") + return True + logger.debug(f"System still shutting down, checking again... ({attempt + 1}/12)") + sleep(10) + + logger.warning("System still appears to be online after shutdown command") + return False + else: + logger.error("Failed to initiate shutdown") + return False + + # Заглушки для расширенных методов + def get_shared_folders(self) -> List[Dict[str, Any]]: + """Получение списка общих папок (заглушка)""" + logger.warning("Function get_shared_folders() is not implemented yet") + return [] + + def get_system_load(self) -> Dict[str, Any]: + """Получение информации о загрузке системы (заглушка)""" + logger.warning("Function get_system_load() is not implemented yet") + return {} + + def get_storage_status(self) -> Dict[str, Any]: + """Получение подробной информации о хранилище (заглушка)""" + logger.warning("Function get_storage_status() is not implemented yet") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for storage status request") + return {"error": "authentication_failed"} + + try: + # Здесь будет реализация после добавления соответствующего API + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.Storage.CGI.Storage", + "version": "1", + "method": "load_info", + "sid": self.sid + } + + # В текущей версии просто возвращаем заглушку + return { + "volumes": [], + "disks": [], + "total_size": 0, + "total_used": 0, + "error": "not_implemented" + } + except Exception as e: + logger.error(f"Error in get_storage_status: {str(e)}") + return {"error": str(e)} + + def get_security_status(self) -> Dict[str, Any]: + """Получение информации о состоянии безопасности (заглушка)""" + logger.warning("Function get_security_status() is not implemented yet") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for security status request") + return {"success": False, "error": "authentication_failed"} + + try: + # Здесь будет реализация после добавления соответствующего API + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.Core.SecurityScan.Status", + "version": "1", + "method": "get", + "sid": self.sid + } + + # В текущей версии просто возвращаем заглушку + return { + "success": False, + "status": "not_implemented", + "last_check": None, + "is_secure": False, + "error": "not_implemented" + } + except Exception as e: + logger.error(f"Error in get_security_status: {str(e)}") + return {"success": False, "error": str(e)} diff --git a/.history/src/api/synology_20250830075053.py b/.history/src/api/synology_20250830075053.py new file mode 100644 index 0000000..1149fa0 --- /dev/null +++ b/.history/src/api/synology_20250830075053.py @@ -0,0 +1,761 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Модуль для взаимодействия с API Synology NAS +""" + +import requests +from requests.adapters import HTTPAdapter +import json +import logging +import time +import urllib3 +from urllib3.util import Retry +from typing import Dict, Any, Optional, List +import socket +import struct +from time import sleep + +from src.config.config import ( + SYNOLOGY_HOST, + SYNOLOGY_PORT, + SYNOLOGY_USERNAME, + SYNOLOGY_PASSWORD, + SYNOLOGY_SECURE, + SYNOLOGY_TIMEOUT, + SYNOLOGY_MAC, + WOL_PORT +) + +# Отключение предупреждений о небезопасных SSL-соединениях +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + +logger = logging.getLogger(__name__) + +class SynologyAPI: + """Класс для взаимодействия с API Synology NAS""" + + def __init__(self): + """Инициализация класса SynologyAPI""" + logger.info("Creating API with auto-retry and connection pool") + logger.debug(f"Connection details: Host={SYNOLOGY_HOST}, Port={SYNOLOGY_PORT}, Secure={SYNOLOGY_SECURE}") + + self.protocol = "https" if SYNOLOGY_SECURE else "http" + self.base_url = f"{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi" + self.sid = None + self.session = requests.Session() + + # Настройка SSL + if self.protocol == "https": + logger.debug("SSL enabled, disabling certificate verification for internal network") + self.session.verify = False # Отключаем проверку SSL для внутренней сети + + # Добавляем повторные попытки для HTTP-запросов + retry_strategy = Retry( + total=5, # Увеличиваем количество попыток + status_forcelist=[429, 500, 502, 503, 504, 404], + allowed_methods=["GET", "POST"], + backoff_factor=1.5, # Увеличиваем задержку между попытками + respect_retry_after_header=True + ) + adapter = HTTPAdapter( + max_retries=retry_strategy, + pool_connections=3, + pool_maxsize=10 + ) + self.session.mount("http://", adapter) + self.session.mount("https://", adapter) + + # Таймауты будут указаны в запросах + self.default_timeout = (SYNOLOGY_TIMEOUT, SYNOLOGY_TIMEOUT*2) + logger.debug(f"Setting default request timeout: {self.default_timeout}") + + # Кэш для хранения результатов запросов + self._cache = {} + self._cache_ttl = {} + self._last_online_check = 0 + self._last_online_status = False + self._online_check_interval = 30 # Интервал для кэширования проверок онлайн-статуса + + # Время последней успешной аутентификации и срок действия сессии + self._last_auth_time = 0 + self._auth_expiry = 3600 # По умолчанию 1 час + + def login(self) -> bool: + """Авторизация в API Synology NAS""" + # Проверяем, действительна ли текущая сессия + current_time = time.time() + if self.sid and (current_time - self._last_auth_time) < self._auth_expiry: + logger.debug("Using existing session, still valid") + return True + + logger.info("Attempting to authenticate with Synology NAS...") + logger.debug(f"Base URL: {self.base_url}") + + # Проверяем доступность NAS перед запросом + online_status = self.is_online() + if not online_status: + logger.error("Cannot login: Synology NAS is not reachable") + return False + + try: + url = f"{self.base_url}/auth.cgi" + params = { + "api": "SYNO.API.Auth", + "version": "3", + "method": "login", + "account": SYNOLOGY_USERNAME, + "passwd": SYNOLOGY_PASSWORD, + "session": "SynologyPowerControlBot", + "format": "cookie" + } + + logger.debug(f"Sending auth request to {url}") + start_time = time.time() + response = self.session.get( + url, + params=params, + timeout=self.default_timeout, + verify=False + ) + elapsed_time = time.time() - start_time + logger.debug(f"Auth request completed in {elapsed_time:.2f}s with status code {response.status_code}") + + # Проверка статуса HTTP + if response.status_code != 200: + logger.error(f"HTTP error: {response.status_code}") + logger.debug(f"Response content: {response.text[:200]}") + return False + + try: + data = response.json() + except json.JSONDecodeError: + logger.error("Failed to decode JSON response") + logger.debug(f"Response content: {response.text[:200]}") + return False + + if data.get("success"): + self.sid = data.get("data", {}).get("sid") + self._last_auth_time = current_time + logger.info("Successfully logged in to Synology NAS") + logger.debug(f"Session ID obtained: {self.sid[:5]}...") # Показываем только начало SID для безопасности + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to log in to Synology NAS: Error code {error_code}") + + # Дополнительная диагностика + if error_code == 400: + logger.error("Authentication error: Invalid credentials") + elif error_code == 401: + logger.error("Authentication error: Account disabled") + elif error_code == 402: + logger.error("Authentication error: Permission denied") + elif error_code == 403: + logger.error("Authentication error: 2-factor authentication required") + elif error_code == 404: + logger.error("Authentication error: Failed to authenticate with 2-factor authentication") + + return False + + except requests.exceptions.Timeout: + logger.error(f"Connection timeout after {SYNOLOGY_TIMEOUT} seconds") + return False + except requests.exceptions.ConnectionError as e: + logger.error(f"Connection error: {str(e)}") + logger.debug(f"Connection details: {self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}") + return False + except requests.RequestException as e: + logger.error(f"Request error: {str(e)}") + return False + except Exception as e: + logger.error(f"Unexpected error during login: {str(e)}", exc_info=True) + return False + + def logout(self) -> bool: + """Выход из API Synology NAS""" + if not self.sid: + return True + + try: + url = f"{self.base_url}/auth.cgi" + params = { + "api": "SYNO.API.Auth", + "version": "1", + "method": "logout", + "session": "SynologyPowerControlBot", + "_sid": self.sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + self.sid = None + logger.info("Successfully logged out from Synology NAS") + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to log out from Synology NAS: Error code {error_code}") + return False + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + return False + + def _make_api_request(self, api_name: str, method: str, version: int = 1, + params: Optional[Dict[str, Any]] = None, retry_auth: bool = True) -> Optional[Dict[str, Any]]: + """Обобщенный метод для выполнения API запросов с обработкой ошибок""" + # Проверка наличия авторизации + if not self.sid and not self.login(): + logger.error(f"Not authenticated for API request: {api_name}.{method}") + return None + + # Подготовка базовых параметров запроса + base_params = { + "api": api_name, + "version": str(version), + "method": method, + "sid": self.sid + } + + # Добавление дополнительных параметров, если они заданы + if params: + base_params.update(params) + + url = f"{self.base_url}/entry.cgi" + logger.debug(f"API request: {api_name}.{method} v{version}") + + try: + start_time = time.time() + response = self.session.get( + url, + params=base_params, + timeout=self.default_timeout, + verify=False + ) + elapsed_time = time.time() - start_time + logger.debug(f"API request completed in {elapsed_time:.2f}s with status {response.status_code}") + + # Проверка статуса HTTP + if response.status_code != 200: + logger.error(f"HTTP error: {response.status_code} for {api_name}.{method}") + return None + + try: + data = response.json() + except json.JSONDecodeError: + logger.error(f"Failed to decode JSON response for {api_name}.{method}") + logger.debug(f"Response content: {response.text[:200]}") + return None + + if data.get("success"): + logger.info(f"API request successful for {method}") + return data.get("data", {}) + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"API error for {api_name}.{method}: {error_code}") + + # Если ошибка связана с авторизацией и нам разрешено повторить попытку + if error_code in [106, 107, 119] and retry_auth: + logger.info("Session may have expired, re-authenticating...") + self.sid = None # Сбрасываем SID + if self.login(): + logger.info("Re-authenticated, retrying API request...") + # Рекурсивный вызов, но без повторной авторизации во избежание зацикливания + return self._make_api_request(api_name, method, version, params, False) + + return None + + except requests.exceptions.Timeout: + logger.error(f"Request timeout for {api_name}.{method}") + return None + except requests.exceptions.ConnectionError as e: + logger.error(f"Connection error for {api_name}.{method}: {str(e)}") + return None + except Exception as e: + logger.error(f"Unexpected error during {api_name}.{method}: {str(e)}") + return None + + def get_system_status(self) -> Dict[str, Any]: + """Получение статуса системы""" + # Проверяем доступность системы + if not self.is_online(): + logger.info("Device is offline, skipping API request") + return {"status": "offline"} + + # Проверяем, есть ли кэшированный результат + cache_key = "system_status" + current_time = time.time() + if cache_key in self._cache and current_time - self._cache_ttl.get(cache_key, 0) < 60: + logger.debug("Using cached system status") + return self._cache[cache_key] + + # Делаем API запрос + result = self._make_api_request("SYNO.DSM.Info", "getinfo") + + if result: + # Формируем расширенный ответ с дополнительной информацией + system_info = { + "status": "online", + "hostname": result.get("hostname", "unknown"), + "model": result.get("model", "unknown"), + "version": result.get("version", "unknown"), + "uptime": result.get("uptime", 0), + "time": current_time, + "is_online": True + } + + # Сохраняем в кэше + self._cache[cache_key] = system_info + self._cache_ttl[cache_key] = current_time + + return system_info + else: + # Если запрос не удался, но система онлайн + return { + "status": "error", + "error": "Failed to fetch system information", + "is_online": True, + "time": current_time + } + + def shutdown_system(self) -> bool: + """Выключение системы""" + # Проверяем, включено ли устройство перед попыткой его выключить + if not self.is_online(force_check=True): + logger.info("Synology NAS is already offline, no need to shut down") + return True + + logger.info("Attempting to shutdown Synology NAS...") + + # Пробуем DSM.System API (первый метод) + result = self._make_api_request("SYNO.DSM.System", "shutdown") + if result is not None: + logger.info("Successfully initiated system shutdown using SYNO.DSM.System") + + # Даем системе время начать процесс выключения + logger.info("Waiting for shutdown to initialize...") + sleep(5) + + # Проверяем статус + if not self.is_online(force_check=True): + logger.info("System is now offline. Shutdown confirmed successful.") + return True + else: + logger.info("System still appears to be online, but shutdown may be in progress.") + return True + + # Пробуем альтернативный Core.System API (второй метод) + logger.info("First shutdown method failed, trying alternative API...") + result = self._make_api_request("SYNO.Core.System", "shutdown", version=3) + if result is not None: + logger.info("Successfully initiated system shutdown using SYNO.Core.System") + + # Даем системе время начать процесс выключения + logger.info("Waiting for shutdown to initialize...") + sleep(5) + + # Проверяем статус + if not self.is_online(force_check=True): + logger.info("System is now offline. Shutdown confirmed successful.") + return True + else: + logger.info("System still appears to be online, but shutdown may be in progress.") + return True + + # Если оба метода не сработали, но система стала недоступна + if not self.is_online(force_check=True): + logger.info("System appears to be shutting down despite API errors") + return True + + logger.error("Failed to shutdown system after trying multiple APIs") + return False + + def reboot_system(self) -> bool: + """Перезагрузка системы""" + # Проверяем, включена ли система + if not self.is_online(force_check=True): + logger.error("Cannot reboot: System is offline") + return False + + logger.info("Attempting to reboot Synology NAS...") + + # Пробуем DSM.System API (первый метод) + result = self._make_api_request("SYNO.DSM.System", "reboot") + if result is not None: + logger.info("Successfully initiated system reboot using SYNO.DSM.System") + + # Даем системе время начать процесс перезагрузки + logger.info("Waiting for reboot to initialize...") + sleep(5) + + # Ждем, пока система станет недоступна (признак перезагрузки) + reboot_started = False + for i in range(12): # Проверяем в течение 60 секунд (12 * 5сек) + if not self.is_online(force_check=True): + logger.info(f"System went offline after {i*5} seconds, reboot in progress") + reboot_started = True + break + logger.debug(f"System still online, waiting... ({i+1}/12)") + sleep(5) + + if reboot_started: + # Ждем, пока система снова станет доступна + logger.info("Waiting for system to come back online...") + return self.wait_for_boot(max_attempts=30, delay=10) + else: + logger.warning("System did not go offline after reboot command") + return False + + return True + + # Пробуем альтернативный Core.System API (второй метод) + logger.info("First reboot method failed, trying alternative API...") + result = self._make_api_request("SYNO.Core.System", "reboot", version=3) + if result is not None: + logger.info("Successfully initiated system reboot using SYNO.Core.System") + + # Даем системе время начать процесс перезагрузки + logger.info("Waiting for reboot to initialize...") + sleep(5) + + # Ждем, пока система станет недоступна (признак перезагрузки) + reboot_started = False + for i in range(12): # Проверяем в течение 60 секунд (12 * 5сек) + if not self.is_online(force_check=True): + logger.info(f"System went offline after {i*5} seconds, reboot in progress") + reboot_started = True + break + logger.debug(f"System still online, waiting... ({i+1}/12)") + sleep(5) + + if reboot_started: + # Ждем, пока система снова станет доступна + logger.info("Waiting for system to come back online...") + return self.wait_for_boot(max_attempts=30, delay=10) + else: + logger.warning("System did not go offline after reboot command") + return False + + logger.error("Failed to reboot system after trying multiple APIs") + return False + + def _get_error_description(self, error_code: int) -> str: + """Получение описания ошибки по коду""" + error_descriptions = { + 100: "Unknown error", + 101: "Invalid parameter", + 102: "API does not exist", + 103: "Method does not exist", + 104: "Version does not support", + 105: "Permission denied", + 106: "Session timeout", + 107: "Session interrupted by duplicate login", + 400: "Invalid credentials", + 401: "Account disabled", + 402: "Permission denied", + 403: "2FA required", + 404: "Failed to authenticate with 2FA" + } + return error_descriptions.get(error_code, "Unknown error code") + + def is_online(self, force_check=False) -> bool: + """Проверка онлайн-статуса Synology NAS""" + # Используем кэшированное значение, если доступно и не устарело + current_time = time.time() + if not force_check and (current_time - self._last_online_check) < self._online_check_interval: + logger.debug(f"Using cached online status: {self._last_online_status}") + return self._last_online_status + + logger.info("Checking if NAS is online...") + try: + # Сначала проверяем TCP-соединение + socket_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + socket_obj.settimeout(SYNOLOGY_TIMEOUT) + result = socket_obj.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT)) + socket_obj.close() + + online_status = (result == 0) + logger.info(f"Detected Synology NAS online status: {online_status}") + + # Если TCP-соединение успешно, попробуем более детальную проверку через API + if online_status: + logger.info("Trying to fetch more detailed online status through API...") + # Пробуем получить информацию, но не вызываем is_online() рекурсивно + if self.sid or self.login(): + try: + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.DSM.Info", + "version": "1", + "method": "getinfo", + "sid": self.sid + } + + response = self.session.get( + url, + params=params, + timeout=self.default_timeout, + verify=False + ) + + if response.status_code == 200: + data = response.json() + if data.get("success"): + logger.info("API request successful for getinfo") + logger.info("Synology NAS is online with API access") + else: + logger.warning("API response indicates an error, but NAS is reachable") + logger.debug(f"API error: {data.get('error', {}).get('code', -1)}") + else: + logger.warning(f"API returned status code {response.status_code}") + except Exception as e: + logger.warning(f"API check failed, but TCP connection succeeded: {str(e)}") + + # Обновляем кэшированное значение + self._last_online_check = current_time + self._last_online_status = online_status + + return online_status + except socket.error as e: + logger.error(f"Socket error during online check: {str(e)}") + self._last_online_check = current_time + self._last_online_status = False + return False + except Exception as e: + logger.error(f"Unexpected error during online check: {str(e)}") + self._last_online_check = current_time + self._last_online_status = False + return False + + def wake_on_lan(self) -> bool: + """Отправка Wake-on-LAN пакета для включения Synology NAS""" + if not SYNOLOGY_MAC: + logger.error("MAC address not configured") + return False + + try: + # Преобразование MAC-адреса в байты + mac_address = SYNOLOGY_MAC.replace(':', '').replace('-', '') + if len(mac_address) != 12: + logger.error(f"Invalid MAC address format: {SYNOLOGY_MAC}") + return False + + try: + mac_bytes = bytes.fromhex(mac_address) + except ValueError as e: + logger.error(f"Failed to parse MAC address: {str(e)}") + logger.error(f"MAC address must contain only hex characters: {SYNOLOGY_MAC}") + return False + + # Создание Magic Packet + magic_packet = b'\xff' * 6 + mac_bytes * 16 + + # Отправка пакета на конкретный адрес + try: + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) + sock.sendto(magic_packet, (SYNOLOGY_HOST, WOL_PORT)) + sock.close() + logger.info(f"Wake-on-LAN packet sent to {SYNOLOGY_MAC} at {SYNOLOGY_HOST}:{WOL_PORT}") + except Exception as e: + logger.error(f"Error sending directed WoL packet: {str(e)}") + return False + + # Для надежности отправляем также широковещательный пакет + try: + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) + + # Используем стандартный широковещательный адрес + broadcast_addr = "255.255.255.255" + sock.sendto(magic_packet, (broadcast_addr, WOL_PORT)) + sock.close() + logger.info(f"Wake-on-LAN broadcast packet sent to {broadcast_addr}:{WOL_PORT}") + except Exception as e: + logger.warning(f"Error sending broadcast WoL packet (non-critical): {str(e)}") + # Не считаем ошибкой, т.к. основной пакет уже отправлен + + return True + + except Exception as e: + logger.error(f"Unexpected error in wake_on_lan: {str(e)}") + return False + + def wait_for_boot(self, max_attempts: int = 30, delay: int = 10) -> bool: + """Ожидание загрузки Synology NAS""" + logger.info(f"Waiting for Synology NAS to boot (timeout: {max_attempts * delay}s)...") + + for attempt in range(max_attempts): + # Принудительно проверяем статус без использования кэша + if self.is_online(force_check=True): + logger.info(f"Synology NAS is online after {attempt + 1} attempts ({(attempt + 1) * delay}s)") + + # Проверяем, что не только сеть доступна, но и API загрузился + api_ready = False + logger.info("Waiting for API services to initialize...") + + for api_check in range(5): # Даем еще до 50 секунд для загрузки API + if self.sid or self.login(): + api_ready = True + logger.info(f"API services are ready after {api_check + 1} attempts") + break + logger.debug(f"API not ready yet, waiting... ({api_check + 1}/5)") + sleep(10) + + if not api_ready: + logger.warning("System is online but API services may not be fully initialized") + + # Дадим дополнительное время для полной загрузки всех сервисов + sleep(delay) + return True + + sleep(delay) + logger.info(f"Waiting for boot... Attempt {attempt + 1}/{max_attempts}") + + logger.error(f"Synology NAS didn't come online after {max_attempts * delay} seconds") + return False + + def power_on(self) -> bool: + """Включение Synology NAS""" + # Принудительная проверка статуса + if self.is_online(force_check=True): + logger.info("Synology NAS is already online") + return True + + logger.info("Powering on Synology NAS via Wake-on-LAN...") + + # Проверяем, настроен ли MAC-адрес + if not SYNOLOGY_MAC: + logger.error("Cannot power on: MAC address not configured in settings") + return False + + # Пробуем отправить несколько WoL пакетов для надежности + success = False + for attempt in range(3): + logger.debug(f"Sending WoL packet, attempt {attempt + 1}/3") + if self.wake_on_lan(): + success = True + break + sleep(1) + + if not success: + logger.error("Failed to send Wake-on-LAN packets") + return False + + # Ожидание загрузки + logger.info("WoL packets sent successfully, waiting for system to boot...") + boot_result = self.wait_for_boot(max_attempts=30, delay=10) + + if boot_result: + # Проверяем доступность API после загрузки + system_status = self.get_system_status() + if system_status.get("status") == "online": + logger.info("System booted successfully with API access") + return True + else: + logger.warning("System appears to be online but API may not be fully ready") + return True + else: + logger.error("System did not come online after WoL") + return False + + def power_off(self) -> bool: + """Выключение Synology NAS""" + if not self.is_online(force_check=True): + logger.info("Synology NAS is already offline") + return True + + logger.info("Powering off Synology NAS...") + result = self.shutdown_system() + + if result: + # Дополнительная проверка, что система действительно выключилась + logger.info("Verifying system is offline...") + for attempt in range(12): # Проверяем в течение 2 минут (12 * 10 сек) + if not self.is_online(force_check=True): + logger.info(f"System confirmed offline after {attempt * 10} seconds") + return True + logger.debug(f"System still shutting down, checking again... ({attempt + 1}/12)") + sleep(10) + + logger.warning("System still appears to be online after shutdown command") + return False + else: + logger.error("Failed to initiate shutdown") + return False + + # Заглушки для расширенных методов + def get_shared_folders(self) -> List[Dict[str, Any]]: + """Получение списка общих папок (заглушка)""" + logger.warning("Function get_shared_folders() is not implemented yet") + return [] + + def get_system_load(self) -> Dict[str, Any]: + """Получение информации о загрузке системы (заглушка)""" + logger.warning("Function get_system_load() is not implemented yet") + return {} + + def get_storage_status(self) -> Dict[str, Any]: + """Получение подробной информации о хранилище (заглушка)""" + logger.warning("Function get_storage_status() is not implemented yet") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for storage status request") + return {"error": "authentication_failed"} + + try: + # Здесь будет реализация после добавления соответствующего API + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.Storage.CGI.Storage", + "version": "1", + "method": "load_info", + "sid": self.sid + } + + # В текущей версии просто возвращаем заглушку + return { + "volumes": [], + "disks": [], + "total_size": 0, + "total_used": 0, + "error": "not_implemented" + } + except Exception as e: + logger.error(f"Error in get_storage_status: {str(e)}") + return {"error": str(e)} + + def get_security_status(self) -> Dict[str, Any]: + """Получение информации о состоянии безопасности (заглушка)""" + logger.warning("Function get_security_status() is not implemented yet") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for security status request") + return {"success": False, "error": "authentication_failed"} + + try: + # Здесь будет реализация после добавления соответствующего API + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.Core.SecurityScan.Status", + "version": "1", + "method": "get", + "sid": self.sid + } + + # В текущей версии просто возвращаем заглушку + return { + "success": False, + "status": "not_implemented", + "last_check": None, + "is_secure": False, + "error": "not_implemented" + } + except Exception as e: + logger.error(f"Error in get_security_status: {str(e)}") + return {"success": False, "error": str(e)} diff --git a/.history/src/api/synology_20250830075107.py b/.history/src/api/synology_20250830075107.py new file mode 100644 index 0000000..1149fa0 --- /dev/null +++ b/.history/src/api/synology_20250830075107.py @@ -0,0 +1,761 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Модуль для взаимодействия с API Synology NAS +""" + +import requests +from requests.adapters import HTTPAdapter +import json +import logging +import time +import urllib3 +from urllib3.util import Retry +from typing import Dict, Any, Optional, List +import socket +import struct +from time import sleep + +from src.config.config import ( + SYNOLOGY_HOST, + SYNOLOGY_PORT, + SYNOLOGY_USERNAME, + SYNOLOGY_PASSWORD, + SYNOLOGY_SECURE, + SYNOLOGY_TIMEOUT, + SYNOLOGY_MAC, + WOL_PORT +) + +# Отключение предупреждений о небезопасных SSL-соединениях +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + +logger = logging.getLogger(__name__) + +class SynologyAPI: + """Класс для взаимодействия с API Synology NAS""" + + def __init__(self): + """Инициализация класса SynologyAPI""" + logger.info("Creating API with auto-retry and connection pool") + logger.debug(f"Connection details: Host={SYNOLOGY_HOST}, Port={SYNOLOGY_PORT}, Secure={SYNOLOGY_SECURE}") + + self.protocol = "https" if SYNOLOGY_SECURE else "http" + self.base_url = f"{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi" + self.sid = None + self.session = requests.Session() + + # Настройка SSL + if self.protocol == "https": + logger.debug("SSL enabled, disabling certificate verification for internal network") + self.session.verify = False # Отключаем проверку SSL для внутренней сети + + # Добавляем повторные попытки для HTTP-запросов + retry_strategy = Retry( + total=5, # Увеличиваем количество попыток + status_forcelist=[429, 500, 502, 503, 504, 404], + allowed_methods=["GET", "POST"], + backoff_factor=1.5, # Увеличиваем задержку между попытками + respect_retry_after_header=True + ) + adapter = HTTPAdapter( + max_retries=retry_strategy, + pool_connections=3, + pool_maxsize=10 + ) + self.session.mount("http://", adapter) + self.session.mount("https://", adapter) + + # Таймауты будут указаны в запросах + self.default_timeout = (SYNOLOGY_TIMEOUT, SYNOLOGY_TIMEOUT*2) + logger.debug(f"Setting default request timeout: {self.default_timeout}") + + # Кэш для хранения результатов запросов + self._cache = {} + self._cache_ttl = {} + self._last_online_check = 0 + self._last_online_status = False + self._online_check_interval = 30 # Интервал для кэширования проверок онлайн-статуса + + # Время последней успешной аутентификации и срок действия сессии + self._last_auth_time = 0 + self._auth_expiry = 3600 # По умолчанию 1 час + + def login(self) -> bool: + """Авторизация в API Synology NAS""" + # Проверяем, действительна ли текущая сессия + current_time = time.time() + if self.sid and (current_time - self._last_auth_time) < self._auth_expiry: + logger.debug("Using existing session, still valid") + return True + + logger.info("Attempting to authenticate with Synology NAS...") + logger.debug(f"Base URL: {self.base_url}") + + # Проверяем доступность NAS перед запросом + online_status = self.is_online() + if not online_status: + logger.error("Cannot login: Synology NAS is not reachable") + return False + + try: + url = f"{self.base_url}/auth.cgi" + params = { + "api": "SYNO.API.Auth", + "version": "3", + "method": "login", + "account": SYNOLOGY_USERNAME, + "passwd": SYNOLOGY_PASSWORD, + "session": "SynologyPowerControlBot", + "format": "cookie" + } + + logger.debug(f"Sending auth request to {url}") + start_time = time.time() + response = self.session.get( + url, + params=params, + timeout=self.default_timeout, + verify=False + ) + elapsed_time = time.time() - start_time + logger.debug(f"Auth request completed in {elapsed_time:.2f}s with status code {response.status_code}") + + # Проверка статуса HTTP + if response.status_code != 200: + logger.error(f"HTTP error: {response.status_code}") + logger.debug(f"Response content: {response.text[:200]}") + return False + + try: + data = response.json() + except json.JSONDecodeError: + logger.error("Failed to decode JSON response") + logger.debug(f"Response content: {response.text[:200]}") + return False + + if data.get("success"): + self.sid = data.get("data", {}).get("sid") + self._last_auth_time = current_time + logger.info("Successfully logged in to Synology NAS") + logger.debug(f"Session ID obtained: {self.sid[:5]}...") # Показываем только начало SID для безопасности + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to log in to Synology NAS: Error code {error_code}") + + # Дополнительная диагностика + if error_code == 400: + logger.error("Authentication error: Invalid credentials") + elif error_code == 401: + logger.error("Authentication error: Account disabled") + elif error_code == 402: + logger.error("Authentication error: Permission denied") + elif error_code == 403: + logger.error("Authentication error: 2-factor authentication required") + elif error_code == 404: + logger.error("Authentication error: Failed to authenticate with 2-factor authentication") + + return False + + except requests.exceptions.Timeout: + logger.error(f"Connection timeout after {SYNOLOGY_TIMEOUT} seconds") + return False + except requests.exceptions.ConnectionError as e: + logger.error(f"Connection error: {str(e)}") + logger.debug(f"Connection details: {self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}") + return False + except requests.RequestException as e: + logger.error(f"Request error: {str(e)}") + return False + except Exception as e: + logger.error(f"Unexpected error during login: {str(e)}", exc_info=True) + return False + + def logout(self) -> bool: + """Выход из API Synology NAS""" + if not self.sid: + return True + + try: + url = f"{self.base_url}/auth.cgi" + params = { + "api": "SYNO.API.Auth", + "version": "1", + "method": "logout", + "session": "SynologyPowerControlBot", + "_sid": self.sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + self.sid = None + logger.info("Successfully logged out from Synology NAS") + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to log out from Synology NAS: Error code {error_code}") + return False + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + return False + + def _make_api_request(self, api_name: str, method: str, version: int = 1, + params: Optional[Dict[str, Any]] = None, retry_auth: bool = True) -> Optional[Dict[str, Any]]: + """Обобщенный метод для выполнения API запросов с обработкой ошибок""" + # Проверка наличия авторизации + if not self.sid and not self.login(): + logger.error(f"Not authenticated for API request: {api_name}.{method}") + return None + + # Подготовка базовых параметров запроса + base_params = { + "api": api_name, + "version": str(version), + "method": method, + "sid": self.sid + } + + # Добавление дополнительных параметров, если они заданы + if params: + base_params.update(params) + + url = f"{self.base_url}/entry.cgi" + logger.debug(f"API request: {api_name}.{method} v{version}") + + try: + start_time = time.time() + response = self.session.get( + url, + params=base_params, + timeout=self.default_timeout, + verify=False + ) + elapsed_time = time.time() - start_time + logger.debug(f"API request completed in {elapsed_time:.2f}s with status {response.status_code}") + + # Проверка статуса HTTP + if response.status_code != 200: + logger.error(f"HTTP error: {response.status_code} for {api_name}.{method}") + return None + + try: + data = response.json() + except json.JSONDecodeError: + logger.error(f"Failed to decode JSON response for {api_name}.{method}") + logger.debug(f"Response content: {response.text[:200]}") + return None + + if data.get("success"): + logger.info(f"API request successful for {method}") + return data.get("data", {}) + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"API error for {api_name}.{method}: {error_code}") + + # Если ошибка связана с авторизацией и нам разрешено повторить попытку + if error_code in [106, 107, 119] and retry_auth: + logger.info("Session may have expired, re-authenticating...") + self.sid = None # Сбрасываем SID + if self.login(): + logger.info("Re-authenticated, retrying API request...") + # Рекурсивный вызов, но без повторной авторизации во избежание зацикливания + return self._make_api_request(api_name, method, version, params, False) + + return None + + except requests.exceptions.Timeout: + logger.error(f"Request timeout for {api_name}.{method}") + return None + except requests.exceptions.ConnectionError as e: + logger.error(f"Connection error for {api_name}.{method}: {str(e)}") + return None + except Exception as e: + logger.error(f"Unexpected error during {api_name}.{method}: {str(e)}") + return None + + def get_system_status(self) -> Dict[str, Any]: + """Получение статуса системы""" + # Проверяем доступность системы + if not self.is_online(): + logger.info("Device is offline, skipping API request") + return {"status": "offline"} + + # Проверяем, есть ли кэшированный результат + cache_key = "system_status" + current_time = time.time() + if cache_key in self._cache and current_time - self._cache_ttl.get(cache_key, 0) < 60: + logger.debug("Using cached system status") + return self._cache[cache_key] + + # Делаем API запрос + result = self._make_api_request("SYNO.DSM.Info", "getinfo") + + if result: + # Формируем расширенный ответ с дополнительной информацией + system_info = { + "status": "online", + "hostname": result.get("hostname", "unknown"), + "model": result.get("model", "unknown"), + "version": result.get("version", "unknown"), + "uptime": result.get("uptime", 0), + "time": current_time, + "is_online": True + } + + # Сохраняем в кэше + self._cache[cache_key] = system_info + self._cache_ttl[cache_key] = current_time + + return system_info + else: + # Если запрос не удался, но система онлайн + return { + "status": "error", + "error": "Failed to fetch system information", + "is_online": True, + "time": current_time + } + + def shutdown_system(self) -> bool: + """Выключение системы""" + # Проверяем, включено ли устройство перед попыткой его выключить + if not self.is_online(force_check=True): + logger.info("Synology NAS is already offline, no need to shut down") + return True + + logger.info("Attempting to shutdown Synology NAS...") + + # Пробуем DSM.System API (первый метод) + result = self._make_api_request("SYNO.DSM.System", "shutdown") + if result is not None: + logger.info("Successfully initiated system shutdown using SYNO.DSM.System") + + # Даем системе время начать процесс выключения + logger.info("Waiting for shutdown to initialize...") + sleep(5) + + # Проверяем статус + if not self.is_online(force_check=True): + logger.info("System is now offline. Shutdown confirmed successful.") + return True + else: + logger.info("System still appears to be online, but shutdown may be in progress.") + return True + + # Пробуем альтернативный Core.System API (второй метод) + logger.info("First shutdown method failed, trying alternative API...") + result = self._make_api_request("SYNO.Core.System", "shutdown", version=3) + if result is not None: + logger.info("Successfully initiated system shutdown using SYNO.Core.System") + + # Даем системе время начать процесс выключения + logger.info("Waiting for shutdown to initialize...") + sleep(5) + + # Проверяем статус + if not self.is_online(force_check=True): + logger.info("System is now offline. Shutdown confirmed successful.") + return True + else: + logger.info("System still appears to be online, but shutdown may be in progress.") + return True + + # Если оба метода не сработали, но система стала недоступна + if not self.is_online(force_check=True): + logger.info("System appears to be shutting down despite API errors") + return True + + logger.error("Failed to shutdown system after trying multiple APIs") + return False + + def reboot_system(self) -> bool: + """Перезагрузка системы""" + # Проверяем, включена ли система + if not self.is_online(force_check=True): + logger.error("Cannot reboot: System is offline") + return False + + logger.info("Attempting to reboot Synology NAS...") + + # Пробуем DSM.System API (первый метод) + result = self._make_api_request("SYNO.DSM.System", "reboot") + if result is not None: + logger.info("Successfully initiated system reboot using SYNO.DSM.System") + + # Даем системе время начать процесс перезагрузки + logger.info("Waiting for reboot to initialize...") + sleep(5) + + # Ждем, пока система станет недоступна (признак перезагрузки) + reboot_started = False + for i in range(12): # Проверяем в течение 60 секунд (12 * 5сек) + if not self.is_online(force_check=True): + logger.info(f"System went offline after {i*5} seconds, reboot in progress") + reboot_started = True + break + logger.debug(f"System still online, waiting... ({i+1}/12)") + sleep(5) + + if reboot_started: + # Ждем, пока система снова станет доступна + logger.info("Waiting for system to come back online...") + return self.wait_for_boot(max_attempts=30, delay=10) + else: + logger.warning("System did not go offline after reboot command") + return False + + return True + + # Пробуем альтернативный Core.System API (второй метод) + logger.info("First reboot method failed, trying alternative API...") + result = self._make_api_request("SYNO.Core.System", "reboot", version=3) + if result is not None: + logger.info("Successfully initiated system reboot using SYNO.Core.System") + + # Даем системе время начать процесс перезагрузки + logger.info("Waiting for reboot to initialize...") + sleep(5) + + # Ждем, пока система станет недоступна (признак перезагрузки) + reboot_started = False + for i in range(12): # Проверяем в течение 60 секунд (12 * 5сек) + if not self.is_online(force_check=True): + logger.info(f"System went offline after {i*5} seconds, reboot in progress") + reboot_started = True + break + logger.debug(f"System still online, waiting... ({i+1}/12)") + sleep(5) + + if reboot_started: + # Ждем, пока система снова станет доступна + logger.info("Waiting for system to come back online...") + return self.wait_for_boot(max_attempts=30, delay=10) + else: + logger.warning("System did not go offline after reboot command") + return False + + logger.error("Failed to reboot system after trying multiple APIs") + return False + + def _get_error_description(self, error_code: int) -> str: + """Получение описания ошибки по коду""" + error_descriptions = { + 100: "Unknown error", + 101: "Invalid parameter", + 102: "API does not exist", + 103: "Method does not exist", + 104: "Version does not support", + 105: "Permission denied", + 106: "Session timeout", + 107: "Session interrupted by duplicate login", + 400: "Invalid credentials", + 401: "Account disabled", + 402: "Permission denied", + 403: "2FA required", + 404: "Failed to authenticate with 2FA" + } + return error_descriptions.get(error_code, "Unknown error code") + + def is_online(self, force_check=False) -> bool: + """Проверка онлайн-статуса Synology NAS""" + # Используем кэшированное значение, если доступно и не устарело + current_time = time.time() + if not force_check and (current_time - self._last_online_check) < self._online_check_interval: + logger.debug(f"Using cached online status: {self._last_online_status}") + return self._last_online_status + + logger.info("Checking if NAS is online...") + try: + # Сначала проверяем TCP-соединение + socket_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + socket_obj.settimeout(SYNOLOGY_TIMEOUT) + result = socket_obj.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT)) + socket_obj.close() + + online_status = (result == 0) + logger.info(f"Detected Synology NAS online status: {online_status}") + + # Если TCP-соединение успешно, попробуем более детальную проверку через API + if online_status: + logger.info("Trying to fetch more detailed online status through API...") + # Пробуем получить информацию, но не вызываем is_online() рекурсивно + if self.sid or self.login(): + try: + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.DSM.Info", + "version": "1", + "method": "getinfo", + "sid": self.sid + } + + response = self.session.get( + url, + params=params, + timeout=self.default_timeout, + verify=False + ) + + if response.status_code == 200: + data = response.json() + if data.get("success"): + logger.info("API request successful for getinfo") + logger.info("Synology NAS is online with API access") + else: + logger.warning("API response indicates an error, but NAS is reachable") + logger.debug(f"API error: {data.get('error', {}).get('code', -1)}") + else: + logger.warning(f"API returned status code {response.status_code}") + except Exception as e: + logger.warning(f"API check failed, but TCP connection succeeded: {str(e)}") + + # Обновляем кэшированное значение + self._last_online_check = current_time + self._last_online_status = online_status + + return online_status + except socket.error as e: + logger.error(f"Socket error during online check: {str(e)}") + self._last_online_check = current_time + self._last_online_status = False + return False + except Exception as e: + logger.error(f"Unexpected error during online check: {str(e)}") + self._last_online_check = current_time + self._last_online_status = False + return False + + def wake_on_lan(self) -> bool: + """Отправка Wake-on-LAN пакета для включения Synology NAS""" + if not SYNOLOGY_MAC: + logger.error("MAC address not configured") + return False + + try: + # Преобразование MAC-адреса в байты + mac_address = SYNOLOGY_MAC.replace(':', '').replace('-', '') + if len(mac_address) != 12: + logger.error(f"Invalid MAC address format: {SYNOLOGY_MAC}") + return False + + try: + mac_bytes = bytes.fromhex(mac_address) + except ValueError as e: + logger.error(f"Failed to parse MAC address: {str(e)}") + logger.error(f"MAC address must contain only hex characters: {SYNOLOGY_MAC}") + return False + + # Создание Magic Packet + magic_packet = b'\xff' * 6 + mac_bytes * 16 + + # Отправка пакета на конкретный адрес + try: + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) + sock.sendto(magic_packet, (SYNOLOGY_HOST, WOL_PORT)) + sock.close() + logger.info(f"Wake-on-LAN packet sent to {SYNOLOGY_MAC} at {SYNOLOGY_HOST}:{WOL_PORT}") + except Exception as e: + logger.error(f"Error sending directed WoL packet: {str(e)}") + return False + + # Для надежности отправляем также широковещательный пакет + try: + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) + + # Используем стандартный широковещательный адрес + broadcast_addr = "255.255.255.255" + sock.sendto(magic_packet, (broadcast_addr, WOL_PORT)) + sock.close() + logger.info(f"Wake-on-LAN broadcast packet sent to {broadcast_addr}:{WOL_PORT}") + except Exception as e: + logger.warning(f"Error sending broadcast WoL packet (non-critical): {str(e)}") + # Не считаем ошибкой, т.к. основной пакет уже отправлен + + return True + + except Exception as e: + logger.error(f"Unexpected error in wake_on_lan: {str(e)}") + return False + + def wait_for_boot(self, max_attempts: int = 30, delay: int = 10) -> bool: + """Ожидание загрузки Synology NAS""" + logger.info(f"Waiting for Synology NAS to boot (timeout: {max_attempts * delay}s)...") + + for attempt in range(max_attempts): + # Принудительно проверяем статус без использования кэша + if self.is_online(force_check=True): + logger.info(f"Synology NAS is online after {attempt + 1} attempts ({(attempt + 1) * delay}s)") + + # Проверяем, что не только сеть доступна, но и API загрузился + api_ready = False + logger.info("Waiting for API services to initialize...") + + for api_check in range(5): # Даем еще до 50 секунд для загрузки API + if self.sid or self.login(): + api_ready = True + logger.info(f"API services are ready after {api_check + 1} attempts") + break + logger.debug(f"API not ready yet, waiting... ({api_check + 1}/5)") + sleep(10) + + if not api_ready: + logger.warning("System is online but API services may not be fully initialized") + + # Дадим дополнительное время для полной загрузки всех сервисов + sleep(delay) + return True + + sleep(delay) + logger.info(f"Waiting for boot... Attempt {attempt + 1}/{max_attempts}") + + logger.error(f"Synology NAS didn't come online after {max_attempts * delay} seconds") + return False + + def power_on(self) -> bool: + """Включение Synology NAS""" + # Принудительная проверка статуса + if self.is_online(force_check=True): + logger.info("Synology NAS is already online") + return True + + logger.info("Powering on Synology NAS via Wake-on-LAN...") + + # Проверяем, настроен ли MAC-адрес + if not SYNOLOGY_MAC: + logger.error("Cannot power on: MAC address not configured in settings") + return False + + # Пробуем отправить несколько WoL пакетов для надежности + success = False + for attempt in range(3): + logger.debug(f"Sending WoL packet, attempt {attempt + 1}/3") + if self.wake_on_lan(): + success = True + break + sleep(1) + + if not success: + logger.error("Failed to send Wake-on-LAN packets") + return False + + # Ожидание загрузки + logger.info("WoL packets sent successfully, waiting for system to boot...") + boot_result = self.wait_for_boot(max_attempts=30, delay=10) + + if boot_result: + # Проверяем доступность API после загрузки + system_status = self.get_system_status() + if system_status.get("status") == "online": + logger.info("System booted successfully with API access") + return True + else: + logger.warning("System appears to be online but API may not be fully ready") + return True + else: + logger.error("System did not come online after WoL") + return False + + def power_off(self) -> bool: + """Выключение Synology NAS""" + if not self.is_online(force_check=True): + logger.info("Synology NAS is already offline") + return True + + logger.info("Powering off Synology NAS...") + result = self.shutdown_system() + + if result: + # Дополнительная проверка, что система действительно выключилась + logger.info("Verifying system is offline...") + for attempt in range(12): # Проверяем в течение 2 минут (12 * 10 сек) + if not self.is_online(force_check=True): + logger.info(f"System confirmed offline after {attempt * 10} seconds") + return True + logger.debug(f"System still shutting down, checking again... ({attempt + 1}/12)") + sleep(10) + + logger.warning("System still appears to be online after shutdown command") + return False + else: + logger.error("Failed to initiate shutdown") + return False + + # Заглушки для расширенных методов + def get_shared_folders(self) -> List[Dict[str, Any]]: + """Получение списка общих папок (заглушка)""" + logger.warning("Function get_shared_folders() is not implemented yet") + return [] + + def get_system_load(self) -> Dict[str, Any]: + """Получение информации о загрузке системы (заглушка)""" + logger.warning("Function get_system_load() is not implemented yet") + return {} + + def get_storage_status(self) -> Dict[str, Any]: + """Получение подробной информации о хранилище (заглушка)""" + logger.warning("Function get_storage_status() is not implemented yet") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for storage status request") + return {"error": "authentication_failed"} + + try: + # Здесь будет реализация после добавления соответствующего API + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.Storage.CGI.Storage", + "version": "1", + "method": "load_info", + "sid": self.sid + } + + # В текущей версии просто возвращаем заглушку + return { + "volumes": [], + "disks": [], + "total_size": 0, + "total_used": 0, + "error": "not_implemented" + } + except Exception as e: + logger.error(f"Error in get_storage_status: {str(e)}") + return {"error": str(e)} + + def get_security_status(self) -> Dict[str, Any]: + """Получение информации о состоянии безопасности (заглушка)""" + logger.warning("Function get_security_status() is not implemented yet") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for security status request") + return {"success": False, "error": "authentication_failed"} + + try: + # Здесь будет реализация после добавления соответствующего API + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.Core.SecurityScan.Status", + "version": "1", + "method": "get", + "sid": self.sid + } + + # В текущей версии просто возвращаем заглушку + return { + "success": False, + "status": "not_implemented", + "last_check": None, + "is_secure": False, + "error": "not_implemented" + } + except Exception as e: + logger.error(f"Error in get_security_status: {str(e)}") + return {"success": False, "error": str(e)} diff --git a/.history/src/api/synology_20250830075248.py b/.history/src/api/synology_20250830075248.py new file mode 100644 index 0000000..95ecda7 --- /dev/null +++ b/.history/src/api/synology_20250830075248.py @@ -0,0 +1,761 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Модуль для взаимодействия с API Synology NAS +""" + +import requests +from requests.adapters import HTTPAdapter +import json +import logging +import time +import urllib3 +from urllib3.util import Retry +from typing import Dict, Any, Optional, List +import socket +import struct +from time import sleep + +from src.config.config import ( + SYNOLOGY_HOST, + SYNOLOGY_PORT, + SYNOLOGY_USERNAME, + SYNOLOGY_PASSWORD, + SYNOLOGY_SECURE, + SYNOLOGY_TIMEOUT, + SYNOLOGY_MAC, + WOL_PORT +) + +# Отключение предупреждений о небезопасных SSL-соединениях +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + +logger = logging.getLogger(__name__) + +class SynologyAPI: + """Класс для взаимодействия с API Synology NAS""" + + def __init__(self): + """Инициализация класса SynologyAPI""" + logger.info("Creating API with auto-retry and connection pool") + logger.debug(f"Connection details: Host={SYNOLOGY_HOST}, Port={SYNOLOGY_PORT}, Secure={SYNOLOGY_SECURE}") + + self.protocol = "https" if SYNOLOGY_SECURE else "http" + self.base_url = f"{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi" + self.sid = None + self.session = requests.Session() + + # Настройка SSL + if self.protocol == "https": + logger.debug("SSL enabled, disabling certificate verification for internal network") + self.session.verify = False # Отключаем проверку SSL для внутренней сети + + # Добавляем повторные попытки для HTTP-запросов + retry_strategy = Retry( + total=5, # Увеличиваем количество попыток + status_forcelist=[429, 500, 502, 503, 504, 404], + allowed_methods=["GET", "POST"], + backoff_factor=1.5, # Увеличиваем задержку между попытками + respect_retry_after_header=True + ) + adapter = HTTPAdapter( + max_retries=retry_strategy, + pool_connections=3, + pool_maxsize=10 + ) + self.session.mount("http://", adapter) + self.session.mount("https://", adapter) + + # Таймауты будут указаны в запросах + self.default_timeout = (SYNOLOGY_TIMEOUT, SYNOLOGY_TIMEOUT*2) + logger.debug(f"Setting default request timeout: {self.default_timeout}") + + # Кэш для хранения результатов запросов + self._cache = {} + self._cache_ttl = {} + self._last_online_check = 0 + self._last_online_status = False + self._online_check_interval = 30 # Интервал для кэширования проверок онлайн-статуса + + # Время последней успешной аутентификации и срок действия сессии + self._last_auth_time = 0 + self._auth_expiry = 3600 # По умолчанию 1 час + + def login(self) -> bool: + """Авторизация в API Synology NAS""" + # Проверяем, действительна ли текущая сессия + current_time = time.time() + if self.sid and (current_time - self._last_auth_time) < self._auth_expiry: + logger.debug("Using existing session, still valid") + return True + + logger.info("Attempting to authenticate with Synology NAS...") + logger.debug(f"Base URL: {self.base_url}") + + # Проверяем доступность NAS перед запросом + online_status = self.is_online() + if not online_status: + logger.error("Cannot login: Synology NAS is not reachable") + return False + + try: + url = f"{self.base_url}/auth.cgi" + params = { + "api": "SYNO.API.Auth", + "version": "3", + "method": "login", + "account": SYNOLOGY_USERNAME, + "passwd": SYNOLOGY_PASSWORD, + "session": "SynologyPowerControlBot", + "format": "cookie" + } + + logger.debug(f"Sending auth request to {url}") + start_time = time.time() + response = self.session.get( + url, + params=params, + timeout=self.default_timeout, + verify=False + ) + elapsed_time = time.time() - start_time + logger.debug(f"Auth request completed in {elapsed_time:.2f}s with status code {response.status_code}") + + # Проверка статуса HTTP + if response.status_code != 200: + logger.error(f"HTTP error: {response.status_code}") + logger.debug(f"Response content: {response.text[:200]}") + return False + + try: + data = response.json() + except json.JSONDecodeError: + logger.error("Failed to decode JSON response") + logger.debug(f"Response content: {response.text[:200]}") + return False + + if data.get("success"): + self.sid = data.get("data", {}).get("sid") + self._last_auth_time = current_time + logger.info("Successfully logged in to Synology NAS") + logger.debug(f"Session ID obtained: {self.sid[:5]}...") # Показываем только начало SID для безопасности + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to log in to Synology NAS: Error code {error_code}") + + # Дополнительная диагностика + if error_code == 400: + logger.error("Authentication error: Invalid credentials") + elif error_code == 401: + logger.error("Authentication error: Account disabled") + elif error_code == 402: + logger.error("Authentication error: Permission denied") + elif error_code == 403: + logger.error("Authentication error: 2-factor authentication required") + elif error_code == 404: + logger.error("Authentication error: Failed to authenticate with 2-factor authentication") + + return False + + except requests.exceptions.Timeout: + logger.error(f"Connection timeout after {SYNOLOGY_TIMEOUT} seconds") + return False + except requests.exceptions.ConnectionError as e: + logger.error(f"Connection error: {str(e)}") + logger.debug(f"Connection details: {self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}") + return False + except requests.RequestException as e: + logger.error(f"Request error: {str(e)}") + return False + except Exception as e: + logger.error(f"Unexpected error during login: {str(e)}", exc_info=True) + return False + + def logout(self) -> bool: + """Выход из API Synology NAS""" + if not self.sid: + return True + + try: + url = f"{self.base_url}/auth.cgi" + params = { + "api": "SYNO.API.Auth", + "version": "1", + "method": "logout", + "session": "SynologyPowerControlBot", + "_sid": self.sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + self.sid = None + logger.info("Successfully logged out from Synology NAS") + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to log out from Synology NAS: Error code {error_code}") + return False + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + return False + + def _make_api_request(self, api_name: str, method: str, version: int = 1, + params: Optional[Dict[str, Any]] = None, retry_auth: bool = True) -> Optional[Dict[str, Any]]: + """Обобщенный метод для выполнения API запросов с обработкой ошибок""" + # Проверка наличия авторизации + if not self.sid and not self.login(): + logger.error(f"Not authenticated for API request: {api_name}.{method}") + return None + + # Подготовка базовых параметров запроса + base_params = { + "api": api_name, + "version": str(version), + "method": method, + "sid": self.sid + } + + # Добавление дополнительных параметров, если они заданы + if params: + base_params.update(params) + + url = f"{self.base_url}/entry.cgi" + logger.debug(f"API request: {api_name}.{method} v{version}") + + try: + start_time = time.time() + response = self.session.get( + url, + params=base_params, + timeout=self.default_timeout, + verify=False + ) + elapsed_time = time.time() - start_time + logger.debug(f"API request completed in {elapsed_time:.2f}s with status {response.status_code}") + + # Проверка статуса HTTP + if response.status_code != 200: + logger.error(f"HTTP error: {response.status_code} for {api_name}.{method}") + return None + + try: + data = response.json() + except json.JSONDecodeError: + logger.error(f"Failed to decode JSON response for {api_name}.{method}") + logger.debug(f"Response content: {response.text[:200]}") + return None + + if data.get("success"): + logger.info(f"API request successful for {method}") + return data.get("data", {}) + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"API error for {api_name}.{method}: {error_code}") + + # Если ошибка связана с авторизацией и нам разрешено повторить попытку + if error_code in [106, 107, 119] and retry_auth: + logger.info("Session may have expired, re-authenticating...") + self.sid = None # Сбрасываем SID + if self.login(): + logger.info("Re-authenticated, retrying API request...") + # Рекурсивный вызов, но без повторной авторизации во избежание зацикливания + return self._make_api_request(api_name, method, version, params, False) + + return None + + except requests.exceptions.Timeout: + logger.error(f"Request timeout for {api_name}.{method}") + return None + except requests.exceptions.ConnectionError as e: + logger.error(f"Connection error for {api_name}.{method}: {str(e)}") + return None + except Exception as e: + logger.error(f"Unexpected error during {api_name}.{method}: {str(e)}") + return None + + def get_system_status(self) -> Dict[str, Any]: + """Получение статуса системы""" + # Проверяем доступность системы + if not self.is_online(): + logger.info("Device is offline, skipping API request") + return {"status": "offline"} + + # Проверяем, есть ли кэшированный результат + cache_key = "system_status" + current_time = time.time() + if cache_key in self._cache and current_time - self._cache_ttl.get(cache_key, 0) < 60: + logger.debug("Using cached system status") + return self._cache[cache_key] + + # Делаем API запрос + result = self._make_api_request("SYNO.DSM.Info", "getinfo") + + if result: + # Формируем расширенный ответ с дополнительной информацией + system_info = { + "status": "online", + "hostname": result.get("hostname", "unknown"), + "model": result.get("model", "unknown"), + "version": result.get("version", "unknown"), + "uptime": result.get("uptime", 0), + "time": current_time, + "is_online": True + } + + # Сохраняем в кэше + self._cache[cache_key] = system_info + self._cache_ttl[cache_key] = current_time + + return system_info + else: + # Если запрос не удался, но система онлайн + return { + "status": "error", + "error": "Failed to fetch system information", + "is_online": True, + "time": current_time + } + + def shutdown_system(self) -> bool: + """Выключение системы""" + # Проверяем, включено ли устройство перед попыткой его выключить + if not self.is_online(force_check=True): + logger.info("Synology NAS is already offline, no need to shut down") + return True + + logger.info("Attempting to shutdown Synology NAS...") + + # Пробуем DSM.System API (первый метод) + result = self._make_api_request("SYNO.DSM.System", "shutdown") + if result is not None: + logger.info("Successfully initiated system shutdown using SYNO.DSM.System") + + # Даем системе время начать процесс выключения + logger.info("Waiting for shutdown to initialize...") + sleep(5) + + # Проверяем статус + if not self.is_online(force_check=True): + logger.info("System is now offline. Shutdown confirmed successful.") + return True + else: + logger.info("System still appears to be online, but shutdown may be in progress.") + return True + + # Пробуем альтернативный Core.System API (второй метод) + logger.info("First shutdown method failed, trying alternative API...") + result = self._make_api_request("SYNO.Core.System", "shutdown", version=3) + if result is not None: + logger.info("Successfully initiated system shutdown using SYNO.Core.System") + + # Даем системе время начать процесс выключения + logger.info("Waiting for shutdown to initialize...") + sleep(5) + + # Проверяем статус + if not self.is_online(force_check=True): + logger.info("System is now offline. Shutdown confirmed successful.") + return True + else: + logger.info("System still appears to be online, but shutdown may be in progress.") + return True + + # Если оба метода не сработали, но система стала недоступна + if not self.is_online(force_check=True): + logger.info("System appears to be shutting down despite API errors") + return True + + logger.error("Failed to shutdown system after trying multiple APIs") + return False + + def reboot_system(self) -> bool: + """Перезагрузка системы""" + # Проверяем, включена ли система + if not self.is_online(force_check=True): + logger.error("Cannot reboot: System is offline") + return False + + logger.info("Attempting to reboot Synology NAS...") + + # Пробуем DSM.System API (первый метод) + result = self._make_api_request("SYNO.DSM.System", "reboot") + if result is not None: + logger.info("Successfully initiated system reboot using SYNO.DSM.System") + + # Даем системе время начать процесс перезагрузки + logger.info("Waiting for reboot to initialize...") + sleep(5) + + # Ждем, пока система станет недоступна (признак перезагрузки) + reboot_started = False + for i in range(12): # Проверяем в течение 60 секунд (12 * 5сек) + if not self.is_online(force_check=True): + logger.info(f"System went offline after {i*5} seconds, reboot in progress") + reboot_started = True + break + logger.debug(f"System still online, waiting... ({i+1}/12)") + sleep(5) + + if reboot_started: + # Ждем, пока система снова станет доступна + logger.info("Waiting for system to come back online...") + return self.wait_for_boot(max_attempts=30, delay=10) + else: + logger.warning("System did not go offline after reboot command") + return False + + return True + + # Пробуем альтернативный Core.System API (второй метод) + logger.info("First reboot method failed, trying alternative API...") + result = self._make_api_request("SYNO.Core.System", "reboot", version=3) + if result is not None: + logger.info("Successfully initiated system reboot using SYNO.Core.System") + + # Даем системе время начать процесс перезагрузки + logger.info("Waiting for reboot to initialize...") + sleep(5) + + # Ждем, пока система станет недоступна (признак перезагрузки) + reboot_started = False + for i in range(12): # Проверяем в течение 60 секунд (12 * 5сек) + if not self.is_online(force_check=True): + logger.info(f"System went offline after {i*5} seconds, reboot in progress") + reboot_started = True + break + logger.debug(f"System still online, waiting... ({i+1}/12)") + sleep(5) + + if reboot_started: + # Ждем, пока система снова станет доступна + logger.info("Waiting for system to come back online...") + return self.wait_for_boot(max_attempts=30, delay=10) + else: + logger.warning("System did not go offline after reboot command") + return False + + logger.error("Failed to reboot system after trying multiple APIs") + return False + + def _get_error_description(self, error_code: int) -> str: + """Получение описания ошибки по коду""" + error_descriptions = { + 100: "Unknown error", + 101: "Invalid parameter", + 102: "API does not exist", + 103: "Method does not exist", + 104: "Version does not support", + 105: "Permission denied", + 106: "Session timeout", + 107: "Session interrupted by duplicate login", + 400: "Invalid credentials", + 401: "Account disabled", + 402: "Permission denied", + 403: "2FA required", + 404: "Failed to authenticate with 2FA" + } + return error_descriptions.get(error_code, "Unknown error code") + + def _check_tcp_connection(self) -> bool: + """Проверка базового TCP-соединения с Synology NAS""" + try: + socket_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + socket_obj.settimeout(SYNOLOGY_TIMEOUT) + result = socket_obj.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT)) + socket_obj.close() + + return result == 0 + except socket.error as e: + logger.error(f"Socket error during connection check: {str(e)}") + return False + except Exception as e: + logger.error(f"Unexpected error during connection check: {str(e)}") + return False + + def is_online(self, force_check=False) -> bool: + """Проверка онлайн-статуса Synology NAS""" + # Используем кэшированное значение, если доступно и не устарело + current_time = time.time() + if not force_check and (current_time - self._last_online_check) < self._online_check_interval: + logger.debug(f"Using cached online status: {self._last_online_status}") + return self._last_online_status + + logger.info("Checking if NAS is online...") + + # Проверяем TCP-соединение + online_status = self._check_tcp_connection() + logger.info(f"Detected Synology NAS online status: {online_status}") + + # Если TCP-соединение успешно и у нас есть действующий SID, + # попробуем более детальную проверку через API + if online_status and self.sid: + logger.info("Trying to fetch more detailed online status through API...") + try: + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.DSM.Info", + "version": "1", + "method": "getinfo", + "sid": self.sid + } + + response = self.session.get( + url, + params=params, + timeout=self.default_timeout, + verify=False + ) + + if response.status_code == 200: + data = response.json() + if data.get("success"): + logger.info("API request successful for getinfo") + logger.info("Synology NAS is online with API access") + else: + logger.warning("API response indicates an error, but NAS is reachable") + logger.debug(f"API error: {data.get('error', {}).get('code', -1)}") + else: + logger.warning(f"API returned status code {response.status_code}") + except Exception as e: + logger.warning(f"API check failed, but TCP connection succeeded: {str(e)}") + + # Обновляем кэшированное значение + self._last_online_check = current_time + self._last_online_status = online_status + + return online_status + + def wake_on_lan(self) -> bool: + """Отправка Wake-on-LAN пакета для включения Synology NAS""" + if not SYNOLOGY_MAC: + logger.error("MAC address not configured") + return False + + try: + # Преобразование MAC-адреса в байты + mac_address = SYNOLOGY_MAC.replace(':', '').replace('-', '') + if len(mac_address) != 12: + logger.error(f"Invalid MAC address format: {SYNOLOGY_MAC}") + return False + + try: + mac_bytes = bytes.fromhex(mac_address) + except ValueError as e: + logger.error(f"Failed to parse MAC address: {str(e)}") + logger.error(f"MAC address must contain only hex characters: {SYNOLOGY_MAC}") + return False + + # Создание Magic Packet + magic_packet = b'\xff' * 6 + mac_bytes * 16 + + # Отправка пакета на конкретный адрес + try: + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) + sock.sendto(magic_packet, (SYNOLOGY_HOST, WOL_PORT)) + sock.close() + logger.info(f"Wake-on-LAN packet sent to {SYNOLOGY_MAC} at {SYNOLOGY_HOST}:{WOL_PORT}") + except Exception as e: + logger.error(f"Error sending directed WoL packet: {str(e)}") + return False + + # Для надежности отправляем также широковещательный пакет + try: + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) + + # Используем стандартный широковещательный адрес + broadcast_addr = "255.255.255.255" + sock.sendto(magic_packet, (broadcast_addr, WOL_PORT)) + sock.close() + logger.info(f"Wake-on-LAN broadcast packet sent to {broadcast_addr}:{WOL_PORT}") + except Exception as e: + logger.warning(f"Error sending broadcast WoL packet (non-critical): {str(e)}") + # Не считаем ошибкой, т.к. основной пакет уже отправлен + + return True + + except Exception as e: + logger.error(f"Unexpected error in wake_on_lan: {str(e)}") + return False + + def wait_for_boot(self, max_attempts: int = 30, delay: int = 10) -> bool: + """Ожидание загрузки Synology NAS""" + logger.info(f"Waiting for Synology NAS to boot (timeout: {max_attempts * delay}s)...") + + for attempt in range(max_attempts): + # Принудительно проверяем статус без использования кэша + if self.is_online(force_check=True): + logger.info(f"Synology NAS is online after {attempt + 1} attempts ({(attempt + 1) * delay}s)") + + # Проверяем, что не только сеть доступна, но и API загрузился + api_ready = False + logger.info("Waiting for API services to initialize...") + + for api_check in range(5): # Даем еще до 50 секунд для загрузки API + if self.sid or self.login(): + api_ready = True + logger.info(f"API services are ready after {api_check + 1} attempts") + break + logger.debug(f"API not ready yet, waiting... ({api_check + 1}/5)") + sleep(10) + + if not api_ready: + logger.warning("System is online but API services may not be fully initialized") + + # Дадим дополнительное время для полной загрузки всех сервисов + sleep(delay) + return True + + sleep(delay) + logger.info(f"Waiting for boot... Attempt {attempt + 1}/{max_attempts}") + + logger.error(f"Synology NAS didn't come online after {max_attempts * delay} seconds") + return False + + def power_on(self) -> bool: + """Включение Synology NAS""" + # Принудительная проверка статуса + if self.is_online(force_check=True): + logger.info("Synology NAS is already online") + return True + + logger.info("Powering on Synology NAS via Wake-on-LAN...") + + # Проверяем, настроен ли MAC-адрес + if not SYNOLOGY_MAC: + logger.error("Cannot power on: MAC address not configured in settings") + return False + + # Пробуем отправить несколько WoL пакетов для надежности + success = False + for attempt in range(3): + logger.debug(f"Sending WoL packet, attempt {attempt + 1}/3") + if self.wake_on_lan(): + success = True + break + sleep(1) + + if not success: + logger.error("Failed to send Wake-on-LAN packets") + return False + + # Ожидание загрузки + logger.info("WoL packets sent successfully, waiting for system to boot...") + boot_result = self.wait_for_boot(max_attempts=30, delay=10) + + if boot_result: + # Проверяем доступность API после загрузки + system_status = self.get_system_status() + if system_status.get("status") == "online": + logger.info("System booted successfully with API access") + return True + else: + logger.warning("System appears to be online but API may not be fully ready") + return True + else: + logger.error("System did not come online after WoL") + return False + + def power_off(self) -> bool: + """Выключение Synology NAS""" + if not self.is_online(force_check=True): + logger.info("Synology NAS is already offline") + return True + + logger.info("Powering off Synology NAS...") + result = self.shutdown_system() + + if result: + # Дополнительная проверка, что система действительно выключилась + logger.info("Verifying system is offline...") + for attempt in range(12): # Проверяем в течение 2 минут (12 * 10 сек) + if not self.is_online(force_check=True): + logger.info(f"System confirmed offline after {attempt * 10} seconds") + return True + logger.debug(f"System still shutting down, checking again... ({attempt + 1}/12)") + sleep(10) + + logger.warning("System still appears to be online after shutdown command") + return False + else: + logger.error("Failed to initiate shutdown") + return False + + # Заглушки для расширенных методов + def get_shared_folders(self) -> List[Dict[str, Any]]: + """Получение списка общих папок (заглушка)""" + logger.warning("Function get_shared_folders() is not implemented yet") + return [] + + def get_system_load(self) -> Dict[str, Any]: + """Получение информации о загрузке системы (заглушка)""" + logger.warning("Function get_system_load() is not implemented yet") + return {} + + def get_storage_status(self) -> Dict[str, Any]: + """Получение подробной информации о хранилище (заглушка)""" + logger.warning("Function get_storage_status() is not implemented yet") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for storage status request") + return {"error": "authentication_failed"} + + try: + # Здесь будет реализация после добавления соответствующего API + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.Storage.CGI.Storage", + "version": "1", + "method": "load_info", + "sid": self.sid + } + + # В текущей версии просто возвращаем заглушку + return { + "volumes": [], + "disks": [], + "total_size": 0, + "total_used": 0, + "error": "not_implemented" + } + except Exception as e: + logger.error(f"Error in get_storage_status: {str(e)}") + return {"error": str(e)} + + def get_security_status(self) -> Dict[str, Any]: + """Получение информации о состоянии безопасности (заглушка)""" + logger.warning("Function get_security_status() is not implemented yet") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for security status request") + return {"success": False, "error": "authentication_failed"} + + try: + # Здесь будет реализация после добавления соответствующего API + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.Core.SecurityScan.Status", + "version": "1", + "method": "get", + "sid": self.sid + } + + # В текущей версии просто возвращаем заглушку + return { + "success": False, + "status": "not_implemented", + "last_check": None, + "is_secure": False, + "error": "not_implemented" + } + except Exception as e: + logger.error(f"Error in get_security_status: {str(e)}") + return {"success": False, "error": str(e)} diff --git a/.history/src/api/synology_20250830075326.py b/.history/src/api/synology_20250830075326.py new file mode 100644 index 0000000..4337cd4 --- /dev/null +++ b/.history/src/api/synology_20250830075326.py @@ -0,0 +1,762 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Модуль для взаимодействия с API Synology NAS +""" + +import requests +from requests.adapters import HTTPAdapter +import json +import logging +import time +import urllib3 +from urllib3.util import Retry +from typing import Dict, Any, Optional, List +import socket +import struct +from time import sleep + +from src.config.config import ( + SYNOLOGY_HOST, + SYNOLOGY_PORT, + SYNOLOGY_USERNAME, + SYNOLOGY_PASSWORD, + SYNOLOGY_SECURE, + SYNOLOGY_TIMEOUT, + SYNOLOGY_MAC, + WOL_PORT +) + +# Отключение предупреждений о небезопасных SSL-соединениях +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + +logger = logging.getLogger(__name__) + +class SynologyAPI: + """Класс для взаимодействия с API Synology NAS""" + + def __init__(self): + """Инициализация класса SynologyAPI""" + logger.info("Creating API with auto-retry and connection pool") + logger.debug(f"Connection details: Host={SYNOLOGY_HOST}, Port={SYNOLOGY_PORT}, Secure={SYNOLOGY_SECURE}") + + self.protocol = "https" if SYNOLOGY_SECURE else "http" + self.base_url = f"{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi" + self.sid = None + self.session = requests.Session() + + # Настройка SSL + if self.protocol == "https": + logger.debug("SSL enabled, disabling certificate verification for internal network") + self.session.verify = False # Отключаем проверку SSL для внутренней сети + + # Добавляем повторные попытки для HTTP-запросов + retry_strategy = Retry( + total=5, # Увеличиваем количество попыток + status_forcelist=[429, 500, 502, 503, 504, 404], + allowed_methods=["GET", "POST"], + backoff_factor=1.5, # Увеличиваем задержку между попытками + respect_retry_after_header=True + ) + adapter = HTTPAdapter( + max_retries=retry_strategy, + pool_connections=3, + pool_maxsize=10 + ) + self.session.mount("http://", adapter) + self.session.mount("https://", adapter) + + # Таймауты будут указаны в запросах + self.default_timeout = (SYNOLOGY_TIMEOUT, SYNOLOGY_TIMEOUT*2) + logger.debug(f"Setting default request timeout: {self.default_timeout}") + + # Кэш для хранения результатов запросов + self._cache = {} + self._cache_ttl = {} + self._last_online_check = 0 + self._last_online_status = False + self._online_check_interval = 30 # Интервал для кэширования проверок онлайн-статуса + + # Время последней успешной аутентификации и срок действия сессии + self._last_auth_time = 0 + self._auth_expiry = 3600 # По умолчанию 1 час + + def login(self) -> bool: + """Авторизация в API Synology NAS""" + # Проверяем, действительна ли текущая сессия + current_time = time.time() + if self.sid and (current_time - self._last_auth_time) < self._auth_expiry: + logger.debug("Using existing session, still valid") + return True + + logger.info("Attempting to authenticate with Synology NAS...") + logger.debug(f"Base URL: {self.base_url}") + + # Проверяем доступность NAS перед запросом с помощью прямой TCP-проверки + # Избегаем вызова is_online(), чтобы не создавать рекурсию + online_status = self._check_tcp_connection() + if not online_status: + logger.error("Cannot login: Synology NAS is not reachable") + return False + + try: + url = f"{self.base_url}/auth.cgi" + params = { + "api": "SYNO.API.Auth", + "version": "3", + "method": "login", + "account": SYNOLOGY_USERNAME, + "passwd": SYNOLOGY_PASSWORD, + "session": "SynologyPowerControlBot", + "format": "cookie" + } + + logger.debug(f"Sending auth request to {url}") + start_time = time.time() + response = self.session.get( + url, + params=params, + timeout=self.default_timeout, + verify=False + ) + elapsed_time = time.time() - start_time + logger.debug(f"Auth request completed in {elapsed_time:.2f}s with status code {response.status_code}") + + # Проверка статуса HTTP + if response.status_code != 200: + logger.error(f"HTTP error: {response.status_code}") + logger.debug(f"Response content: {response.text[:200]}") + return False + + try: + data = response.json() + except json.JSONDecodeError: + logger.error("Failed to decode JSON response") + logger.debug(f"Response content: {response.text[:200]}") + return False + + if data.get("success"): + self.sid = data.get("data", {}).get("sid") + self._last_auth_time = current_time + logger.info("Successfully logged in to Synology NAS") + logger.debug(f"Session ID obtained: {self.sid[:5] if self.sid else 'None'}...") # Показываем только начало SID для безопасности + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to log in to Synology NAS: Error code {error_code}") + + # Дополнительная диагностика + if error_code == 400: + logger.error("Authentication error: Invalid credentials") + elif error_code == 401: + logger.error("Authentication error: Account disabled") + elif error_code == 402: + logger.error("Authentication error: Permission denied") + elif error_code == 403: + logger.error("Authentication error: 2-factor authentication required") + elif error_code == 404: + logger.error("Authentication error: Failed to authenticate with 2-factor authentication") + + return False + + except requests.exceptions.Timeout: + logger.error(f"Connection timeout after {SYNOLOGY_TIMEOUT} seconds") + return False + except requests.exceptions.ConnectionError as e: + logger.error(f"Connection error: {str(e)}") + logger.debug(f"Connection details: {self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}") + return False + except requests.RequestException as e: + logger.error(f"Request error: {str(e)}") + return False + except Exception as e: + logger.error(f"Unexpected error during login: {str(e)}", exc_info=True) + return False + + def logout(self) -> bool: + """Выход из API Synology NAS""" + if not self.sid: + return True + + try: + url = f"{self.base_url}/auth.cgi" + params = { + "api": "SYNO.API.Auth", + "version": "1", + "method": "logout", + "session": "SynologyPowerControlBot", + "_sid": self.sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + self.sid = None + logger.info("Successfully logged out from Synology NAS") + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to log out from Synology NAS: Error code {error_code}") + return False + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + return False + + def _make_api_request(self, api_name: str, method: str, version: int = 1, + params: Optional[Dict[str, Any]] = None, retry_auth: bool = True) -> Optional[Dict[str, Any]]: + """Обобщенный метод для выполнения API запросов с обработкой ошибок""" + # Проверка наличия авторизации + if not self.sid and not self.login(): + logger.error(f"Not authenticated for API request: {api_name}.{method}") + return None + + # Подготовка базовых параметров запроса + base_params = { + "api": api_name, + "version": str(version), + "method": method, + "sid": self.sid + } + + # Добавление дополнительных параметров, если они заданы + if params: + base_params.update(params) + + url = f"{self.base_url}/entry.cgi" + logger.debug(f"API request: {api_name}.{method} v{version}") + + try: + start_time = time.time() + response = self.session.get( + url, + params=base_params, + timeout=self.default_timeout, + verify=False + ) + elapsed_time = time.time() - start_time + logger.debug(f"API request completed in {elapsed_time:.2f}s with status {response.status_code}") + + # Проверка статуса HTTP + if response.status_code != 200: + logger.error(f"HTTP error: {response.status_code} for {api_name}.{method}") + return None + + try: + data = response.json() + except json.JSONDecodeError: + logger.error(f"Failed to decode JSON response for {api_name}.{method}") + logger.debug(f"Response content: {response.text[:200]}") + return None + + if data.get("success"): + logger.info(f"API request successful for {method}") + return data.get("data", {}) + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"API error for {api_name}.{method}: {error_code}") + + # Если ошибка связана с авторизацией и нам разрешено повторить попытку + if error_code in [106, 107, 119] and retry_auth: + logger.info("Session may have expired, re-authenticating...") + self.sid = None # Сбрасываем SID + if self.login(): + logger.info("Re-authenticated, retrying API request...") + # Рекурсивный вызов, но без повторной авторизации во избежание зацикливания + return self._make_api_request(api_name, method, version, params, False) + + return None + + except requests.exceptions.Timeout: + logger.error(f"Request timeout for {api_name}.{method}") + return None + except requests.exceptions.ConnectionError as e: + logger.error(f"Connection error for {api_name}.{method}: {str(e)}") + return None + except Exception as e: + logger.error(f"Unexpected error during {api_name}.{method}: {str(e)}") + return None + + def get_system_status(self) -> Dict[str, Any]: + """Получение статуса системы""" + # Проверяем доступность системы + if not self.is_online(): + logger.info("Device is offline, skipping API request") + return {"status": "offline"} + + # Проверяем, есть ли кэшированный результат + cache_key = "system_status" + current_time = time.time() + if cache_key in self._cache and current_time - self._cache_ttl.get(cache_key, 0) < 60: + logger.debug("Using cached system status") + return self._cache[cache_key] + + # Делаем API запрос + result = self._make_api_request("SYNO.DSM.Info", "getinfo") + + if result: + # Формируем расширенный ответ с дополнительной информацией + system_info = { + "status": "online", + "hostname": result.get("hostname", "unknown"), + "model": result.get("model", "unknown"), + "version": result.get("version", "unknown"), + "uptime": result.get("uptime", 0), + "time": current_time, + "is_online": True + } + + # Сохраняем в кэше + self._cache[cache_key] = system_info + self._cache_ttl[cache_key] = current_time + + return system_info + else: + # Если запрос не удался, но система онлайн + return { + "status": "error", + "error": "Failed to fetch system information", + "is_online": True, + "time": current_time + } + + def shutdown_system(self) -> bool: + """Выключение системы""" + # Проверяем, включено ли устройство перед попыткой его выключить + if not self.is_online(force_check=True): + logger.info("Synology NAS is already offline, no need to shut down") + return True + + logger.info("Attempting to shutdown Synology NAS...") + + # Пробуем DSM.System API (первый метод) + result = self._make_api_request("SYNO.DSM.System", "shutdown") + if result is not None: + logger.info("Successfully initiated system shutdown using SYNO.DSM.System") + + # Даем системе время начать процесс выключения + logger.info("Waiting for shutdown to initialize...") + sleep(5) + + # Проверяем статус + if not self.is_online(force_check=True): + logger.info("System is now offline. Shutdown confirmed successful.") + return True + else: + logger.info("System still appears to be online, but shutdown may be in progress.") + return True + + # Пробуем альтернативный Core.System API (второй метод) + logger.info("First shutdown method failed, trying alternative API...") + result = self._make_api_request("SYNO.Core.System", "shutdown", version=3) + if result is not None: + logger.info("Successfully initiated system shutdown using SYNO.Core.System") + + # Даем системе время начать процесс выключения + logger.info("Waiting for shutdown to initialize...") + sleep(5) + + # Проверяем статус + if not self.is_online(force_check=True): + logger.info("System is now offline. Shutdown confirmed successful.") + return True + else: + logger.info("System still appears to be online, but shutdown may be in progress.") + return True + + # Если оба метода не сработали, но система стала недоступна + if not self.is_online(force_check=True): + logger.info("System appears to be shutting down despite API errors") + return True + + logger.error("Failed to shutdown system after trying multiple APIs") + return False + + def reboot_system(self) -> bool: + """Перезагрузка системы""" + # Проверяем, включена ли система + if not self.is_online(force_check=True): + logger.error("Cannot reboot: System is offline") + return False + + logger.info("Attempting to reboot Synology NAS...") + + # Пробуем DSM.System API (первый метод) + result = self._make_api_request("SYNO.DSM.System", "reboot") + if result is not None: + logger.info("Successfully initiated system reboot using SYNO.DSM.System") + + # Даем системе время начать процесс перезагрузки + logger.info("Waiting for reboot to initialize...") + sleep(5) + + # Ждем, пока система станет недоступна (признак перезагрузки) + reboot_started = False + for i in range(12): # Проверяем в течение 60 секунд (12 * 5сек) + if not self.is_online(force_check=True): + logger.info(f"System went offline after {i*5} seconds, reboot in progress") + reboot_started = True + break + logger.debug(f"System still online, waiting... ({i+1}/12)") + sleep(5) + + if reboot_started: + # Ждем, пока система снова станет доступна + logger.info("Waiting for system to come back online...") + return self.wait_for_boot(max_attempts=30, delay=10) + else: + logger.warning("System did not go offline after reboot command") + return False + + return True + + # Пробуем альтернативный Core.System API (второй метод) + logger.info("First reboot method failed, trying alternative API...") + result = self._make_api_request("SYNO.Core.System", "reboot", version=3) + if result is not None: + logger.info("Successfully initiated system reboot using SYNO.Core.System") + + # Даем системе время начать процесс перезагрузки + logger.info("Waiting for reboot to initialize...") + sleep(5) + + # Ждем, пока система станет недоступна (признак перезагрузки) + reboot_started = False + for i in range(12): # Проверяем в течение 60 секунд (12 * 5сек) + if not self.is_online(force_check=True): + logger.info(f"System went offline after {i*5} seconds, reboot in progress") + reboot_started = True + break + logger.debug(f"System still online, waiting... ({i+1}/12)") + sleep(5) + + if reboot_started: + # Ждем, пока система снова станет доступна + logger.info("Waiting for system to come back online...") + return self.wait_for_boot(max_attempts=30, delay=10) + else: + logger.warning("System did not go offline after reboot command") + return False + + logger.error("Failed to reboot system after trying multiple APIs") + return False + + def _get_error_description(self, error_code: int) -> str: + """Получение описания ошибки по коду""" + error_descriptions = { + 100: "Unknown error", + 101: "Invalid parameter", + 102: "API does not exist", + 103: "Method does not exist", + 104: "Version does not support", + 105: "Permission denied", + 106: "Session timeout", + 107: "Session interrupted by duplicate login", + 400: "Invalid credentials", + 401: "Account disabled", + 402: "Permission denied", + 403: "2FA required", + 404: "Failed to authenticate with 2FA" + } + return error_descriptions.get(error_code, "Unknown error code") + + def _check_tcp_connection(self) -> bool: + """Проверка базового TCP-соединения с Synology NAS""" + try: + socket_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + socket_obj.settimeout(SYNOLOGY_TIMEOUT) + result = socket_obj.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT)) + socket_obj.close() + + return result == 0 + except socket.error as e: + logger.error(f"Socket error during connection check: {str(e)}") + return False + except Exception as e: + logger.error(f"Unexpected error during connection check: {str(e)}") + return False + + def is_online(self, force_check=False) -> bool: + """Проверка онлайн-статуса Synology NAS""" + # Используем кэшированное значение, если доступно и не устарело + current_time = time.time() + if not force_check and (current_time - self._last_online_check) < self._online_check_interval: + logger.debug(f"Using cached online status: {self._last_online_status}") + return self._last_online_status + + logger.info("Checking if NAS is online...") + + # Проверяем TCP-соединение + online_status = self._check_tcp_connection() + logger.info(f"Detected Synology NAS online status: {online_status}") + + # Если TCP-соединение успешно и у нас есть действующий SID, + # попробуем более детальную проверку через API + if online_status and self.sid: + logger.info("Trying to fetch more detailed online status through API...") + try: + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.DSM.Info", + "version": "1", + "method": "getinfo", + "sid": self.sid + } + + response = self.session.get( + url, + params=params, + timeout=self.default_timeout, + verify=False + ) + + if response.status_code == 200: + data = response.json() + if data.get("success"): + logger.info("API request successful for getinfo") + logger.info("Synology NAS is online with API access") + else: + logger.warning("API response indicates an error, but NAS is reachable") + logger.debug(f"API error: {data.get('error', {}).get('code', -1)}") + else: + logger.warning(f"API returned status code {response.status_code}") + except Exception as e: + logger.warning(f"API check failed, but TCP connection succeeded: {str(e)}") + + # Обновляем кэшированное значение + self._last_online_check = current_time + self._last_online_status = online_status + + return online_status + + def wake_on_lan(self) -> bool: + """Отправка Wake-on-LAN пакета для включения Synology NAS""" + if not SYNOLOGY_MAC: + logger.error("MAC address not configured") + return False + + try: + # Преобразование MAC-адреса в байты + mac_address = SYNOLOGY_MAC.replace(':', '').replace('-', '') + if len(mac_address) != 12: + logger.error(f"Invalid MAC address format: {SYNOLOGY_MAC}") + return False + + try: + mac_bytes = bytes.fromhex(mac_address) + except ValueError as e: + logger.error(f"Failed to parse MAC address: {str(e)}") + logger.error(f"MAC address must contain only hex characters: {SYNOLOGY_MAC}") + return False + + # Создание Magic Packet + magic_packet = b'\xff' * 6 + mac_bytes * 16 + + # Отправка пакета на конкретный адрес + try: + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) + sock.sendto(magic_packet, (SYNOLOGY_HOST, WOL_PORT)) + sock.close() + logger.info(f"Wake-on-LAN packet sent to {SYNOLOGY_MAC} at {SYNOLOGY_HOST}:{WOL_PORT}") + except Exception as e: + logger.error(f"Error sending directed WoL packet: {str(e)}") + return False + + # Для надежности отправляем также широковещательный пакет + try: + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) + + # Используем стандартный широковещательный адрес + broadcast_addr = "255.255.255.255" + sock.sendto(magic_packet, (broadcast_addr, WOL_PORT)) + sock.close() + logger.info(f"Wake-on-LAN broadcast packet sent to {broadcast_addr}:{WOL_PORT}") + except Exception as e: + logger.warning(f"Error sending broadcast WoL packet (non-critical): {str(e)}") + # Не считаем ошибкой, т.к. основной пакет уже отправлен + + return True + + except Exception as e: + logger.error(f"Unexpected error in wake_on_lan: {str(e)}") + return False + + def wait_for_boot(self, max_attempts: int = 30, delay: int = 10) -> bool: + """Ожидание загрузки Synology NAS""" + logger.info(f"Waiting for Synology NAS to boot (timeout: {max_attempts * delay}s)...") + + for attempt in range(max_attempts): + # Принудительно проверяем статус без использования кэша + if self.is_online(force_check=True): + logger.info(f"Synology NAS is online after {attempt + 1} attempts ({(attempt + 1) * delay}s)") + + # Проверяем, что не только сеть доступна, но и API загрузился + api_ready = False + logger.info("Waiting for API services to initialize...") + + for api_check in range(5): # Даем еще до 50 секунд для загрузки API + if self.sid or self.login(): + api_ready = True + logger.info(f"API services are ready after {api_check + 1} attempts") + break + logger.debug(f"API not ready yet, waiting... ({api_check + 1}/5)") + sleep(10) + + if not api_ready: + logger.warning("System is online but API services may not be fully initialized") + + # Дадим дополнительное время для полной загрузки всех сервисов + sleep(delay) + return True + + sleep(delay) + logger.info(f"Waiting for boot... Attempt {attempt + 1}/{max_attempts}") + + logger.error(f"Synology NAS didn't come online after {max_attempts * delay} seconds") + return False + + def power_on(self) -> bool: + """Включение Synology NAS""" + # Принудительная проверка статуса + if self.is_online(force_check=True): + logger.info("Synology NAS is already online") + return True + + logger.info("Powering on Synology NAS via Wake-on-LAN...") + + # Проверяем, настроен ли MAC-адрес + if not SYNOLOGY_MAC: + logger.error("Cannot power on: MAC address not configured in settings") + return False + + # Пробуем отправить несколько WoL пакетов для надежности + success = False + for attempt in range(3): + logger.debug(f"Sending WoL packet, attempt {attempt + 1}/3") + if self.wake_on_lan(): + success = True + break + sleep(1) + + if not success: + logger.error("Failed to send Wake-on-LAN packets") + return False + + # Ожидание загрузки + logger.info("WoL packets sent successfully, waiting for system to boot...") + boot_result = self.wait_for_boot(max_attempts=30, delay=10) + + if boot_result: + # Проверяем доступность API после загрузки + system_status = self.get_system_status() + if system_status.get("status") == "online": + logger.info("System booted successfully with API access") + return True + else: + logger.warning("System appears to be online but API may not be fully ready") + return True + else: + logger.error("System did not come online after WoL") + return False + + def power_off(self) -> bool: + """Выключение Synology NAS""" + if not self.is_online(force_check=True): + logger.info("Synology NAS is already offline") + return True + + logger.info("Powering off Synology NAS...") + result = self.shutdown_system() + + if result: + # Дополнительная проверка, что система действительно выключилась + logger.info("Verifying system is offline...") + for attempt in range(12): # Проверяем в течение 2 минут (12 * 10 сек) + if not self.is_online(force_check=True): + logger.info(f"System confirmed offline after {attempt * 10} seconds") + return True + logger.debug(f"System still shutting down, checking again... ({attempt + 1}/12)") + sleep(10) + + logger.warning("System still appears to be online after shutdown command") + return False + else: + logger.error("Failed to initiate shutdown") + return False + + # Заглушки для расширенных методов + def get_shared_folders(self) -> List[Dict[str, Any]]: + """Получение списка общих папок (заглушка)""" + logger.warning("Function get_shared_folders() is not implemented yet") + return [] + + def get_system_load(self) -> Dict[str, Any]: + """Получение информации о загрузке системы (заглушка)""" + logger.warning("Function get_system_load() is not implemented yet") + return {} + + def get_storage_status(self) -> Dict[str, Any]: + """Получение подробной информации о хранилище (заглушка)""" + logger.warning("Function get_storage_status() is not implemented yet") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for storage status request") + return {"error": "authentication_failed"} + + try: + # Здесь будет реализация после добавления соответствующего API + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.Storage.CGI.Storage", + "version": "1", + "method": "load_info", + "sid": self.sid + } + + # В текущей версии просто возвращаем заглушку + return { + "volumes": [], + "disks": [], + "total_size": 0, + "total_used": 0, + "error": "not_implemented" + } + except Exception as e: + logger.error(f"Error in get_storage_status: {str(e)}") + return {"error": str(e)} + + def get_security_status(self) -> Dict[str, Any]: + """Получение информации о состоянии безопасности (заглушка)""" + logger.warning("Function get_security_status() is not implemented yet") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for security status request") + return {"success": False, "error": "authentication_failed"} + + try: + # Здесь будет реализация после добавления соответствующего API + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.Core.SecurityScan.Status", + "version": "1", + "method": "get", + "sid": self.sid + } + + # В текущей версии просто возвращаем заглушку + return { + "success": False, + "status": "not_implemented", + "last_check": None, + "is_secure": False, + "error": "not_implemented" + } + except Exception as e: + logger.error(f"Error in get_security_status: {str(e)}") + return {"success": False, "error": str(e)} diff --git a/.history/src/api/synology_20250830075348.py b/.history/src/api/synology_20250830075348.py new file mode 100644 index 0000000..4337cd4 --- /dev/null +++ b/.history/src/api/synology_20250830075348.py @@ -0,0 +1,762 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Модуль для взаимодействия с API Synology NAS +""" + +import requests +from requests.adapters import HTTPAdapter +import json +import logging +import time +import urllib3 +from urllib3.util import Retry +from typing import Dict, Any, Optional, List +import socket +import struct +from time import sleep + +from src.config.config import ( + SYNOLOGY_HOST, + SYNOLOGY_PORT, + SYNOLOGY_USERNAME, + SYNOLOGY_PASSWORD, + SYNOLOGY_SECURE, + SYNOLOGY_TIMEOUT, + SYNOLOGY_MAC, + WOL_PORT +) + +# Отключение предупреждений о небезопасных SSL-соединениях +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + +logger = logging.getLogger(__name__) + +class SynologyAPI: + """Класс для взаимодействия с API Synology NAS""" + + def __init__(self): + """Инициализация класса SynologyAPI""" + logger.info("Creating API with auto-retry and connection pool") + logger.debug(f"Connection details: Host={SYNOLOGY_HOST}, Port={SYNOLOGY_PORT}, Secure={SYNOLOGY_SECURE}") + + self.protocol = "https" if SYNOLOGY_SECURE else "http" + self.base_url = f"{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi" + self.sid = None + self.session = requests.Session() + + # Настройка SSL + if self.protocol == "https": + logger.debug("SSL enabled, disabling certificate verification for internal network") + self.session.verify = False # Отключаем проверку SSL для внутренней сети + + # Добавляем повторные попытки для HTTP-запросов + retry_strategy = Retry( + total=5, # Увеличиваем количество попыток + status_forcelist=[429, 500, 502, 503, 504, 404], + allowed_methods=["GET", "POST"], + backoff_factor=1.5, # Увеличиваем задержку между попытками + respect_retry_after_header=True + ) + adapter = HTTPAdapter( + max_retries=retry_strategy, + pool_connections=3, + pool_maxsize=10 + ) + self.session.mount("http://", adapter) + self.session.mount("https://", adapter) + + # Таймауты будут указаны в запросах + self.default_timeout = (SYNOLOGY_TIMEOUT, SYNOLOGY_TIMEOUT*2) + logger.debug(f"Setting default request timeout: {self.default_timeout}") + + # Кэш для хранения результатов запросов + self._cache = {} + self._cache_ttl = {} + self._last_online_check = 0 + self._last_online_status = False + self._online_check_interval = 30 # Интервал для кэширования проверок онлайн-статуса + + # Время последней успешной аутентификации и срок действия сессии + self._last_auth_time = 0 + self._auth_expiry = 3600 # По умолчанию 1 час + + def login(self) -> bool: + """Авторизация в API Synology NAS""" + # Проверяем, действительна ли текущая сессия + current_time = time.time() + if self.sid and (current_time - self._last_auth_time) < self._auth_expiry: + logger.debug("Using existing session, still valid") + return True + + logger.info("Attempting to authenticate with Synology NAS...") + logger.debug(f"Base URL: {self.base_url}") + + # Проверяем доступность NAS перед запросом с помощью прямой TCP-проверки + # Избегаем вызова is_online(), чтобы не создавать рекурсию + online_status = self._check_tcp_connection() + if not online_status: + logger.error("Cannot login: Synology NAS is not reachable") + return False + + try: + url = f"{self.base_url}/auth.cgi" + params = { + "api": "SYNO.API.Auth", + "version": "3", + "method": "login", + "account": SYNOLOGY_USERNAME, + "passwd": SYNOLOGY_PASSWORD, + "session": "SynologyPowerControlBot", + "format": "cookie" + } + + logger.debug(f"Sending auth request to {url}") + start_time = time.time() + response = self.session.get( + url, + params=params, + timeout=self.default_timeout, + verify=False + ) + elapsed_time = time.time() - start_time + logger.debug(f"Auth request completed in {elapsed_time:.2f}s with status code {response.status_code}") + + # Проверка статуса HTTP + if response.status_code != 200: + logger.error(f"HTTP error: {response.status_code}") + logger.debug(f"Response content: {response.text[:200]}") + return False + + try: + data = response.json() + except json.JSONDecodeError: + logger.error("Failed to decode JSON response") + logger.debug(f"Response content: {response.text[:200]}") + return False + + if data.get("success"): + self.sid = data.get("data", {}).get("sid") + self._last_auth_time = current_time + logger.info("Successfully logged in to Synology NAS") + logger.debug(f"Session ID obtained: {self.sid[:5] if self.sid else 'None'}...") # Показываем только начало SID для безопасности + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to log in to Synology NAS: Error code {error_code}") + + # Дополнительная диагностика + if error_code == 400: + logger.error("Authentication error: Invalid credentials") + elif error_code == 401: + logger.error("Authentication error: Account disabled") + elif error_code == 402: + logger.error("Authentication error: Permission denied") + elif error_code == 403: + logger.error("Authentication error: 2-factor authentication required") + elif error_code == 404: + logger.error("Authentication error: Failed to authenticate with 2-factor authentication") + + return False + + except requests.exceptions.Timeout: + logger.error(f"Connection timeout after {SYNOLOGY_TIMEOUT} seconds") + return False + except requests.exceptions.ConnectionError as e: + logger.error(f"Connection error: {str(e)}") + logger.debug(f"Connection details: {self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}") + return False + except requests.RequestException as e: + logger.error(f"Request error: {str(e)}") + return False + except Exception as e: + logger.error(f"Unexpected error during login: {str(e)}", exc_info=True) + return False + + def logout(self) -> bool: + """Выход из API Synology NAS""" + if not self.sid: + return True + + try: + url = f"{self.base_url}/auth.cgi" + params = { + "api": "SYNO.API.Auth", + "version": "1", + "method": "logout", + "session": "SynologyPowerControlBot", + "_sid": self.sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + self.sid = None + logger.info("Successfully logged out from Synology NAS") + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to log out from Synology NAS: Error code {error_code}") + return False + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + return False + + def _make_api_request(self, api_name: str, method: str, version: int = 1, + params: Optional[Dict[str, Any]] = None, retry_auth: bool = True) -> Optional[Dict[str, Any]]: + """Обобщенный метод для выполнения API запросов с обработкой ошибок""" + # Проверка наличия авторизации + if not self.sid and not self.login(): + logger.error(f"Not authenticated for API request: {api_name}.{method}") + return None + + # Подготовка базовых параметров запроса + base_params = { + "api": api_name, + "version": str(version), + "method": method, + "sid": self.sid + } + + # Добавление дополнительных параметров, если они заданы + if params: + base_params.update(params) + + url = f"{self.base_url}/entry.cgi" + logger.debug(f"API request: {api_name}.{method} v{version}") + + try: + start_time = time.time() + response = self.session.get( + url, + params=base_params, + timeout=self.default_timeout, + verify=False + ) + elapsed_time = time.time() - start_time + logger.debug(f"API request completed in {elapsed_time:.2f}s with status {response.status_code}") + + # Проверка статуса HTTP + if response.status_code != 200: + logger.error(f"HTTP error: {response.status_code} for {api_name}.{method}") + return None + + try: + data = response.json() + except json.JSONDecodeError: + logger.error(f"Failed to decode JSON response for {api_name}.{method}") + logger.debug(f"Response content: {response.text[:200]}") + return None + + if data.get("success"): + logger.info(f"API request successful for {method}") + return data.get("data", {}) + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"API error for {api_name}.{method}: {error_code}") + + # Если ошибка связана с авторизацией и нам разрешено повторить попытку + if error_code in [106, 107, 119] and retry_auth: + logger.info("Session may have expired, re-authenticating...") + self.sid = None # Сбрасываем SID + if self.login(): + logger.info("Re-authenticated, retrying API request...") + # Рекурсивный вызов, но без повторной авторизации во избежание зацикливания + return self._make_api_request(api_name, method, version, params, False) + + return None + + except requests.exceptions.Timeout: + logger.error(f"Request timeout for {api_name}.{method}") + return None + except requests.exceptions.ConnectionError as e: + logger.error(f"Connection error for {api_name}.{method}: {str(e)}") + return None + except Exception as e: + logger.error(f"Unexpected error during {api_name}.{method}: {str(e)}") + return None + + def get_system_status(self) -> Dict[str, Any]: + """Получение статуса системы""" + # Проверяем доступность системы + if not self.is_online(): + logger.info("Device is offline, skipping API request") + return {"status": "offline"} + + # Проверяем, есть ли кэшированный результат + cache_key = "system_status" + current_time = time.time() + if cache_key in self._cache and current_time - self._cache_ttl.get(cache_key, 0) < 60: + logger.debug("Using cached system status") + return self._cache[cache_key] + + # Делаем API запрос + result = self._make_api_request("SYNO.DSM.Info", "getinfo") + + if result: + # Формируем расширенный ответ с дополнительной информацией + system_info = { + "status": "online", + "hostname": result.get("hostname", "unknown"), + "model": result.get("model", "unknown"), + "version": result.get("version", "unknown"), + "uptime": result.get("uptime", 0), + "time": current_time, + "is_online": True + } + + # Сохраняем в кэше + self._cache[cache_key] = system_info + self._cache_ttl[cache_key] = current_time + + return system_info + else: + # Если запрос не удался, но система онлайн + return { + "status": "error", + "error": "Failed to fetch system information", + "is_online": True, + "time": current_time + } + + def shutdown_system(self) -> bool: + """Выключение системы""" + # Проверяем, включено ли устройство перед попыткой его выключить + if not self.is_online(force_check=True): + logger.info("Synology NAS is already offline, no need to shut down") + return True + + logger.info("Attempting to shutdown Synology NAS...") + + # Пробуем DSM.System API (первый метод) + result = self._make_api_request("SYNO.DSM.System", "shutdown") + if result is not None: + logger.info("Successfully initiated system shutdown using SYNO.DSM.System") + + # Даем системе время начать процесс выключения + logger.info("Waiting for shutdown to initialize...") + sleep(5) + + # Проверяем статус + if not self.is_online(force_check=True): + logger.info("System is now offline. Shutdown confirmed successful.") + return True + else: + logger.info("System still appears to be online, but shutdown may be in progress.") + return True + + # Пробуем альтернативный Core.System API (второй метод) + logger.info("First shutdown method failed, trying alternative API...") + result = self._make_api_request("SYNO.Core.System", "shutdown", version=3) + if result is not None: + logger.info("Successfully initiated system shutdown using SYNO.Core.System") + + # Даем системе время начать процесс выключения + logger.info("Waiting for shutdown to initialize...") + sleep(5) + + # Проверяем статус + if not self.is_online(force_check=True): + logger.info("System is now offline. Shutdown confirmed successful.") + return True + else: + logger.info("System still appears to be online, but shutdown may be in progress.") + return True + + # Если оба метода не сработали, но система стала недоступна + if not self.is_online(force_check=True): + logger.info("System appears to be shutting down despite API errors") + return True + + logger.error("Failed to shutdown system after trying multiple APIs") + return False + + def reboot_system(self) -> bool: + """Перезагрузка системы""" + # Проверяем, включена ли система + if not self.is_online(force_check=True): + logger.error("Cannot reboot: System is offline") + return False + + logger.info("Attempting to reboot Synology NAS...") + + # Пробуем DSM.System API (первый метод) + result = self._make_api_request("SYNO.DSM.System", "reboot") + if result is not None: + logger.info("Successfully initiated system reboot using SYNO.DSM.System") + + # Даем системе время начать процесс перезагрузки + logger.info("Waiting for reboot to initialize...") + sleep(5) + + # Ждем, пока система станет недоступна (признак перезагрузки) + reboot_started = False + for i in range(12): # Проверяем в течение 60 секунд (12 * 5сек) + if not self.is_online(force_check=True): + logger.info(f"System went offline after {i*5} seconds, reboot in progress") + reboot_started = True + break + logger.debug(f"System still online, waiting... ({i+1}/12)") + sleep(5) + + if reboot_started: + # Ждем, пока система снова станет доступна + logger.info("Waiting for system to come back online...") + return self.wait_for_boot(max_attempts=30, delay=10) + else: + logger.warning("System did not go offline after reboot command") + return False + + return True + + # Пробуем альтернативный Core.System API (второй метод) + logger.info("First reboot method failed, trying alternative API...") + result = self._make_api_request("SYNO.Core.System", "reboot", version=3) + if result is not None: + logger.info("Successfully initiated system reboot using SYNO.Core.System") + + # Даем системе время начать процесс перезагрузки + logger.info("Waiting for reboot to initialize...") + sleep(5) + + # Ждем, пока система станет недоступна (признак перезагрузки) + reboot_started = False + for i in range(12): # Проверяем в течение 60 секунд (12 * 5сек) + if not self.is_online(force_check=True): + logger.info(f"System went offline after {i*5} seconds, reboot in progress") + reboot_started = True + break + logger.debug(f"System still online, waiting... ({i+1}/12)") + sleep(5) + + if reboot_started: + # Ждем, пока система снова станет доступна + logger.info("Waiting for system to come back online...") + return self.wait_for_boot(max_attempts=30, delay=10) + else: + logger.warning("System did not go offline after reboot command") + return False + + logger.error("Failed to reboot system after trying multiple APIs") + return False + + def _get_error_description(self, error_code: int) -> str: + """Получение описания ошибки по коду""" + error_descriptions = { + 100: "Unknown error", + 101: "Invalid parameter", + 102: "API does not exist", + 103: "Method does not exist", + 104: "Version does not support", + 105: "Permission denied", + 106: "Session timeout", + 107: "Session interrupted by duplicate login", + 400: "Invalid credentials", + 401: "Account disabled", + 402: "Permission denied", + 403: "2FA required", + 404: "Failed to authenticate with 2FA" + } + return error_descriptions.get(error_code, "Unknown error code") + + def _check_tcp_connection(self) -> bool: + """Проверка базового TCP-соединения с Synology NAS""" + try: + socket_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + socket_obj.settimeout(SYNOLOGY_TIMEOUT) + result = socket_obj.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT)) + socket_obj.close() + + return result == 0 + except socket.error as e: + logger.error(f"Socket error during connection check: {str(e)}") + return False + except Exception as e: + logger.error(f"Unexpected error during connection check: {str(e)}") + return False + + def is_online(self, force_check=False) -> bool: + """Проверка онлайн-статуса Synology NAS""" + # Используем кэшированное значение, если доступно и не устарело + current_time = time.time() + if not force_check and (current_time - self._last_online_check) < self._online_check_interval: + logger.debug(f"Using cached online status: {self._last_online_status}") + return self._last_online_status + + logger.info("Checking if NAS is online...") + + # Проверяем TCP-соединение + online_status = self._check_tcp_connection() + logger.info(f"Detected Synology NAS online status: {online_status}") + + # Если TCP-соединение успешно и у нас есть действующий SID, + # попробуем более детальную проверку через API + if online_status and self.sid: + logger.info("Trying to fetch more detailed online status through API...") + try: + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.DSM.Info", + "version": "1", + "method": "getinfo", + "sid": self.sid + } + + response = self.session.get( + url, + params=params, + timeout=self.default_timeout, + verify=False + ) + + if response.status_code == 200: + data = response.json() + if data.get("success"): + logger.info("API request successful for getinfo") + logger.info("Synology NAS is online with API access") + else: + logger.warning("API response indicates an error, but NAS is reachable") + logger.debug(f"API error: {data.get('error', {}).get('code', -1)}") + else: + logger.warning(f"API returned status code {response.status_code}") + except Exception as e: + logger.warning(f"API check failed, but TCP connection succeeded: {str(e)}") + + # Обновляем кэшированное значение + self._last_online_check = current_time + self._last_online_status = online_status + + return online_status + + def wake_on_lan(self) -> bool: + """Отправка Wake-on-LAN пакета для включения Synology NAS""" + if not SYNOLOGY_MAC: + logger.error("MAC address not configured") + return False + + try: + # Преобразование MAC-адреса в байты + mac_address = SYNOLOGY_MAC.replace(':', '').replace('-', '') + if len(mac_address) != 12: + logger.error(f"Invalid MAC address format: {SYNOLOGY_MAC}") + return False + + try: + mac_bytes = bytes.fromhex(mac_address) + except ValueError as e: + logger.error(f"Failed to parse MAC address: {str(e)}") + logger.error(f"MAC address must contain only hex characters: {SYNOLOGY_MAC}") + return False + + # Создание Magic Packet + magic_packet = b'\xff' * 6 + mac_bytes * 16 + + # Отправка пакета на конкретный адрес + try: + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) + sock.sendto(magic_packet, (SYNOLOGY_HOST, WOL_PORT)) + sock.close() + logger.info(f"Wake-on-LAN packet sent to {SYNOLOGY_MAC} at {SYNOLOGY_HOST}:{WOL_PORT}") + except Exception as e: + logger.error(f"Error sending directed WoL packet: {str(e)}") + return False + + # Для надежности отправляем также широковещательный пакет + try: + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) + + # Используем стандартный широковещательный адрес + broadcast_addr = "255.255.255.255" + sock.sendto(magic_packet, (broadcast_addr, WOL_PORT)) + sock.close() + logger.info(f"Wake-on-LAN broadcast packet sent to {broadcast_addr}:{WOL_PORT}") + except Exception as e: + logger.warning(f"Error sending broadcast WoL packet (non-critical): {str(e)}") + # Не считаем ошибкой, т.к. основной пакет уже отправлен + + return True + + except Exception as e: + logger.error(f"Unexpected error in wake_on_lan: {str(e)}") + return False + + def wait_for_boot(self, max_attempts: int = 30, delay: int = 10) -> bool: + """Ожидание загрузки Synology NAS""" + logger.info(f"Waiting for Synology NAS to boot (timeout: {max_attempts * delay}s)...") + + for attempt in range(max_attempts): + # Принудительно проверяем статус без использования кэша + if self.is_online(force_check=True): + logger.info(f"Synology NAS is online after {attempt + 1} attempts ({(attempt + 1) * delay}s)") + + # Проверяем, что не только сеть доступна, но и API загрузился + api_ready = False + logger.info("Waiting for API services to initialize...") + + for api_check in range(5): # Даем еще до 50 секунд для загрузки API + if self.sid or self.login(): + api_ready = True + logger.info(f"API services are ready after {api_check + 1} attempts") + break + logger.debug(f"API not ready yet, waiting... ({api_check + 1}/5)") + sleep(10) + + if not api_ready: + logger.warning("System is online but API services may not be fully initialized") + + # Дадим дополнительное время для полной загрузки всех сервисов + sleep(delay) + return True + + sleep(delay) + logger.info(f"Waiting for boot... Attempt {attempt + 1}/{max_attempts}") + + logger.error(f"Synology NAS didn't come online after {max_attempts * delay} seconds") + return False + + def power_on(self) -> bool: + """Включение Synology NAS""" + # Принудительная проверка статуса + if self.is_online(force_check=True): + logger.info("Synology NAS is already online") + return True + + logger.info("Powering on Synology NAS via Wake-on-LAN...") + + # Проверяем, настроен ли MAC-адрес + if not SYNOLOGY_MAC: + logger.error("Cannot power on: MAC address not configured in settings") + return False + + # Пробуем отправить несколько WoL пакетов для надежности + success = False + for attempt in range(3): + logger.debug(f"Sending WoL packet, attempt {attempt + 1}/3") + if self.wake_on_lan(): + success = True + break + sleep(1) + + if not success: + logger.error("Failed to send Wake-on-LAN packets") + return False + + # Ожидание загрузки + logger.info("WoL packets sent successfully, waiting for system to boot...") + boot_result = self.wait_for_boot(max_attempts=30, delay=10) + + if boot_result: + # Проверяем доступность API после загрузки + system_status = self.get_system_status() + if system_status.get("status") == "online": + logger.info("System booted successfully with API access") + return True + else: + logger.warning("System appears to be online but API may not be fully ready") + return True + else: + logger.error("System did not come online after WoL") + return False + + def power_off(self) -> bool: + """Выключение Synology NAS""" + if not self.is_online(force_check=True): + logger.info("Synology NAS is already offline") + return True + + logger.info("Powering off Synology NAS...") + result = self.shutdown_system() + + if result: + # Дополнительная проверка, что система действительно выключилась + logger.info("Verifying system is offline...") + for attempt in range(12): # Проверяем в течение 2 минут (12 * 10 сек) + if not self.is_online(force_check=True): + logger.info(f"System confirmed offline after {attempt * 10} seconds") + return True + logger.debug(f"System still shutting down, checking again... ({attempt + 1}/12)") + sleep(10) + + logger.warning("System still appears to be online after shutdown command") + return False + else: + logger.error("Failed to initiate shutdown") + return False + + # Заглушки для расширенных методов + def get_shared_folders(self) -> List[Dict[str, Any]]: + """Получение списка общих папок (заглушка)""" + logger.warning("Function get_shared_folders() is not implemented yet") + return [] + + def get_system_load(self) -> Dict[str, Any]: + """Получение информации о загрузке системы (заглушка)""" + logger.warning("Function get_system_load() is not implemented yet") + return {} + + def get_storage_status(self) -> Dict[str, Any]: + """Получение подробной информации о хранилище (заглушка)""" + logger.warning("Function get_storage_status() is not implemented yet") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for storage status request") + return {"error": "authentication_failed"} + + try: + # Здесь будет реализация после добавления соответствующего API + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.Storage.CGI.Storage", + "version": "1", + "method": "load_info", + "sid": self.sid + } + + # В текущей версии просто возвращаем заглушку + return { + "volumes": [], + "disks": [], + "total_size": 0, + "total_used": 0, + "error": "not_implemented" + } + except Exception as e: + logger.error(f"Error in get_storage_status: {str(e)}") + return {"error": str(e)} + + def get_security_status(self) -> Dict[str, Any]: + """Получение информации о состоянии безопасности (заглушка)""" + logger.warning("Function get_security_status() is not implemented yet") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for security status request") + return {"success": False, "error": "authentication_failed"} + + try: + # Здесь будет реализация после добавления соответствующего API + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.Core.SecurityScan.Status", + "version": "1", + "method": "get", + "sid": self.sid + } + + # В текущей версии просто возвращаем заглушку + return { + "success": False, + "status": "not_implemented", + "last_check": None, + "is_secure": False, + "error": "not_implemented" + } + except Exception as e: + logger.error(f"Error in get_security_status: {str(e)}") + return {"success": False, "error": str(e)} diff --git a/.history/src/api/synology_20250830075503.py b/.history/src/api/synology_20250830075503.py new file mode 100644 index 0000000..5e2e14f --- /dev/null +++ b/.history/src/api/synology_20250830075503.py @@ -0,0 +1,770 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Модуль для взаимодействия с API Synology NAS +""" + +import requests +from requests.adapters import HTTPAdapter +import json +import logging +import time +import urllib3 +from urllib3.util import Retry +from typing import Dict, Any, Optional, List +import socket +import struct +from time import sleep + +from src.config.config import ( + SYNOLOGY_HOST, + SYNOLOGY_PORT, + SYNOLOGY_USERNAME, + SYNOLOGY_PASSWORD, + SYNOLOGY_SECURE, + SYNOLOGY_TIMEOUT, + SYNOLOGY_MAC, + WOL_PORT +) + +# Отключение предупреждений о небезопасных SSL-соединениях +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + +logger = logging.getLogger(__name__) + +class SynologyAPI: + """Класс для взаимодействия с API Synology NAS""" + + def __init__(self): + """Инициализация класса SynologyAPI""" + logger.info("Creating API with auto-retry and connection pool") + logger.debug(f"Connection details: Host={SYNOLOGY_HOST}, Port={SYNOLOGY_PORT}, Secure={SYNOLOGY_SECURE}") + + self.protocol = "https" if SYNOLOGY_SECURE else "http" + self.base_url = f"{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi" + self.sid = None + self.session = requests.Session() + + # Настройка SSL + if self.protocol == "https": + logger.debug("SSL enabled, disabling certificate verification for internal network") + self.session.verify = False # Отключаем проверку SSL для внутренней сети + + # Добавляем повторные попытки для HTTP-запросов + retry_strategy = Retry( + total=5, # Увеличиваем количество попыток + status_forcelist=[429, 500, 502, 503, 504, 404], + allowed_methods=["GET", "POST"], + backoff_factor=1.5, # Увеличиваем задержку между попытками + respect_retry_after_header=True + ) + adapter = HTTPAdapter( + max_retries=retry_strategy, + pool_connections=3, + pool_maxsize=10 + ) + self.session.mount("http://", adapter) + self.session.mount("https://", adapter) + + # Таймауты будут указаны в запросах + self.default_timeout = (SYNOLOGY_TIMEOUT, SYNOLOGY_TIMEOUT*2) + logger.debug(f"Setting default request timeout: {self.default_timeout}") + + # Кэш для хранения результатов запросов + self._cache = {} + self._cache_ttl = {} + self._last_online_check = 0 + self._last_online_status = False + self._online_check_interval = 30 # Интервал для кэширования проверок онлайн-статуса + + # Время последней успешной аутентификации и срок действия сессии + self._last_auth_time = 0 + self._auth_expiry = 3600 # По умолчанию 1 час + + def login(self) -> bool: + """Авторизация в API Synology NAS""" + # Проверяем, действительна ли текущая сессия + current_time = time.time() + if self.sid and (current_time - self._last_auth_time) < self._auth_expiry: + logger.debug("Using existing session, still valid") + return True + + logger.info("Attempting to authenticate with Synology NAS...") + logger.debug(f"Base URL: {self.base_url}") + + # Проверяем доступность NAS перед запросом с помощью прямой TCP-проверки + # Избегаем вызова is_online(), чтобы не создавать рекурсию + online_status = self._check_tcp_connection() + if not online_status: + logger.error("Cannot login: Synology NAS is not reachable") + return False + + try: + url = f"{self.base_url}/auth.cgi" + params = { + "api": "SYNO.API.Auth", + "version": "3", + "method": "login", + "account": SYNOLOGY_USERNAME, + "passwd": SYNOLOGY_PASSWORD, + "session": "SynologyPowerControlBot", + "format": "cookie" + } + + logger.debug(f"Sending auth request to {url}") + start_time = time.time() + response = self.session.get( + url, + params=params, + timeout=self.default_timeout, + verify=False + ) + elapsed_time = time.time() - start_time + logger.debug(f"Auth request completed in {elapsed_time:.2f}s with status code {response.status_code}") + + # Проверка статуса HTTP + if response.status_code != 200: + logger.error(f"HTTP error: {response.status_code}") + logger.debug(f"Response content: {response.text[:200]}") + return False + + try: + data = response.json() + except json.JSONDecodeError: + logger.error("Failed to decode JSON response") + logger.debug(f"Response content: {response.text[:200]}") + return False + + if data.get("success"): + self.sid = data.get("data", {}).get("sid") + self._last_auth_time = current_time + logger.info("Successfully logged in to Synology NAS") + logger.debug(f"Session ID obtained: {self.sid[:5] if self.sid else 'None'}...") # Показываем только начало SID для безопасности + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to log in to Synology NAS: Error code {error_code}") + + # Дополнительная диагностика + if error_code == 400: + logger.error("Authentication error: Invalid credentials") + elif error_code == 401: + logger.error("Authentication error: Account disabled") + elif error_code == 402: + logger.error("Authentication error: Permission denied") + elif error_code == 403: + logger.error("Authentication error: 2-factor authentication required") + elif error_code == 404: + logger.error("Authentication error: Failed to authenticate with 2-factor authentication") + + return False + + except requests.exceptions.Timeout: + logger.error(f"Connection timeout after {SYNOLOGY_TIMEOUT} seconds") + return False + except requests.exceptions.ConnectionError as e: + logger.error(f"Connection error: {str(e)}") + logger.debug(f"Connection details: {self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}") + return False + except requests.RequestException as e: + logger.error(f"Request error: {str(e)}") + return False + except Exception as e: + logger.error(f"Unexpected error during login: {str(e)}", exc_info=True) + return False + + def logout(self) -> bool: + """Выход из API Synology NAS""" + if not self.sid: + return True + + try: + url = f"{self.base_url}/auth.cgi" + params = { + "api": "SYNO.API.Auth", + "version": "1", + "method": "logout", + "session": "SynologyPowerControlBot", + "_sid": self.sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + self.sid = None + logger.info("Successfully logged out from Synology NAS") + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to log out from Synology NAS: Error code {error_code}") + return False + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + return False + + def _make_api_request(self, api_name: str, method: str, version: int = 1, + params: Optional[Dict[str, Any]] = None, retry_auth: bool = True) -> Optional[Dict[str, Any]]: + """Обобщенный метод для выполнения API запросов с обработкой ошибок""" + # Проверка наличия авторизации + if not self.sid and not self.login(): + logger.error(f"Not authenticated for API request: {api_name}.{method}") + return None + + # Подготовка базовых параметров запроса + base_params = { + "api": api_name, + "version": str(version), + "method": method, + "sid": self.sid + } + + # Добавление дополнительных параметров, если они заданы + if params: + base_params.update(params) + + url = f"{self.base_url}/entry.cgi" + logger.debug(f"API request: {api_name}.{method} v{version}") + + try: + start_time = time.time() + response = self.session.get( + url, + params=base_params, + timeout=self.default_timeout, + verify=False + ) + elapsed_time = time.time() - start_time + logger.debug(f"API request completed in {elapsed_time:.2f}s with status {response.status_code}") + + # Проверка статуса HTTP + if response.status_code != 200: + logger.error(f"HTTP error: {response.status_code} for {api_name}.{method}") + return None + + try: + data = response.json() + except json.JSONDecodeError: + logger.error(f"Failed to decode JSON response for {api_name}.{method}") + logger.debug(f"Response content: {response.text[:200]}") + return None + + if data.get("success"): + logger.info(f"API request successful for {method}") + return data.get("data", {}) + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"API error for {api_name}.{method}: {error_code}") + + # Ошибки доступа или прав часто встречаются, но они не критичные + # Например, ошибка 102 означает, что нет прав, но NAS доступен + if error_code in [102, 103, 104, 105]: + logger.warning(f"Permission-related error for {api_name}.{method}: {error_code}") + # Возвращаем пустой словарь вместо None, + # чтобы вызывающий код мог понять, что запрос выполнен + return {} + + # Если ошибка связана с авторизацией и нам разрешено повторить попытку + if error_code in [106, 107, 119] and retry_auth: + logger.info("Session may have expired, re-authenticating...") + self.sid = None # Сбрасываем SID + if self.login(): + logger.info("Re-authenticated, retrying API request...") + # Рекурсивный вызов, но без повторной авторизации во избежание зацикливания + return self._make_api_request(api_name, method, version, params, False) + + return None + + except requests.exceptions.Timeout: + logger.error(f"Request timeout for {api_name}.{method}") + return None + except requests.exceptions.ConnectionError as e: + logger.error(f"Connection error for {api_name}.{method}: {str(e)}") + return None + except Exception as e: + logger.error(f"Unexpected error during {api_name}.{method}: {str(e)}") + return None + + def get_system_status(self) -> Dict[str, Any]: + """Получение статуса системы""" + # Проверяем доступность системы + if not self.is_online(): + logger.info("Device is offline, skipping API request") + return {"status": "offline"} + + # Проверяем, есть ли кэшированный результат + cache_key = "system_status" + current_time = time.time() + if cache_key in self._cache and current_time - self._cache_ttl.get(cache_key, 0) < 60: + logger.debug("Using cached system status") + return self._cache[cache_key] + + # Делаем API запрос + result = self._make_api_request("SYNO.DSM.Info", "getinfo") + + if result: + # Формируем расширенный ответ с дополнительной информацией + system_info = { + "status": "online", + "hostname": result.get("hostname", "unknown"), + "model": result.get("model", "unknown"), + "version": result.get("version", "unknown"), + "uptime": result.get("uptime", 0), + "time": current_time, + "is_online": True + } + + # Сохраняем в кэше + self._cache[cache_key] = system_info + self._cache_ttl[cache_key] = current_time + + return system_info + else: + # Если запрос не удался, но система онлайн + return { + "status": "error", + "error": "Failed to fetch system information", + "is_online": True, + "time": current_time + } + + def shutdown_system(self) -> bool: + """Выключение системы""" + # Проверяем, включено ли устройство перед попыткой его выключить + if not self.is_online(force_check=True): + logger.info("Synology NAS is already offline, no need to shut down") + return True + + logger.info("Attempting to shutdown Synology NAS...") + + # Пробуем DSM.System API (первый метод) + result = self._make_api_request("SYNO.DSM.System", "shutdown") + if result is not None: + logger.info("Successfully initiated system shutdown using SYNO.DSM.System") + + # Даем системе время начать процесс выключения + logger.info("Waiting for shutdown to initialize...") + sleep(5) + + # Проверяем статус + if not self.is_online(force_check=True): + logger.info("System is now offline. Shutdown confirmed successful.") + return True + else: + logger.info("System still appears to be online, but shutdown may be in progress.") + return True + + # Пробуем альтернативный Core.System API (второй метод) + logger.info("First shutdown method failed, trying alternative API...") + result = self._make_api_request("SYNO.Core.System", "shutdown", version=3) + if result is not None: + logger.info("Successfully initiated system shutdown using SYNO.Core.System") + + # Даем системе время начать процесс выключения + logger.info("Waiting for shutdown to initialize...") + sleep(5) + + # Проверяем статус + if not self.is_online(force_check=True): + logger.info("System is now offline. Shutdown confirmed successful.") + return True + else: + logger.info("System still appears to be online, but shutdown may be in progress.") + return True + + # Если оба метода не сработали, но система стала недоступна + if not self.is_online(force_check=True): + logger.info("System appears to be shutting down despite API errors") + return True + + logger.error("Failed to shutdown system after trying multiple APIs") + return False + + def reboot_system(self) -> bool: + """Перезагрузка системы""" + # Проверяем, включена ли система + if not self.is_online(force_check=True): + logger.error("Cannot reboot: System is offline") + return False + + logger.info("Attempting to reboot Synology NAS...") + + # Пробуем DSM.System API (первый метод) + result = self._make_api_request("SYNO.DSM.System", "reboot") + if result is not None: + logger.info("Successfully initiated system reboot using SYNO.DSM.System") + + # Даем системе время начать процесс перезагрузки + logger.info("Waiting for reboot to initialize...") + sleep(5) + + # Ждем, пока система станет недоступна (признак перезагрузки) + reboot_started = False + for i in range(12): # Проверяем в течение 60 секунд (12 * 5сек) + if not self.is_online(force_check=True): + logger.info(f"System went offline after {i*5} seconds, reboot in progress") + reboot_started = True + break + logger.debug(f"System still online, waiting... ({i+1}/12)") + sleep(5) + + if reboot_started: + # Ждем, пока система снова станет доступна + logger.info("Waiting for system to come back online...") + return self.wait_for_boot(max_attempts=30, delay=10) + else: + logger.warning("System did not go offline after reboot command") + return False + + return True + + # Пробуем альтернативный Core.System API (второй метод) + logger.info("First reboot method failed, trying alternative API...") + result = self._make_api_request("SYNO.Core.System", "reboot", version=3) + if result is not None: + logger.info("Successfully initiated system reboot using SYNO.Core.System") + + # Даем системе время начать процесс перезагрузки + logger.info("Waiting for reboot to initialize...") + sleep(5) + + # Ждем, пока система станет недоступна (признак перезагрузки) + reboot_started = False + for i in range(12): # Проверяем в течение 60 секунд (12 * 5сек) + if not self.is_online(force_check=True): + logger.info(f"System went offline after {i*5} seconds, reboot in progress") + reboot_started = True + break + logger.debug(f"System still online, waiting... ({i+1}/12)") + sleep(5) + + if reboot_started: + # Ждем, пока система снова станет доступна + logger.info("Waiting for system to come back online...") + return self.wait_for_boot(max_attempts=30, delay=10) + else: + logger.warning("System did not go offline after reboot command") + return False + + logger.error("Failed to reboot system after trying multiple APIs") + return False + + def _get_error_description(self, error_code: int) -> str: + """Получение описания ошибки по коду""" + error_descriptions = { + 100: "Unknown error", + 101: "Invalid parameter", + 102: "API does not exist", + 103: "Method does not exist", + 104: "Version does not support", + 105: "Permission denied", + 106: "Session timeout", + 107: "Session interrupted by duplicate login", + 400: "Invalid credentials", + 401: "Account disabled", + 402: "Permission denied", + 403: "2FA required", + 404: "Failed to authenticate with 2FA" + } + return error_descriptions.get(error_code, "Unknown error code") + + def _check_tcp_connection(self) -> bool: + """Проверка базового TCP-соединения с Synology NAS""" + try: + socket_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + socket_obj.settimeout(SYNOLOGY_TIMEOUT) + result = socket_obj.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT)) + socket_obj.close() + + return result == 0 + except socket.error as e: + logger.error(f"Socket error during connection check: {str(e)}") + return False + except Exception as e: + logger.error(f"Unexpected error during connection check: {str(e)}") + return False + + def is_online(self, force_check=False) -> bool: + """Проверка онлайн-статуса Synology NAS""" + # Используем кэшированное значение, если доступно и не устарело + current_time = time.time() + if not force_check and (current_time - self._last_online_check) < self._online_check_interval: + logger.debug(f"Using cached online status: {self._last_online_status}") + return self._last_online_status + + logger.info("Checking if NAS is online...") + + # Проверяем TCP-соединение + online_status = self._check_tcp_connection() + logger.info(f"Detected Synology NAS online status: {online_status}") + + # Если TCP-соединение успешно и у нас есть действующий SID, + # попробуем более детальную проверку через API + if online_status and self.sid: + logger.info("Trying to fetch more detailed online status through API...") + try: + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.DSM.Info", + "version": "1", + "method": "getinfo", + "sid": self.sid + } + + response = self.session.get( + url, + params=params, + timeout=self.default_timeout, + verify=False + ) + + if response.status_code == 200: + data = response.json() + if data.get("success"): + logger.info("API request successful for getinfo") + logger.info("Synology NAS is online with API access") + else: + logger.warning("API response indicates an error, but NAS is reachable") + logger.debug(f"API error: {data.get('error', {}).get('code', -1)}") + else: + logger.warning(f"API returned status code {response.status_code}") + except Exception as e: + logger.warning(f"API check failed, but TCP connection succeeded: {str(e)}") + + # Обновляем кэшированное значение + self._last_online_check = current_time + self._last_online_status = online_status + + return online_status + + def wake_on_lan(self) -> bool: + """Отправка Wake-on-LAN пакета для включения Synology NAS""" + if not SYNOLOGY_MAC: + logger.error("MAC address not configured") + return False + + try: + # Преобразование MAC-адреса в байты + mac_address = SYNOLOGY_MAC.replace(':', '').replace('-', '') + if len(mac_address) != 12: + logger.error(f"Invalid MAC address format: {SYNOLOGY_MAC}") + return False + + try: + mac_bytes = bytes.fromhex(mac_address) + except ValueError as e: + logger.error(f"Failed to parse MAC address: {str(e)}") + logger.error(f"MAC address must contain only hex characters: {SYNOLOGY_MAC}") + return False + + # Создание Magic Packet + magic_packet = b'\xff' * 6 + mac_bytes * 16 + + # Отправка пакета на конкретный адрес + try: + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) + sock.sendto(magic_packet, (SYNOLOGY_HOST, WOL_PORT)) + sock.close() + logger.info(f"Wake-on-LAN packet sent to {SYNOLOGY_MAC} at {SYNOLOGY_HOST}:{WOL_PORT}") + except Exception as e: + logger.error(f"Error sending directed WoL packet: {str(e)}") + return False + + # Для надежности отправляем также широковещательный пакет + try: + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) + + # Используем стандартный широковещательный адрес + broadcast_addr = "255.255.255.255" + sock.sendto(magic_packet, (broadcast_addr, WOL_PORT)) + sock.close() + logger.info(f"Wake-on-LAN broadcast packet sent to {broadcast_addr}:{WOL_PORT}") + except Exception as e: + logger.warning(f"Error sending broadcast WoL packet (non-critical): {str(e)}") + # Не считаем ошибкой, т.к. основной пакет уже отправлен + + return True + + except Exception as e: + logger.error(f"Unexpected error in wake_on_lan: {str(e)}") + return False + + def wait_for_boot(self, max_attempts: int = 30, delay: int = 10) -> bool: + """Ожидание загрузки Synology NAS""" + logger.info(f"Waiting for Synology NAS to boot (timeout: {max_attempts * delay}s)...") + + for attempt in range(max_attempts): + # Принудительно проверяем статус без использования кэша + if self.is_online(force_check=True): + logger.info(f"Synology NAS is online after {attempt + 1} attempts ({(attempt + 1) * delay}s)") + + # Проверяем, что не только сеть доступна, но и API загрузился + api_ready = False + logger.info("Waiting for API services to initialize...") + + for api_check in range(5): # Даем еще до 50 секунд для загрузки API + if self.sid or self.login(): + api_ready = True + logger.info(f"API services are ready after {api_check + 1} attempts") + break + logger.debug(f"API not ready yet, waiting... ({api_check + 1}/5)") + sleep(10) + + if not api_ready: + logger.warning("System is online but API services may not be fully initialized") + + # Дадим дополнительное время для полной загрузки всех сервисов + sleep(delay) + return True + + sleep(delay) + logger.info(f"Waiting for boot... Attempt {attempt + 1}/{max_attempts}") + + logger.error(f"Synology NAS didn't come online after {max_attempts * delay} seconds") + return False + + def power_on(self) -> bool: + """Включение Synology NAS""" + # Принудительная проверка статуса + if self.is_online(force_check=True): + logger.info("Synology NAS is already online") + return True + + logger.info("Powering on Synology NAS via Wake-on-LAN...") + + # Проверяем, настроен ли MAC-адрес + if not SYNOLOGY_MAC: + logger.error("Cannot power on: MAC address not configured in settings") + return False + + # Пробуем отправить несколько WoL пакетов для надежности + success = False + for attempt in range(3): + logger.debug(f"Sending WoL packet, attempt {attempt + 1}/3") + if self.wake_on_lan(): + success = True + break + sleep(1) + + if not success: + logger.error("Failed to send Wake-on-LAN packets") + return False + + # Ожидание загрузки + logger.info("WoL packets sent successfully, waiting for system to boot...") + boot_result = self.wait_for_boot(max_attempts=30, delay=10) + + if boot_result: + # Проверяем доступность API после загрузки + system_status = self.get_system_status() + if system_status.get("status") == "online": + logger.info("System booted successfully with API access") + return True + else: + logger.warning("System appears to be online but API may not be fully ready") + return True + else: + logger.error("System did not come online after WoL") + return False + + def power_off(self) -> bool: + """Выключение Synology NAS""" + if not self.is_online(force_check=True): + logger.info("Synology NAS is already offline") + return True + + logger.info("Powering off Synology NAS...") + result = self.shutdown_system() + + if result: + # Дополнительная проверка, что система действительно выключилась + logger.info("Verifying system is offline...") + for attempt in range(12): # Проверяем в течение 2 минут (12 * 10 сек) + if not self.is_online(force_check=True): + logger.info(f"System confirmed offline after {attempt * 10} seconds") + return True + logger.debug(f"System still shutting down, checking again... ({attempt + 1}/12)") + sleep(10) + + logger.warning("System still appears to be online after shutdown command") + return False + else: + logger.error("Failed to initiate shutdown") + return False + + # Заглушки для расширенных методов + def get_shared_folders(self) -> List[Dict[str, Any]]: + """Получение списка общих папок (заглушка)""" + logger.warning("Function get_shared_folders() is not implemented yet") + return [] + + def get_system_load(self) -> Dict[str, Any]: + """Получение информации о загрузке системы (заглушка)""" + logger.warning("Function get_system_load() is not implemented yet") + return {} + + def get_storage_status(self) -> Dict[str, Any]: + """Получение подробной информации о хранилище (заглушка)""" + logger.warning("Function get_storage_status() is not implemented yet") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for storage status request") + return {"error": "authentication_failed"} + + try: + # Здесь будет реализация после добавления соответствующего API + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.Storage.CGI.Storage", + "version": "1", + "method": "load_info", + "sid": self.sid + } + + # В текущей версии просто возвращаем заглушку + return { + "volumes": [], + "disks": [], + "total_size": 0, + "total_used": 0, + "error": "not_implemented" + } + except Exception as e: + logger.error(f"Error in get_storage_status: {str(e)}") + return {"error": str(e)} + + def get_security_status(self) -> Dict[str, Any]: + """Получение информации о состоянии безопасности (заглушка)""" + logger.warning("Function get_security_status() is not implemented yet") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for security status request") + return {"success": False, "error": "authentication_failed"} + + try: + # Здесь будет реализация после добавления соответствующего API + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.Core.SecurityScan.Status", + "version": "1", + "method": "get", + "sid": self.sid + } + + # В текущей версии просто возвращаем заглушку + return { + "success": False, + "status": "not_implemented", + "last_check": None, + "is_secure": False, + "error": "not_implemented" + } + except Exception as e: + logger.error(f"Error in get_security_status: {str(e)}") + return {"success": False, "error": str(e)} diff --git a/.history/src/api/synology_20250830075522.py b/.history/src/api/synology_20250830075522.py new file mode 100644 index 0000000..5e2e14f --- /dev/null +++ b/.history/src/api/synology_20250830075522.py @@ -0,0 +1,770 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Модуль для взаимодействия с API Synology NAS +""" + +import requests +from requests.adapters import HTTPAdapter +import json +import logging +import time +import urllib3 +from urllib3.util import Retry +from typing import Dict, Any, Optional, List +import socket +import struct +from time import sleep + +from src.config.config import ( + SYNOLOGY_HOST, + SYNOLOGY_PORT, + SYNOLOGY_USERNAME, + SYNOLOGY_PASSWORD, + SYNOLOGY_SECURE, + SYNOLOGY_TIMEOUT, + SYNOLOGY_MAC, + WOL_PORT +) + +# Отключение предупреждений о небезопасных SSL-соединениях +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + +logger = logging.getLogger(__name__) + +class SynologyAPI: + """Класс для взаимодействия с API Synology NAS""" + + def __init__(self): + """Инициализация класса SynologyAPI""" + logger.info("Creating API with auto-retry and connection pool") + logger.debug(f"Connection details: Host={SYNOLOGY_HOST}, Port={SYNOLOGY_PORT}, Secure={SYNOLOGY_SECURE}") + + self.protocol = "https" if SYNOLOGY_SECURE else "http" + self.base_url = f"{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi" + self.sid = None + self.session = requests.Session() + + # Настройка SSL + if self.protocol == "https": + logger.debug("SSL enabled, disabling certificate verification for internal network") + self.session.verify = False # Отключаем проверку SSL для внутренней сети + + # Добавляем повторные попытки для HTTP-запросов + retry_strategy = Retry( + total=5, # Увеличиваем количество попыток + status_forcelist=[429, 500, 502, 503, 504, 404], + allowed_methods=["GET", "POST"], + backoff_factor=1.5, # Увеличиваем задержку между попытками + respect_retry_after_header=True + ) + adapter = HTTPAdapter( + max_retries=retry_strategy, + pool_connections=3, + pool_maxsize=10 + ) + self.session.mount("http://", adapter) + self.session.mount("https://", adapter) + + # Таймауты будут указаны в запросах + self.default_timeout = (SYNOLOGY_TIMEOUT, SYNOLOGY_TIMEOUT*2) + logger.debug(f"Setting default request timeout: {self.default_timeout}") + + # Кэш для хранения результатов запросов + self._cache = {} + self._cache_ttl = {} + self._last_online_check = 0 + self._last_online_status = False + self._online_check_interval = 30 # Интервал для кэширования проверок онлайн-статуса + + # Время последней успешной аутентификации и срок действия сессии + self._last_auth_time = 0 + self._auth_expiry = 3600 # По умолчанию 1 час + + def login(self) -> bool: + """Авторизация в API Synology NAS""" + # Проверяем, действительна ли текущая сессия + current_time = time.time() + if self.sid and (current_time - self._last_auth_time) < self._auth_expiry: + logger.debug("Using existing session, still valid") + return True + + logger.info("Attempting to authenticate with Synology NAS...") + logger.debug(f"Base URL: {self.base_url}") + + # Проверяем доступность NAS перед запросом с помощью прямой TCP-проверки + # Избегаем вызова is_online(), чтобы не создавать рекурсию + online_status = self._check_tcp_connection() + if not online_status: + logger.error("Cannot login: Synology NAS is not reachable") + return False + + try: + url = f"{self.base_url}/auth.cgi" + params = { + "api": "SYNO.API.Auth", + "version": "3", + "method": "login", + "account": SYNOLOGY_USERNAME, + "passwd": SYNOLOGY_PASSWORD, + "session": "SynologyPowerControlBot", + "format": "cookie" + } + + logger.debug(f"Sending auth request to {url}") + start_time = time.time() + response = self.session.get( + url, + params=params, + timeout=self.default_timeout, + verify=False + ) + elapsed_time = time.time() - start_time + logger.debug(f"Auth request completed in {elapsed_time:.2f}s with status code {response.status_code}") + + # Проверка статуса HTTP + if response.status_code != 200: + logger.error(f"HTTP error: {response.status_code}") + logger.debug(f"Response content: {response.text[:200]}") + return False + + try: + data = response.json() + except json.JSONDecodeError: + logger.error("Failed to decode JSON response") + logger.debug(f"Response content: {response.text[:200]}") + return False + + if data.get("success"): + self.sid = data.get("data", {}).get("sid") + self._last_auth_time = current_time + logger.info("Successfully logged in to Synology NAS") + logger.debug(f"Session ID obtained: {self.sid[:5] if self.sid else 'None'}...") # Показываем только начало SID для безопасности + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to log in to Synology NAS: Error code {error_code}") + + # Дополнительная диагностика + if error_code == 400: + logger.error("Authentication error: Invalid credentials") + elif error_code == 401: + logger.error("Authentication error: Account disabled") + elif error_code == 402: + logger.error("Authentication error: Permission denied") + elif error_code == 403: + logger.error("Authentication error: 2-factor authentication required") + elif error_code == 404: + logger.error("Authentication error: Failed to authenticate with 2-factor authentication") + + return False + + except requests.exceptions.Timeout: + logger.error(f"Connection timeout after {SYNOLOGY_TIMEOUT} seconds") + return False + except requests.exceptions.ConnectionError as e: + logger.error(f"Connection error: {str(e)}") + logger.debug(f"Connection details: {self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}") + return False + except requests.RequestException as e: + logger.error(f"Request error: {str(e)}") + return False + except Exception as e: + logger.error(f"Unexpected error during login: {str(e)}", exc_info=True) + return False + + def logout(self) -> bool: + """Выход из API Synology NAS""" + if not self.sid: + return True + + try: + url = f"{self.base_url}/auth.cgi" + params = { + "api": "SYNO.API.Auth", + "version": "1", + "method": "logout", + "session": "SynologyPowerControlBot", + "_sid": self.sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + self.sid = None + logger.info("Successfully logged out from Synology NAS") + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to log out from Synology NAS: Error code {error_code}") + return False + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + return False + + def _make_api_request(self, api_name: str, method: str, version: int = 1, + params: Optional[Dict[str, Any]] = None, retry_auth: bool = True) -> Optional[Dict[str, Any]]: + """Обобщенный метод для выполнения API запросов с обработкой ошибок""" + # Проверка наличия авторизации + if not self.sid and not self.login(): + logger.error(f"Not authenticated for API request: {api_name}.{method}") + return None + + # Подготовка базовых параметров запроса + base_params = { + "api": api_name, + "version": str(version), + "method": method, + "sid": self.sid + } + + # Добавление дополнительных параметров, если они заданы + if params: + base_params.update(params) + + url = f"{self.base_url}/entry.cgi" + logger.debug(f"API request: {api_name}.{method} v{version}") + + try: + start_time = time.time() + response = self.session.get( + url, + params=base_params, + timeout=self.default_timeout, + verify=False + ) + elapsed_time = time.time() - start_time + logger.debug(f"API request completed in {elapsed_time:.2f}s with status {response.status_code}") + + # Проверка статуса HTTP + if response.status_code != 200: + logger.error(f"HTTP error: {response.status_code} for {api_name}.{method}") + return None + + try: + data = response.json() + except json.JSONDecodeError: + logger.error(f"Failed to decode JSON response for {api_name}.{method}") + logger.debug(f"Response content: {response.text[:200]}") + return None + + if data.get("success"): + logger.info(f"API request successful for {method}") + return data.get("data", {}) + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"API error for {api_name}.{method}: {error_code}") + + # Ошибки доступа или прав часто встречаются, но они не критичные + # Например, ошибка 102 означает, что нет прав, но NAS доступен + if error_code in [102, 103, 104, 105]: + logger.warning(f"Permission-related error for {api_name}.{method}: {error_code}") + # Возвращаем пустой словарь вместо None, + # чтобы вызывающий код мог понять, что запрос выполнен + return {} + + # Если ошибка связана с авторизацией и нам разрешено повторить попытку + if error_code in [106, 107, 119] and retry_auth: + logger.info("Session may have expired, re-authenticating...") + self.sid = None # Сбрасываем SID + if self.login(): + logger.info("Re-authenticated, retrying API request...") + # Рекурсивный вызов, но без повторной авторизации во избежание зацикливания + return self._make_api_request(api_name, method, version, params, False) + + return None + + except requests.exceptions.Timeout: + logger.error(f"Request timeout for {api_name}.{method}") + return None + except requests.exceptions.ConnectionError as e: + logger.error(f"Connection error for {api_name}.{method}: {str(e)}") + return None + except Exception as e: + logger.error(f"Unexpected error during {api_name}.{method}: {str(e)}") + return None + + def get_system_status(self) -> Dict[str, Any]: + """Получение статуса системы""" + # Проверяем доступность системы + if not self.is_online(): + logger.info("Device is offline, skipping API request") + return {"status": "offline"} + + # Проверяем, есть ли кэшированный результат + cache_key = "system_status" + current_time = time.time() + if cache_key in self._cache and current_time - self._cache_ttl.get(cache_key, 0) < 60: + logger.debug("Using cached system status") + return self._cache[cache_key] + + # Делаем API запрос + result = self._make_api_request("SYNO.DSM.Info", "getinfo") + + if result: + # Формируем расширенный ответ с дополнительной информацией + system_info = { + "status": "online", + "hostname": result.get("hostname", "unknown"), + "model": result.get("model", "unknown"), + "version": result.get("version", "unknown"), + "uptime": result.get("uptime", 0), + "time": current_time, + "is_online": True + } + + # Сохраняем в кэше + self._cache[cache_key] = system_info + self._cache_ttl[cache_key] = current_time + + return system_info + else: + # Если запрос не удался, но система онлайн + return { + "status": "error", + "error": "Failed to fetch system information", + "is_online": True, + "time": current_time + } + + def shutdown_system(self) -> bool: + """Выключение системы""" + # Проверяем, включено ли устройство перед попыткой его выключить + if not self.is_online(force_check=True): + logger.info("Synology NAS is already offline, no need to shut down") + return True + + logger.info("Attempting to shutdown Synology NAS...") + + # Пробуем DSM.System API (первый метод) + result = self._make_api_request("SYNO.DSM.System", "shutdown") + if result is not None: + logger.info("Successfully initiated system shutdown using SYNO.DSM.System") + + # Даем системе время начать процесс выключения + logger.info("Waiting for shutdown to initialize...") + sleep(5) + + # Проверяем статус + if not self.is_online(force_check=True): + logger.info("System is now offline. Shutdown confirmed successful.") + return True + else: + logger.info("System still appears to be online, but shutdown may be in progress.") + return True + + # Пробуем альтернативный Core.System API (второй метод) + logger.info("First shutdown method failed, trying alternative API...") + result = self._make_api_request("SYNO.Core.System", "shutdown", version=3) + if result is not None: + logger.info("Successfully initiated system shutdown using SYNO.Core.System") + + # Даем системе время начать процесс выключения + logger.info("Waiting for shutdown to initialize...") + sleep(5) + + # Проверяем статус + if not self.is_online(force_check=True): + logger.info("System is now offline. Shutdown confirmed successful.") + return True + else: + logger.info("System still appears to be online, but shutdown may be in progress.") + return True + + # Если оба метода не сработали, но система стала недоступна + if not self.is_online(force_check=True): + logger.info("System appears to be shutting down despite API errors") + return True + + logger.error("Failed to shutdown system after trying multiple APIs") + return False + + def reboot_system(self) -> bool: + """Перезагрузка системы""" + # Проверяем, включена ли система + if not self.is_online(force_check=True): + logger.error("Cannot reboot: System is offline") + return False + + logger.info("Attempting to reboot Synology NAS...") + + # Пробуем DSM.System API (первый метод) + result = self._make_api_request("SYNO.DSM.System", "reboot") + if result is not None: + logger.info("Successfully initiated system reboot using SYNO.DSM.System") + + # Даем системе время начать процесс перезагрузки + logger.info("Waiting for reboot to initialize...") + sleep(5) + + # Ждем, пока система станет недоступна (признак перезагрузки) + reboot_started = False + for i in range(12): # Проверяем в течение 60 секунд (12 * 5сек) + if not self.is_online(force_check=True): + logger.info(f"System went offline after {i*5} seconds, reboot in progress") + reboot_started = True + break + logger.debug(f"System still online, waiting... ({i+1}/12)") + sleep(5) + + if reboot_started: + # Ждем, пока система снова станет доступна + logger.info("Waiting for system to come back online...") + return self.wait_for_boot(max_attempts=30, delay=10) + else: + logger.warning("System did not go offline after reboot command") + return False + + return True + + # Пробуем альтернативный Core.System API (второй метод) + logger.info("First reboot method failed, trying alternative API...") + result = self._make_api_request("SYNO.Core.System", "reboot", version=3) + if result is not None: + logger.info("Successfully initiated system reboot using SYNO.Core.System") + + # Даем системе время начать процесс перезагрузки + logger.info("Waiting for reboot to initialize...") + sleep(5) + + # Ждем, пока система станет недоступна (признак перезагрузки) + reboot_started = False + for i in range(12): # Проверяем в течение 60 секунд (12 * 5сек) + if not self.is_online(force_check=True): + logger.info(f"System went offline after {i*5} seconds, reboot in progress") + reboot_started = True + break + logger.debug(f"System still online, waiting... ({i+1}/12)") + sleep(5) + + if reboot_started: + # Ждем, пока система снова станет доступна + logger.info("Waiting for system to come back online...") + return self.wait_for_boot(max_attempts=30, delay=10) + else: + logger.warning("System did not go offline after reboot command") + return False + + logger.error("Failed to reboot system after trying multiple APIs") + return False + + def _get_error_description(self, error_code: int) -> str: + """Получение описания ошибки по коду""" + error_descriptions = { + 100: "Unknown error", + 101: "Invalid parameter", + 102: "API does not exist", + 103: "Method does not exist", + 104: "Version does not support", + 105: "Permission denied", + 106: "Session timeout", + 107: "Session interrupted by duplicate login", + 400: "Invalid credentials", + 401: "Account disabled", + 402: "Permission denied", + 403: "2FA required", + 404: "Failed to authenticate with 2FA" + } + return error_descriptions.get(error_code, "Unknown error code") + + def _check_tcp_connection(self) -> bool: + """Проверка базового TCP-соединения с Synology NAS""" + try: + socket_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + socket_obj.settimeout(SYNOLOGY_TIMEOUT) + result = socket_obj.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT)) + socket_obj.close() + + return result == 0 + except socket.error as e: + logger.error(f"Socket error during connection check: {str(e)}") + return False + except Exception as e: + logger.error(f"Unexpected error during connection check: {str(e)}") + return False + + def is_online(self, force_check=False) -> bool: + """Проверка онлайн-статуса Synology NAS""" + # Используем кэшированное значение, если доступно и не устарело + current_time = time.time() + if not force_check and (current_time - self._last_online_check) < self._online_check_interval: + logger.debug(f"Using cached online status: {self._last_online_status}") + return self._last_online_status + + logger.info("Checking if NAS is online...") + + # Проверяем TCP-соединение + online_status = self._check_tcp_connection() + logger.info(f"Detected Synology NAS online status: {online_status}") + + # Если TCP-соединение успешно и у нас есть действующий SID, + # попробуем более детальную проверку через API + if online_status and self.sid: + logger.info("Trying to fetch more detailed online status through API...") + try: + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.DSM.Info", + "version": "1", + "method": "getinfo", + "sid": self.sid + } + + response = self.session.get( + url, + params=params, + timeout=self.default_timeout, + verify=False + ) + + if response.status_code == 200: + data = response.json() + if data.get("success"): + logger.info("API request successful for getinfo") + logger.info("Synology NAS is online with API access") + else: + logger.warning("API response indicates an error, but NAS is reachable") + logger.debug(f"API error: {data.get('error', {}).get('code', -1)}") + else: + logger.warning(f"API returned status code {response.status_code}") + except Exception as e: + logger.warning(f"API check failed, but TCP connection succeeded: {str(e)}") + + # Обновляем кэшированное значение + self._last_online_check = current_time + self._last_online_status = online_status + + return online_status + + def wake_on_lan(self) -> bool: + """Отправка Wake-on-LAN пакета для включения Synology NAS""" + if not SYNOLOGY_MAC: + logger.error("MAC address not configured") + return False + + try: + # Преобразование MAC-адреса в байты + mac_address = SYNOLOGY_MAC.replace(':', '').replace('-', '') + if len(mac_address) != 12: + logger.error(f"Invalid MAC address format: {SYNOLOGY_MAC}") + return False + + try: + mac_bytes = bytes.fromhex(mac_address) + except ValueError as e: + logger.error(f"Failed to parse MAC address: {str(e)}") + logger.error(f"MAC address must contain only hex characters: {SYNOLOGY_MAC}") + return False + + # Создание Magic Packet + magic_packet = b'\xff' * 6 + mac_bytes * 16 + + # Отправка пакета на конкретный адрес + try: + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) + sock.sendto(magic_packet, (SYNOLOGY_HOST, WOL_PORT)) + sock.close() + logger.info(f"Wake-on-LAN packet sent to {SYNOLOGY_MAC} at {SYNOLOGY_HOST}:{WOL_PORT}") + except Exception as e: + logger.error(f"Error sending directed WoL packet: {str(e)}") + return False + + # Для надежности отправляем также широковещательный пакет + try: + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) + + # Используем стандартный широковещательный адрес + broadcast_addr = "255.255.255.255" + sock.sendto(magic_packet, (broadcast_addr, WOL_PORT)) + sock.close() + logger.info(f"Wake-on-LAN broadcast packet sent to {broadcast_addr}:{WOL_PORT}") + except Exception as e: + logger.warning(f"Error sending broadcast WoL packet (non-critical): {str(e)}") + # Не считаем ошибкой, т.к. основной пакет уже отправлен + + return True + + except Exception as e: + logger.error(f"Unexpected error in wake_on_lan: {str(e)}") + return False + + def wait_for_boot(self, max_attempts: int = 30, delay: int = 10) -> bool: + """Ожидание загрузки Synology NAS""" + logger.info(f"Waiting for Synology NAS to boot (timeout: {max_attempts * delay}s)...") + + for attempt in range(max_attempts): + # Принудительно проверяем статус без использования кэша + if self.is_online(force_check=True): + logger.info(f"Synology NAS is online after {attempt + 1} attempts ({(attempt + 1) * delay}s)") + + # Проверяем, что не только сеть доступна, но и API загрузился + api_ready = False + logger.info("Waiting for API services to initialize...") + + for api_check in range(5): # Даем еще до 50 секунд для загрузки API + if self.sid or self.login(): + api_ready = True + logger.info(f"API services are ready after {api_check + 1} attempts") + break + logger.debug(f"API not ready yet, waiting... ({api_check + 1}/5)") + sleep(10) + + if not api_ready: + logger.warning("System is online but API services may not be fully initialized") + + # Дадим дополнительное время для полной загрузки всех сервисов + sleep(delay) + return True + + sleep(delay) + logger.info(f"Waiting for boot... Attempt {attempt + 1}/{max_attempts}") + + logger.error(f"Synology NAS didn't come online after {max_attempts * delay} seconds") + return False + + def power_on(self) -> bool: + """Включение Synology NAS""" + # Принудительная проверка статуса + if self.is_online(force_check=True): + logger.info("Synology NAS is already online") + return True + + logger.info("Powering on Synology NAS via Wake-on-LAN...") + + # Проверяем, настроен ли MAC-адрес + if not SYNOLOGY_MAC: + logger.error("Cannot power on: MAC address not configured in settings") + return False + + # Пробуем отправить несколько WoL пакетов для надежности + success = False + for attempt in range(3): + logger.debug(f"Sending WoL packet, attempt {attempt + 1}/3") + if self.wake_on_lan(): + success = True + break + sleep(1) + + if not success: + logger.error("Failed to send Wake-on-LAN packets") + return False + + # Ожидание загрузки + logger.info("WoL packets sent successfully, waiting for system to boot...") + boot_result = self.wait_for_boot(max_attempts=30, delay=10) + + if boot_result: + # Проверяем доступность API после загрузки + system_status = self.get_system_status() + if system_status.get("status") == "online": + logger.info("System booted successfully with API access") + return True + else: + logger.warning("System appears to be online but API may not be fully ready") + return True + else: + logger.error("System did not come online after WoL") + return False + + def power_off(self) -> bool: + """Выключение Synology NAS""" + if not self.is_online(force_check=True): + logger.info("Synology NAS is already offline") + return True + + logger.info("Powering off Synology NAS...") + result = self.shutdown_system() + + if result: + # Дополнительная проверка, что система действительно выключилась + logger.info("Verifying system is offline...") + for attempt in range(12): # Проверяем в течение 2 минут (12 * 10 сек) + if not self.is_online(force_check=True): + logger.info(f"System confirmed offline after {attempt * 10} seconds") + return True + logger.debug(f"System still shutting down, checking again... ({attempt + 1}/12)") + sleep(10) + + logger.warning("System still appears to be online after shutdown command") + return False + else: + logger.error("Failed to initiate shutdown") + return False + + # Заглушки для расширенных методов + def get_shared_folders(self) -> List[Dict[str, Any]]: + """Получение списка общих папок (заглушка)""" + logger.warning("Function get_shared_folders() is not implemented yet") + return [] + + def get_system_load(self) -> Dict[str, Any]: + """Получение информации о загрузке системы (заглушка)""" + logger.warning("Function get_system_load() is not implemented yet") + return {} + + def get_storage_status(self) -> Dict[str, Any]: + """Получение подробной информации о хранилище (заглушка)""" + logger.warning("Function get_storage_status() is not implemented yet") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for storage status request") + return {"error": "authentication_failed"} + + try: + # Здесь будет реализация после добавления соответствующего API + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.Storage.CGI.Storage", + "version": "1", + "method": "load_info", + "sid": self.sid + } + + # В текущей версии просто возвращаем заглушку + return { + "volumes": [], + "disks": [], + "total_size": 0, + "total_used": 0, + "error": "not_implemented" + } + except Exception as e: + logger.error(f"Error in get_storage_status: {str(e)}") + return {"error": str(e)} + + def get_security_status(self) -> Dict[str, Any]: + """Получение информации о состоянии безопасности (заглушка)""" + logger.warning("Function get_security_status() is not implemented yet") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for security status request") + return {"success": False, "error": "authentication_failed"} + + try: + # Здесь будет реализация после добавления соответствующего API + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.Core.SecurityScan.Status", + "version": "1", + "method": "get", + "sid": self.sid + } + + # В текущей версии просто возвращаем заглушку + return { + "success": False, + "status": "not_implemented", + "last_check": None, + "is_secure": False, + "error": "not_implemented" + } + except Exception as e: + logger.error(f"Error in get_security_status: {str(e)}") + return {"success": False, "error": str(e)} diff --git a/.history/src/api/synology_20250830080635.py b/.history/src/api/synology_20250830080635.py new file mode 100644 index 0000000..391ab86 --- /dev/null +++ b/.history/src/api/synology_20250830080635.py @@ -0,0 +1,769 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Модуль для взаимодействия с API Synology NAS +""" + +import requests +from requests.adapters import HTTPAdapter +import json +import logging +import time +import urllib3 +from urllib3.util import Retry +from typing import Dict, Any, Optional, List +import socket +import struct +from time import sleep + +from src.config.config import ( + SYNOLOGY_HOST, + SYNOLOGY_PORT, + SYNOLOGY_USERNAME, + SYNOLOGY_PASSWORD, + SYNOLOGY_SECURE, + SYNOLOGY_TIMEOUT, + SYNOLOGY_MAC, + WOL_PORT +) + +# Отключение предупреждений о небезопасных SSL-соединениях +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + +logger = logging.getLogger(__name__) + +class SynologyAPI: + """Класс для взаимодействия с API Synology NAS""" + + def __init__(self): + """Инициализация класса SynologyAPI""" + logger.info("Creating API with auto-retry and connection pool") + logger.debug(f"Connection details: Host={SYNOLOGY_HOST}, Port={SYNOLOGY_PORT}, Secure={SYNOLOGY_SECURE}") + + self.protocol = "https" if SYNOLOGY_SECURE else "http" + self.base_url = f"{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi" + self.sid = None + self.session = requests.Session() + + # Настройка SSL + if self.protocol == "https": + logger.debug("SSL enabled, disabling certificate verification for internal network") + self.session.verify = False # Отключаем проверку SSL для внутренней сети + + # Добавляем повторные попытки для HTTP-запросов + retry_strategy = Retry( + total=5, # Увеличиваем количество попыток + status_forcelist=[429, 500, 502, 503, 504, 404], + allowed_methods=["GET", "POST"], + backoff_factor=1.5, # Увеличиваем задержку между попытками + respect_retry_after_header=True + ) + adapter = HTTPAdapter( + max_retries=retry_strategy, + pool_connections=3, + pool_maxsize=10 + ) + self.session.mount("http://", adapter) + self.session.mount("https://", adapter) + + # Таймауты будут указаны в запросах + self.default_timeout = (SYNOLOGY_TIMEOUT, SYNOLOGY_TIMEOUT*2) + logger.debug(f"Setting default request timeout: {self.default_timeout}") + + # Кэш для хранения результатов запросов + self._cache = {} + self._cache_ttl = {} + self._last_online_check = 0 + self._last_online_status = False + self._online_check_interval = 30 # Интервал для кэширования проверок онлайн-статуса + + # Время последней успешной аутентификации и срок действия сессии + self._last_auth_time = 0 + self._auth_expiry = 3600 # По умолчанию 1 час + + def login(self) -> bool: + """Авторизация в API Synology NAS""" + # Проверяем, действительна ли текущая сессия + current_time = time.time() + if self.sid and (current_time - self._last_auth_time) < self._auth_expiry: + logger.debug("Using existing session, still valid") + return True + + logger.info("Attempting to authenticate with Synology NAS...") + logger.debug(f"Base URL: {self.base_url}") + + # Проверяем доступность NAS перед запросом с помощью прямой TCP-проверки + # Избегаем вызова is_online(), чтобы не создавать рекурсию + online_status = self._check_tcp_connection() + if not online_status: + logger.error("Cannot login: Synology NAS is not reachable") + return False + + try: + url = f"{self.base_url}/auth.cgi" + params = { + "api": "SYNO.API.Auth", + "version": "3", + "method": "login", + "account": SYNOLOGY_USERNAME, + "passwd": SYNOLOGY_PASSWORD, + "session": "SynologyPowerControlBot", + "format": "cookie" + } + + logger.debug(f"Sending auth request to {url}") + start_time = time.time() + response = self.session.get( + url, + params=params, + timeout=self.default_timeout, + verify=False + ) + elapsed_time = time.time() - start_time + logger.debug(f"Auth request completed in {elapsed_time:.2f}s with status code {response.status_code}") + + # Проверка статуса HTTP + if response.status_code != 200: + logger.error(f"HTTP error: {response.status_code}") + logger.debug(f"Response content: {response.text[:200]}") + return False + + try: + data = response.json() + except json.JSONDecodeError: + logger.error("Failed to decode JSON response") + logger.debug(f"Response content: {response.text[:200]}") + return False + + if data.get("success"): + self.sid = data.get("data", {}).get("sid") + self._last_auth_time = current_time + logger.info("Successfully logged in to Synology NAS") + logger.debug(f"Session ID obtained: {self.sid[:5] if self.sid else 'None'}...") # Показываем только начало SID для безопасности + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to log in to Synology NAS: Error code {error_code}") + + # Дополнительная диагностика + if error_code == 400: + logger.error("Authentication error: Invalid credentials") + elif error_code == 401: + logger.error("Authentication error: Account disabled") + elif error_code == 402: + logger.error("Authentication error: Permission denied") + elif error_code == 403: + logger.error("Authentication error: 2-factor authentication required") + elif error_code == 404: + logger.error("Authentication error: Failed to authenticate with 2-factor authentication") + + return False + + except requests.exceptions.Timeout: + logger.error(f"Connection timeout after {SYNOLOGY_TIMEOUT} seconds") + return False + except requests.exceptions.ConnectionError as e: + logger.error(f"Connection error: {str(e)}") + logger.debug(f"Connection details: {self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}") + return False + except requests.RequestException as e: + logger.error(f"Request error: {str(e)}") + return False + except Exception as e: + logger.error(f"Unexpected error during login: {str(e)}", exc_info=True) + return False + + def logout(self) -> bool: + """Выход из API Synology NAS""" + if not self.sid: + return True + + try: + url = f"{self.base_url}/auth.cgi" + params = { + "api": "SYNO.API.Auth", + "version": "1", + "method": "logout", + "session": "SynologyPowerControlBot", + "_sid": self.sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + self.sid = None + logger.info("Successfully logged out from Synology NAS") + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to log out from Synology NAS: Error code {error_code}") + return False + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + return False + + def _make_api_request(self, api_name: str, method: str, version: int = 1, + params: Optional[Dict[str, Any]] = None, retry_auth: bool = True) -> Optional[Dict[str, Any]]: + """Обобщенный метод для выполнения API запросов с обработкой ошибок""" + # Проверка наличия авторизации + if not self.sid and not self.login(): + logger.error(f"Not authenticated for API request: {api_name}.{method}") + return None + + # Подготовка базовых параметров запроса + base_params = { + "api": api_name, + "version": str(version), + "method": method, + "sid": self.sid + } + + # Добавление дополнительных параметров, если они заданы + if params: + base_params.update(params) + + url = f"{self.base_url}/entry.cgi" + logger.debug(f"API request: {api_name}.{method} v{version}") + + try: + start_time = time.time() + response = self.session.get( + url, + params=base_params, + timeout=self.default_timeout, + verify=False + ) + elapsed_time = time.time() - start_time + logger.debug(f"API request completed in {elapsed_time:.2f}s with status {response.status_code}") + + # Проверка статуса HTTP + if response.status_code != 200: + logger.error(f"HTTP error: {response.status_code} for {api_name}.{method}") + return None + + try: + data = response.json() + except json.JSONDecodeError: + logger.error(f"Failed to decode JSON response for {api_name}.{method}") + logger.debug(f"Response content: {response.text[:200]}") + return None + + if data.get("success"): + logger.info(f"API request successful for {method}") + return data.get("data", {}) + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"API error for {api_name}.{method}: {error_code}") + + # Ошибки доступа или прав часто встречаются, но они не критичные + # Например, ошибка 102 означает, что нет прав, но NAS доступен + if error_code in [102, 103, 104, 105]: + logger.warning(f"Permission-related error for {api_name}.{method}: {error_code}") + # Возвращаем пустой словарь вместо None, + # чтобы вызывающий код мог понять, что запрос выполнен + return {} + + # Если ошибка связана с авторизацией и нам разрешено повторить попытку + if error_code in [106, 107, 119] and retry_auth: + logger.info("Session may have expired, re-authenticating...") + self.sid = None # Сбрасываем SID + if self.login(): + logger.info("Re-authenticated, retrying API request...") + # Рекурсивный вызов, но без повторной авторизации во избежание зацикливания + return self._make_api_request(api_name, method, version, params, False) + + return None + + except requests.exceptions.Timeout: + logger.error(f"Request timeout for {api_name}.{method}") + return None + except requests.exceptions.ConnectionError as e: + logger.error(f"Connection error for {api_name}.{method}: {str(e)}") + return None + except Exception as e: + logger.error(f"Unexpected error during {api_name}.{method}: {str(e)}") + return None + + def get_system_status(self) -> Dict[str, Any]: + """Получение статуса системы""" + # Проверяем доступность системы + if not self.is_online(): + logger.info("Device is offline, skipping API request") + return {"status": "offline"} + + # Проверяем, есть ли кэшированный результат + cache_key = "system_status" + current_time = time.time() + if cache_key in self._cache and current_time - self._cache_ttl.get(cache_key, 0) < 60: + logger.debug("Using cached system status") + return self._cache[cache_key] + + # Делаем API запрос + result = self._make_api_request("SYNO.DSM.Info", "getinfo") + + if result: + # Формируем расширенный ответ с дополнительной информацией + system_info = { + "status": "online", + "hostname": result.get("hostname", "unknown"), + "model": result.get("model", "unknown"), + "version": result.get("version", "unknown"), + "uptime": result.get("uptime", 0), + "time": current_time, + "is_online": True + } + + # Сохраняем в кэше + self._cache[cache_key] = system_info + self._cache_ttl[cache_key] = current_time + + return system_info + else: + # Если запрос не удался, но система онлайн + return { + "status": "error", + "error": "Failed to fetch system information", + "is_online": True, + "time": current_time + } + + def shutdown_system(self) -> bool: + """Выключение системы""" + # Проверяем, включено ли устройство перед попыткой его выключить + if not self.is_online(force_check=True): + logger.info("Synology NAS is already offline, no need to shut down") + return True + + logger.info("Attempting to shutdown Synology NAS...") + + # Пробуем DSM.System API (первый метод) + result = self._make_api_request("SYNO.DSM.System", "shutdown") + if result is not None: + logger.info("Successfully initiated system shutdown using SYNO.DSM.System") + + # Даем системе время начать процесс выключения + logger.info("Waiting for shutdown to initialize...") + sleep(5) + + # Проверяем статус + if not self.is_online(force_check=True): + logger.info("System is now offline. Shutdown confirmed successful.") + return True + else: + logger.info("System still appears to be online, but shutdown may be in progress.") + return True + + # Пробуем альтернативный Core.System API (второй метод) + logger.info("First shutdown method failed, trying alternative API...") + result = self._make_api_request("SYNO.Core.System", "shutdown", version=3) + if result is not None: + logger.info("Successfully initiated system shutdown using SYNO.Core.System") + + # Даем системе время начать процесс выключения + logger.info("Waiting for shutdown to initialize...") + sleep(5) + + # Проверяем статус + if not self.is_online(force_check=True): + logger.info("System is now offline. Shutdown confirmed successful.") + return True + else: + logger.info("System still appears to be online, but shutdown may be in progress.") + return True + + # Если оба метода не сработали, но система стала недоступна + if not self.is_online(force_check=True): + logger.info("System appears to be shutting down despite API errors") + return True + + logger.error("Failed to shutdown system after trying multiple APIs") + return False + + def reboot_system(self) -> bool: + """Перезагрузка системы""" + # Проверяем, включена ли система + if not self.is_online(force_check=True): + logger.error("Cannot reboot: System is offline") + return False + + logger.info("Attempting to reboot Synology NAS...") + + # Правильный API-вызов для перезагрузки согласно официальной документации DSM API + result = self._make_api_request("SYNO.Core.System.Power", "restart", version=1) + + if result is not None: + logger.info("Successfully initiated system reboot using SYNO.Core.System.Power") + + # Даем системе время начать процесс перезагрузки + logger.info("Waiting for reboot to initialize...") + sleep(5) + + # Ждем, пока система станет недоступна (признак перезагрузки) + reboot_started = False + for i in range(12): # Проверяем в течение 60 секунд (12 * 5сек) + if not self.is_online(force_check=True): + logger.info(f"System went offline after {i*5} seconds, reboot in progress") + reboot_started = True + break + logger.debug(f"System still online, waiting... ({i+1}/12)") + sleep(5) + + if reboot_started: + # Ждем, пока система снова станет доступна + logger.info("Waiting for system to come back online...") + return self.wait_for_boot(max_attempts=30, delay=10) + else: + logger.warning("System did not go offline after reboot command") + return False + + # Запасной вариант, если первый метод не сработал + logger.info("First reboot method failed, trying alternative API...") + result = self._make_api_request("SYNO.Core.System", "reboot", version=3) + if result is not None: + logger.info("Successfully initiated system reboot using SYNO.Core.System") + + # Даем системе время начать процесс перезагрузки + logger.info("Waiting for reboot to initialize...") + sleep(5) + + # Ждем, пока система станет недоступна (признак перезагрузки) + reboot_started = False + for i in range(12): # Проверяем в течение 60 секунд (12 * 5сек) + if not self.is_online(force_check=True): + logger.info(f"System went offline after {i*5} seconds, reboot in progress") + reboot_started = True + break + logger.debug(f"System still online, waiting... ({i+1}/12)") + sleep(5) + + if reboot_started: + # Ждем, пока система снова станет доступна + logger.info("Waiting for system to come back online...") + return self.wait_for_boot(max_attempts=30, delay=10) + else: + logger.warning("System did not go offline after reboot command") + return False + + logger.error("Failed to reboot system after trying multiple APIs") + return False + + def _get_error_description(self, error_code: int) -> str: + """Получение описания ошибки по коду""" + error_descriptions = { + 100: "Unknown error", + 101: "Invalid parameter", + 102: "API does not exist", + 103: "Method does not exist", + 104: "Version does not support", + 105: "Permission denied", + 106: "Session timeout", + 107: "Session interrupted by duplicate login", + 400: "Invalid credentials", + 401: "Account disabled", + 402: "Permission denied", + 403: "2FA required", + 404: "Failed to authenticate with 2FA" + } + return error_descriptions.get(error_code, "Unknown error code") + + def _check_tcp_connection(self) -> bool: + """Проверка базового TCP-соединения с Synology NAS""" + try: + socket_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + socket_obj.settimeout(SYNOLOGY_TIMEOUT) + result = socket_obj.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT)) + socket_obj.close() + + return result == 0 + except socket.error as e: + logger.error(f"Socket error during connection check: {str(e)}") + return False + except Exception as e: + logger.error(f"Unexpected error during connection check: {str(e)}") + return False + + def is_online(self, force_check=False) -> bool: + """Проверка онлайн-статуса Synology NAS""" + # Используем кэшированное значение, если доступно и не устарело + current_time = time.time() + if not force_check and (current_time - self._last_online_check) < self._online_check_interval: + logger.debug(f"Using cached online status: {self._last_online_status}") + return self._last_online_status + + logger.info("Checking if NAS is online...") + + # Проверяем TCP-соединение + online_status = self._check_tcp_connection() + logger.info(f"Detected Synology NAS online status: {online_status}") + + # Если TCP-соединение успешно и у нас есть действующий SID, + # попробуем более детальную проверку через API + if online_status and self.sid: + logger.info("Trying to fetch more detailed online status through API...") + try: + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.DSM.Info", + "version": "1", + "method": "getinfo", + "sid": self.sid + } + + response = self.session.get( + url, + params=params, + timeout=self.default_timeout, + verify=False + ) + + if response.status_code == 200: + data = response.json() + if data.get("success"): + logger.info("API request successful for getinfo") + logger.info("Synology NAS is online with API access") + else: + logger.warning("API response indicates an error, but NAS is reachable") + logger.debug(f"API error: {data.get('error', {}).get('code', -1)}") + else: + logger.warning(f"API returned status code {response.status_code}") + except Exception as e: + logger.warning(f"API check failed, but TCP connection succeeded: {str(e)}") + + # Обновляем кэшированное значение + self._last_online_check = current_time + self._last_online_status = online_status + + return online_status + + def wake_on_lan(self) -> bool: + """Отправка Wake-on-LAN пакета для включения Synology NAS""" + if not SYNOLOGY_MAC: + logger.error("MAC address not configured") + return False + + try: + # Преобразование MAC-адреса в байты + mac_address = SYNOLOGY_MAC.replace(':', '').replace('-', '') + if len(mac_address) != 12: + logger.error(f"Invalid MAC address format: {SYNOLOGY_MAC}") + return False + + try: + mac_bytes = bytes.fromhex(mac_address) + except ValueError as e: + logger.error(f"Failed to parse MAC address: {str(e)}") + logger.error(f"MAC address must contain only hex characters: {SYNOLOGY_MAC}") + return False + + # Создание Magic Packet + magic_packet = b'\xff' * 6 + mac_bytes * 16 + + # Отправка пакета на конкретный адрес + try: + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) + sock.sendto(magic_packet, (SYNOLOGY_HOST, WOL_PORT)) + sock.close() + logger.info(f"Wake-on-LAN packet sent to {SYNOLOGY_MAC} at {SYNOLOGY_HOST}:{WOL_PORT}") + except Exception as e: + logger.error(f"Error sending directed WoL packet: {str(e)}") + return False + + # Для надежности отправляем также широковещательный пакет + try: + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) + + # Используем стандартный широковещательный адрес + broadcast_addr = "255.255.255.255" + sock.sendto(magic_packet, (broadcast_addr, WOL_PORT)) + sock.close() + logger.info(f"Wake-on-LAN broadcast packet sent to {broadcast_addr}:{WOL_PORT}") + except Exception as e: + logger.warning(f"Error sending broadcast WoL packet (non-critical): {str(e)}") + # Не считаем ошибкой, т.к. основной пакет уже отправлен + + return True + + except Exception as e: + logger.error(f"Unexpected error in wake_on_lan: {str(e)}") + return False + + def wait_for_boot(self, max_attempts: int = 30, delay: int = 10) -> bool: + """Ожидание загрузки Synology NAS""" + logger.info(f"Waiting for Synology NAS to boot (timeout: {max_attempts * delay}s)...") + + for attempt in range(max_attempts): + # Принудительно проверяем статус без использования кэша + if self.is_online(force_check=True): + logger.info(f"Synology NAS is online after {attempt + 1} attempts ({(attempt + 1) * delay}s)") + + # Проверяем, что не только сеть доступна, но и API загрузился + api_ready = False + logger.info("Waiting for API services to initialize...") + + for api_check in range(5): # Даем еще до 50 секунд для загрузки API + if self.sid or self.login(): + api_ready = True + logger.info(f"API services are ready after {api_check + 1} attempts") + break + logger.debug(f"API not ready yet, waiting... ({api_check + 1}/5)") + sleep(10) + + if not api_ready: + logger.warning("System is online but API services may not be fully initialized") + + # Дадим дополнительное время для полной загрузки всех сервисов + sleep(delay) + return True + + sleep(delay) + logger.info(f"Waiting for boot... Attempt {attempt + 1}/{max_attempts}") + + logger.error(f"Synology NAS didn't come online after {max_attempts * delay} seconds") + return False + + def power_on(self) -> bool: + """Включение Synology NAS""" + # Принудительная проверка статуса + if self.is_online(force_check=True): + logger.info("Synology NAS is already online") + return True + + logger.info("Powering on Synology NAS via Wake-on-LAN...") + + # Проверяем, настроен ли MAC-адрес + if not SYNOLOGY_MAC: + logger.error("Cannot power on: MAC address not configured in settings") + return False + + # Пробуем отправить несколько WoL пакетов для надежности + success = False + for attempt in range(3): + logger.debug(f"Sending WoL packet, attempt {attempt + 1}/3") + if self.wake_on_lan(): + success = True + break + sleep(1) + + if not success: + logger.error("Failed to send Wake-on-LAN packets") + return False + + # Ожидание загрузки + logger.info("WoL packets sent successfully, waiting for system to boot...") + boot_result = self.wait_for_boot(max_attempts=30, delay=10) + + if boot_result: + # Проверяем доступность API после загрузки + system_status = self.get_system_status() + if system_status.get("status") == "online": + logger.info("System booted successfully with API access") + return True + else: + logger.warning("System appears to be online but API may not be fully ready") + return True + else: + logger.error("System did not come online after WoL") + return False + + def power_off(self) -> bool: + """Выключение Synology NAS""" + if not self.is_online(force_check=True): + logger.info("Synology NAS is already offline") + return True + + logger.info("Powering off Synology NAS...") + result = self.shutdown_system() + + if result: + # Дополнительная проверка, что система действительно выключилась + logger.info("Verifying system is offline...") + for attempt in range(12): # Проверяем в течение 2 минут (12 * 10 сек) + if not self.is_online(force_check=True): + logger.info(f"System confirmed offline after {attempt * 10} seconds") + return True + logger.debug(f"System still shutting down, checking again... ({attempt + 1}/12)") + sleep(10) + + logger.warning("System still appears to be online after shutdown command") + return False + else: + logger.error("Failed to initiate shutdown") + return False + + # Заглушки для расширенных методов + def get_shared_folders(self) -> List[Dict[str, Any]]: + """Получение списка общих папок (заглушка)""" + logger.warning("Function get_shared_folders() is not implemented yet") + return [] + + def get_system_load(self) -> Dict[str, Any]: + """Получение информации о загрузке системы (заглушка)""" + logger.warning("Function get_system_load() is not implemented yet") + return {} + + def get_storage_status(self) -> Dict[str, Any]: + """Получение подробной информации о хранилище (заглушка)""" + logger.warning("Function get_storage_status() is not implemented yet") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for storage status request") + return {"error": "authentication_failed"} + + try: + # Здесь будет реализация после добавления соответствующего API + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.Storage.CGI.Storage", + "version": "1", + "method": "load_info", + "sid": self.sid + } + + # В текущей версии просто возвращаем заглушку + return { + "volumes": [], + "disks": [], + "total_size": 0, + "total_used": 0, + "error": "not_implemented" + } + except Exception as e: + logger.error(f"Error in get_storage_status: {str(e)}") + return {"error": str(e)} + + def get_security_status(self) -> Dict[str, Any]: + """Получение информации о состоянии безопасности (заглушка)""" + logger.warning("Function get_security_status() is not implemented yet") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for security status request") + return {"success": False, "error": "authentication_failed"} + + try: + # Здесь будет реализация после добавления соответствующего API + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.Core.SecurityScan.Status", + "version": "1", + "method": "get", + "sid": self.sid + } + + # В текущей версии просто возвращаем заглушку + return { + "success": False, + "status": "not_implemented", + "last_check": None, + "is_secure": False, + "error": "not_implemented" + } + except Exception as e: + logger.error(f"Error in get_security_status: {str(e)}") + return {"success": False, "error": str(e)} diff --git a/.history/src/api/synology_20250830080658.py b/.history/src/api/synology_20250830080658.py new file mode 100644 index 0000000..ea1d544 --- /dev/null +++ b/.history/src/api/synology_20250830080658.py @@ -0,0 +1,769 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Модуль для взаимодействия с API Synology NAS +""" + +import requests +from requests.adapters import HTTPAdapter +import json +import logging +import time +import urllib3 +from urllib3.util import Retry +from typing import Dict, Any, Optional, List +import socket +import struct +from time import sleep + +from src.config.config import ( + SYNOLOGY_HOST, + SYNOLOGY_PORT, + SYNOLOGY_USERNAME, + SYNOLOGY_PASSWORD, + SYNOLOGY_SECURE, + SYNOLOGY_TIMEOUT, + SYNOLOGY_MAC, + WOL_PORT +) + +# Отключение предупреждений о небезопасных SSL-соединениях +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + +logger = logging.getLogger(__name__) + +class SynologyAPI: + """Класс для взаимодействия с API Synology NAS""" + + def __init__(self): + """Инициализация класса SynologyAPI""" + logger.info("Creating API with auto-retry and connection pool") + logger.debug(f"Connection details: Host={SYNOLOGY_HOST}, Port={SYNOLOGY_PORT}, Secure={SYNOLOGY_SECURE}") + + self.protocol = "https" if SYNOLOGY_SECURE else "http" + self.base_url = f"{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi" + self.sid = None + self.session = requests.Session() + + # Настройка SSL + if self.protocol == "https": + logger.debug("SSL enabled, disabling certificate verification for internal network") + self.session.verify = False # Отключаем проверку SSL для внутренней сети + + # Добавляем повторные попытки для HTTP-запросов + retry_strategy = Retry( + total=5, # Увеличиваем количество попыток + status_forcelist=[429, 500, 502, 503, 504, 404], + allowed_methods=["GET", "POST"], + backoff_factor=1.5, # Увеличиваем задержку между попытками + respect_retry_after_header=True + ) + adapter = HTTPAdapter( + max_retries=retry_strategy, + pool_connections=3, + pool_maxsize=10 + ) + self.session.mount("http://", adapter) + self.session.mount("https://", adapter) + + # Таймауты будут указаны в запросах + self.default_timeout = (SYNOLOGY_TIMEOUT, SYNOLOGY_TIMEOUT*2) + logger.debug(f"Setting default request timeout: {self.default_timeout}") + + # Кэш для хранения результатов запросов + self._cache = {} + self._cache_ttl = {} + self._last_online_check = 0 + self._last_online_status = False + self._online_check_interval = 30 # Интервал для кэширования проверок онлайн-статуса + + # Время последней успешной аутентификации и срок действия сессии + self._last_auth_time = 0 + self._auth_expiry = 3600 # По умолчанию 1 час + + def login(self) -> bool: + """Авторизация в API Synology NAS""" + # Проверяем, действительна ли текущая сессия + current_time = time.time() + if self.sid and (current_time - self._last_auth_time) < self._auth_expiry: + logger.debug("Using existing session, still valid") + return True + + logger.info("Attempting to authenticate with Synology NAS...") + logger.debug(f"Base URL: {self.base_url}") + + # Проверяем доступность NAS перед запросом с помощью прямой TCP-проверки + # Избегаем вызова is_online(), чтобы не создавать рекурсию + online_status = self._check_tcp_connection() + if not online_status: + logger.error("Cannot login: Synology NAS is not reachable") + return False + + try: + url = f"{self.base_url}/auth.cgi" + params = { + "api": "SYNO.API.Auth", + "version": "3", + "method": "login", + "account": SYNOLOGY_USERNAME, + "passwd": SYNOLOGY_PASSWORD, + "session": "SynologyPowerControlBot", + "format": "cookie" + } + + logger.debug(f"Sending auth request to {url}") + start_time = time.time() + response = self.session.get( + url, + params=params, + timeout=self.default_timeout, + verify=False + ) + elapsed_time = time.time() - start_time + logger.debug(f"Auth request completed in {elapsed_time:.2f}s with status code {response.status_code}") + + # Проверка статуса HTTP + if response.status_code != 200: + logger.error(f"HTTP error: {response.status_code}") + logger.debug(f"Response content: {response.text[:200]}") + return False + + try: + data = response.json() + except json.JSONDecodeError: + logger.error("Failed to decode JSON response") + logger.debug(f"Response content: {response.text[:200]}") + return False + + if data.get("success"): + self.sid = data.get("data", {}).get("sid") + self._last_auth_time = current_time + logger.info("Successfully logged in to Synology NAS") + logger.debug(f"Session ID obtained: {self.sid[:5] if self.sid else 'None'}...") # Показываем только начало SID для безопасности + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to log in to Synology NAS: Error code {error_code}") + + # Дополнительная диагностика + if error_code == 400: + logger.error("Authentication error: Invalid credentials") + elif error_code == 401: + logger.error("Authentication error: Account disabled") + elif error_code == 402: + logger.error("Authentication error: Permission denied") + elif error_code == 403: + logger.error("Authentication error: 2-factor authentication required") + elif error_code == 404: + logger.error("Authentication error: Failed to authenticate with 2-factor authentication") + + return False + + except requests.exceptions.Timeout: + logger.error(f"Connection timeout after {SYNOLOGY_TIMEOUT} seconds") + return False + except requests.exceptions.ConnectionError as e: + logger.error(f"Connection error: {str(e)}") + logger.debug(f"Connection details: {self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}") + return False + except requests.RequestException as e: + logger.error(f"Request error: {str(e)}") + return False + except Exception as e: + logger.error(f"Unexpected error during login: {str(e)}", exc_info=True) + return False + + def logout(self) -> bool: + """Выход из API Synology NAS""" + if not self.sid: + return True + + try: + url = f"{self.base_url}/auth.cgi" + params = { + "api": "SYNO.API.Auth", + "version": "1", + "method": "logout", + "session": "SynologyPowerControlBot", + "_sid": self.sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + self.sid = None + logger.info("Successfully logged out from Synology NAS") + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to log out from Synology NAS: Error code {error_code}") + return False + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + return False + + def _make_api_request(self, api_name: str, method: str, version: int = 1, + params: Optional[Dict[str, Any]] = None, retry_auth: bool = True) -> Optional[Dict[str, Any]]: + """Обобщенный метод для выполнения API запросов с обработкой ошибок""" + # Проверка наличия авторизации + if not self.sid and not self.login(): + logger.error(f"Not authenticated for API request: {api_name}.{method}") + return None + + # Подготовка базовых параметров запроса + base_params = { + "api": api_name, + "version": str(version), + "method": method, + "sid": self.sid + } + + # Добавление дополнительных параметров, если они заданы + if params: + base_params.update(params) + + url = f"{self.base_url}/entry.cgi" + logger.debug(f"API request: {api_name}.{method} v{version}") + + try: + start_time = time.time() + response = self.session.get( + url, + params=base_params, + timeout=self.default_timeout, + verify=False + ) + elapsed_time = time.time() - start_time + logger.debug(f"API request completed in {elapsed_time:.2f}s with status {response.status_code}") + + # Проверка статуса HTTP + if response.status_code != 200: + logger.error(f"HTTP error: {response.status_code} for {api_name}.{method}") + return None + + try: + data = response.json() + except json.JSONDecodeError: + logger.error(f"Failed to decode JSON response for {api_name}.{method}") + logger.debug(f"Response content: {response.text[:200]}") + return None + + if data.get("success"): + logger.info(f"API request successful for {method}") + return data.get("data", {}) + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"API error for {api_name}.{method}: {error_code}") + + # Ошибки доступа или прав часто встречаются, но они не критичные + # Например, ошибка 102 означает, что нет прав, но NAS доступен + if error_code in [102, 103, 104, 105]: + logger.warning(f"Permission-related error for {api_name}.{method}: {error_code}") + # Возвращаем пустой словарь вместо None, + # чтобы вызывающий код мог понять, что запрос выполнен + return {} + + # Если ошибка связана с авторизацией и нам разрешено повторить попытку + if error_code in [106, 107, 119] and retry_auth: + logger.info("Session may have expired, re-authenticating...") + self.sid = None # Сбрасываем SID + if self.login(): + logger.info("Re-authenticated, retrying API request...") + # Рекурсивный вызов, но без повторной авторизации во избежание зацикливания + return self._make_api_request(api_name, method, version, params, False) + + return None + + except requests.exceptions.Timeout: + logger.error(f"Request timeout for {api_name}.{method}") + return None + except requests.exceptions.ConnectionError as e: + logger.error(f"Connection error for {api_name}.{method}: {str(e)}") + return None + except Exception as e: + logger.error(f"Unexpected error during {api_name}.{method}: {str(e)}") + return None + + def get_system_status(self) -> Dict[str, Any]: + """Получение статуса системы""" + # Проверяем доступность системы + if not self.is_online(): + logger.info("Device is offline, skipping API request") + return {"status": "offline"} + + # Проверяем, есть ли кэшированный результат + cache_key = "system_status" + current_time = time.time() + if cache_key in self._cache and current_time - self._cache_ttl.get(cache_key, 0) < 60: + logger.debug("Using cached system status") + return self._cache[cache_key] + + # Делаем API запрос + result = self._make_api_request("SYNO.DSM.Info", "getinfo") + + if result: + # Формируем расширенный ответ с дополнительной информацией + system_info = { + "status": "online", + "hostname": result.get("hostname", "unknown"), + "model": result.get("model", "unknown"), + "version": result.get("version", "unknown"), + "uptime": result.get("uptime", 0), + "time": current_time, + "is_online": True + } + + # Сохраняем в кэше + self._cache[cache_key] = system_info + self._cache_ttl[cache_key] = current_time + + return system_info + else: + # Если запрос не удался, но система онлайн + return { + "status": "error", + "error": "Failed to fetch system information", + "is_online": True, + "time": current_time + } + + def shutdown_system(self) -> bool: + """Выключение системы""" + # Проверяем, включено ли устройство перед попыткой его выключить + if not self.is_online(force_check=True): + logger.info("Synology NAS is already offline, no need to shut down") + return True + + logger.info("Attempting to shutdown Synology NAS...") + + # Правильный API-вызов для выключения согласно официальной документации DSM API + result = self._make_api_request("SYNO.Core.System.Power", "shutdown", version=1) + if result is not None: + logger.info("Successfully initiated system shutdown using SYNO.Core.System.Power") + + # Даем системе время начать процесс выключения + logger.info("Waiting for shutdown to initialize...") + sleep(5) + + # Проверяем статус + if not self.is_online(force_check=True): + logger.info("System is now offline. Shutdown confirmed successful.") + return True + else: + logger.info("System still appears to be online, but shutdown may be in progress.") + return True + + # Запасной вариант с устаревшим API + logger.info("First shutdown method failed, trying alternative API...") + result = self._make_api_request("SYNO.Core.System", "shutdown", version=3) + if result is not None: + logger.info("Successfully initiated system shutdown using SYNO.Core.System") + + # Даем системе время начать процесс выключения + logger.info("Waiting for shutdown to initialize...") + sleep(5) + + # Проверяем статус + if not self.is_online(force_check=True): + logger.info("System is now offline. Shutdown confirmed successful.") + return True + else: + logger.info("System still appears to be online, but shutdown may be in progress.") + return True + + # Если оба метода не сработали, но система стала недоступна + if not self.is_online(force_check=True): + logger.info("System appears to be shutting down despite API errors") + return True + + logger.error("Failed to shutdown system after trying multiple APIs") + return False + + def reboot_system(self) -> bool: + """Перезагрузка системы""" + # Проверяем, включена ли система + if not self.is_online(force_check=True): + logger.error("Cannot reboot: System is offline") + return False + + logger.info("Attempting to reboot Synology NAS...") + + # Правильный API-вызов для перезагрузки согласно официальной документации DSM API + result = self._make_api_request("SYNO.Core.System.Power", "restart", version=1) + + if result is not None: + logger.info("Successfully initiated system reboot using SYNO.Core.System.Power") + + # Даем системе время начать процесс перезагрузки + logger.info("Waiting for reboot to initialize...") + sleep(5) + + # Ждем, пока система станет недоступна (признак перезагрузки) + reboot_started = False + for i in range(12): # Проверяем в течение 60 секунд (12 * 5сек) + if not self.is_online(force_check=True): + logger.info(f"System went offline after {i*5} seconds, reboot in progress") + reboot_started = True + break + logger.debug(f"System still online, waiting... ({i+1}/12)") + sleep(5) + + if reboot_started: + # Ждем, пока система снова станет доступна + logger.info("Waiting for system to come back online...") + return self.wait_for_boot(max_attempts=30, delay=10) + else: + logger.warning("System did not go offline after reboot command") + return False + + # Запасной вариант, если первый метод не сработал + logger.info("First reboot method failed, trying alternative API...") + result = self._make_api_request("SYNO.Core.System", "reboot", version=3) + if result is not None: + logger.info("Successfully initiated system reboot using SYNO.Core.System") + + # Даем системе время начать процесс перезагрузки + logger.info("Waiting for reboot to initialize...") + sleep(5) + + # Ждем, пока система станет недоступна (признак перезагрузки) + reboot_started = False + for i in range(12): # Проверяем в течение 60 секунд (12 * 5сек) + if not self.is_online(force_check=True): + logger.info(f"System went offline after {i*5} seconds, reboot in progress") + reboot_started = True + break + logger.debug(f"System still online, waiting... ({i+1}/12)") + sleep(5) + + if reboot_started: + # Ждем, пока система снова станет доступна + logger.info("Waiting for system to come back online...") + return self.wait_for_boot(max_attempts=30, delay=10) + else: + logger.warning("System did not go offline after reboot command") + return False + + logger.error("Failed to reboot system after trying multiple APIs") + return False + + def _get_error_description(self, error_code: int) -> str: + """Получение описания ошибки по коду""" + error_descriptions = { + 100: "Unknown error", + 101: "Invalid parameter", + 102: "API does not exist", + 103: "Method does not exist", + 104: "Version does not support", + 105: "Permission denied", + 106: "Session timeout", + 107: "Session interrupted by duplicate login", + 400: "Invalid credentials", + 401: "Account disabled", + 402: "Permission denied", + 403: "2FA required", + 404: "Failed to authenticate with 2FA" + } + return error_descriptions.get(error_code, "Unknown error code") + + def _check_tcp_connection(self) -> bool: + """Проверка базового TCP-соединения с Synology NAS""" + try: + socket_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + socket_obj.settimeout(SYNOLOGY_TIMEOUT) + result = socket_obj.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT)) + socket_obj.close() + + return result == 0 + except socket.error as e: + logger.error(f"Socket error during connection check: {str(e)}") + return False + except Exception as e: + logger.error(f"Unexpected error during connection check: {str(e)}") + return False + + def is_online(self, force_check=False) -> bool: + """Проверка онлайн-статуса Synology NAS""" + # Используем кэшированное значение, если доступно и не устарело + current_time = time.time() + if not force_check and (current_time - self._last_online_check) < self._online_check_interval: + logger.debug(f"Using cached online status: {self._last_online_status}") + return self._last_online_status + + logger.info("Checking if NAS is online...") + + # Проверяем TCP-соединение + online_status = self._check_tcp_connection() + logger.info(f"Detected Synology NAS online status: {online_status}") + + # Если TCP-соединение успешно и у нас есть действующий SID, + # попробуем более детальную проверку через API + if online_status and self.sid: + logger.info("Trying to fetch more detailed online status through API...") + try: + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.DSM.Info", + "version": "1", + "method": "getinfo", + "sid": self.sid + } + + response = self.session.get( + url, + params=params, + timeout=self.default_timeout, + verify=False + ) + + if response.status_code == 200: + data = response.json() + if data.get("success"): + logger.info("API request successful for getinfo") + logger.info("Synology NAS is online with API access") + else: + logger.warning("API response indicates an error, but NAS is reachable") + logger.debug(f"API error: {data.get('error', {}).get('code', -1)}") + else: + logger.warning(f"API returned status code {response.status_code}") + except Exception as e: + logger.warning(f"API check failed, but TCP connection succeeded: {str(e)}") + + # Обновляем кэшированное значение + self._last_online_check = current_time + self._last_online_status = online_status + + return online_status + + def wake_on_lan(self) -> bool: + """Отправка Wake-on-LAN пакета для включения Synology NAS""" + if not SYNOLOGY_MAC: + logger.error("MAC address not configured") + return False + + try: + # Преобразование MAC-адреса в байты + mac_address = SYNOLOGY_MAC.replace(':', '').replace('-', '') + if len(mac_address) != 12: + logger.error(f"Invalid MAC address format: {SYNOLOGY_MAC}") + return False + + try: + mac_bytes = bytes.fromhex(mac_address) + except ValueError as e: + logger.error(f"Failed to parse MAC address: {str(e)}") + logger.error(f"MAC address must contain only hex characters: {SYNOLOGY_MAC}") + return False + + # Создание Magic Packet + magic_packet = b'\xff' * 6 + mac_bytes * 16 + + # Отправка пакета на конкретный адрес + try: + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) + sock.sendto(magic_packet, (SYNOLOGY_HOST, WOL_PORT)) + sock.close() + logger.info(f"Wake-on-LAN packet sent to {SYNOLOGY_MAC} at {SYNOLOGY_HOST}:{WOL_PORT}") + except Exception as e: + logger.error(f"Error sending directed WoL packet: {str(e)}") + return False + + # Для надежности отправляем также широковещательный пакет + try: + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) + + # Используем стандартный широковещательный адрес + broadcast_addr = "255.255.255.255" + sock.sendto(magic_packet, (broadcast_addr, WOL_PORT)) + sock.close() + logger.info(f"Wake-on-LAN broadcast packet sent to {broadcast_addr}:{WOL_PORT}") + except Exception as e: + logger.warning(f"Error sending broadcast WoL packet (non-critical): {str(e)}") + # Не считаем ошибкой, т.к. основной пакет уже отправлен + + return True + + except Exception as e: + logger.error(f"Unexpected error in wake_on_lan: {str(e)}") + return False + + def wait_for_boot(self, max_attempts: int = 30, delay: int = 10) -> bool: + """Ожидание загрузки Synology NAS""" + logger.info(f"Waiting for Synology NAS to boot (timeout: {max_attempts * delay}s)...") + + for attempt in range(max_attempts): + # Принудительно проверяем статус без использования кэша + if self.is_online(force_check=True): + logger.info(f"Synology NAS is online after {attempt + 1} attempts ({(attempt + 1) * delay}s)") + + # Проверяем, что не только сеть доступна, но и API загрузился + api_ready = False + logger.info("Waiting for API services to initialize...") + + for api_check in range(5): # Даем еще до 50 секунд для загрузки API + if self.sid or self.login(): + api_ready = True + logger.info(f"API services are ready after {api_check + 1} attempts") + break + logger.debug(f"API not ready yet, waiting... ({api_check + 1}/5)") + sleep(10) + + if not api_ready: + logger.warning("System is online but API services may not be fully initialized") + + # Дадим дополнительное время для полной загрузки всех сервисов + sleep(delay) + return True + + sleep(delay) + logger.info(f"Waiting for boot... Attempt {attempt + 1}/{max_attempts}") + + logger.error(f"Synology NAS didn't come online after {max_attempts * delay} seconds") + return False + + def power_on(self) -> bool: + """Включение Synology NAS""" + # Принудительная проверка статуса + if self.is_online(force_check=True): + logger.info("Synology NAS is already online") + return True + + logger.info("Powering on Synology NAS via Wake-on-LAN...") + + # Проверяем, настроен ли MAC-адрес + if not SYNOLOGY_MAC: + logger.error("Cannot power on: MAC address not configured in settings") + return False + + # Пробуем отправить несколько WoL пакетов для надежности + success = False + for attempt in range(3): + logger.debug(f"Sending WoL packet, attempt {attempt + 1}/3") + if self.wake_on_lan(): + success = True + break + sleep(1) + + if not success: + logger.error("Failed to send Wake-on-LAN packets") + return False + + # Ожидание загрузки + logger.info("WoL packets sent successfully, waiting for system to boot...") + boot_result = self.wait_for_boot(max_attempts=30, delay=10) + + if boot_result: + # Проверяем доступность API после загрузки + system_status = self.get_system_status() + if system_status.get("status") == "online": + logger.info("System booted successfully with API access") + return True + else: + logger.warning("System appears to be online but API may not be fully ready") + return True + else: + logger.error("System did not come online after WoL") + return False + + def power_off(self) -> bool: + """Выключение Synology NAS""" + if not self.is_online(force_check=True): + logger.info("Synology NAS is already offline") + return True + + logger.info("Powering off Synology NAS...") + result = self.shutdown_system() + + if result: + # Дополнительная проверка, что система действительно выключилась + logger.info("Verifying system is offline...") + for attempt in range(12): # Проверяем в течение 2 минут (12 * 10 сек) + if not self.is_online(force_check=True): + logger.info(f"System confirmed offline after {attempt * 10} seconds") + return True + logger.debug(f"System still shutting down, checking again... ({attempt + 1}/12)") + sleep(10) + + logger.warning("System still appears to be online after shutdown command") + return False + else: + logger.error("Failed to initiate shutdown") + return False + + # Заглушки для расширенных методов + def get_shared_folders(self) -> List[Dict[str, Any]]: + """Получение списка общих папок (заглушка)""" + logger.warning("Function get_shared_folders() is not implemented yet") + return [] + + def get_system_load(self) -> Dict[str, Any]: + """Получение информации о загрузке системы (заглушка)""" + logger.warning("Function get_system_load() is not implemented yet") + return {} + + def get_storage_status(self) -> Dict[str, Any]: + """Получение подробной информации о хранилище (заглушка)""" + logger.warning("Function get_storage_status() is not implemented yet") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for storage status request") + return {"error": "authentication_failed"} + + try: + # Здесь будет реализация после добавления соответствующего API + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.Storage.CGI.Storage", + "version": "1", + "method": "load_info", + "sid": self.sid + } + + # В текущей версии просто возвращаем заглушку + return { + "volumes": [], + "disks": [], + "total_size": 0, + "total_used": 0, + "error": "not_implemented" + } + except Exception as e: + logger.error(f"Error in get_storage_status: {str(e)}") + return {"error": str(e)} + + def get_security_status(self) -> Dict[str, Any]: + """Получение информации о состоянии безопасности (заглушка)""" + logger.warning("Function get_security_status() is not implemented yet") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for security status request") + return {"success": False, "error": "authentication_failed"} + + try: + # Здесь будет реализация после добавления соответствующего API + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.Core.SecurityScan.Status", + "version": "1", + "method": "get", + "sid": self.sid + } + + # В текущей версии просто возвращаем заглушку + return { + "success": False, + "status": "not_implemented", + "last_check": None, + "is_secure": False, + "error": "not_implemented" + } + except Exception as e: + logger.error(f"Error in get_security_status: {str(e)}") + return {"success": False, "error": str(e)} diff --git a/.history/src/api/synology_20250830080742.py b/.history/src/api/synology_20250830080742.py new file mode 100644 index 0000000..1b4b472 --- /dev/null +++ b/.history/src/api/synology_20250830080742.py @@ -0,0 +1,819 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Модуль для взаимодействия с API Synology NAS +""" + +import requests +from requests.adapters import HTTPAdapter +import json +import logging +import time +import urllib3 +from urllib3.util import Retry +from typing import Dict, Any, Optional, List +import socket +import struct +from time import sleep + +from src.config.config import ( + SYNOLOGY_HOST, + SYNOLOGY_PORT, + SYNOLOGY_USERNAME, + SYNOLOGY_PASSWORD, + SYNOLOGY_SECURE, + SYNOLOGY_TIMEOUT, + SYNOLOGY_MAC, + WOL_PORT +) + +# Отключение предупреждений о небезопасных SSL-соединениях +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + +logger = logging.getLogger(__name__) + +class SynologyAPI: + """Класс для взаимодействия с API Synology NAS""" + + def __init__(self): + """Инициализация класса SynologyAPI""" + logger.info("Creating API with auto-retry and connection pool") + logger.debug(f"Connection details: Host={SYNOLOGY_HOST}, Port={SYNOLOGY_PORT}, Secure={SYNOLOGY_SECURE}") + + self.protocol = "https" if SYNOLOGY_SECURE else "http" + self.base_url = f"{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi" + self.sid = None + self.session = requests.Session() + + # Настройка SSL + if self.protocol == "https": + logger.debug("SSL enabled, disabling certificate verification for internal network") + self.session.verify = False # Отключаем проверку SSL для внутренней сети + + # Добавляем повторные попытки для HTTP-запросов + retry_strategy = Retry( + total=5, # Увеличиваем количество попыток + status_forcelist=[429, 500, 502, 503, 504, 404], + allowed_methods=["GET", "POST"], + backoff_factor=1.5, # Увеличиваем задержку между попытками + respect_retry_after_header=True + ) + adapter = HTTPAdapter( + max_retries=retry_strategy, + pool_connections=3, + pool_maxsize=10 + ) + self.session.mount("http://", adapter) + self.session.mount("https://", adapter) + + # Таймауты будут указаны в запросах + self.default_timeout = (SYNOLOGY_TIMEOUT, SYNOLOGY_TIMEOUT*2) + logger.debug(f"Setting default request timeout: {self.default_timeout}") + + # Кэш для хранения результатов запросов + self._cache = {} + self._cache_ttl = {} + self._last_online_check = 0 + self._last_online_status = False + self._online_check_interval = 30 # Интервал для кэширования проверок онлайн-статуса + + # Время последней успешной аутентификации и срок действия сессии + self._last_auth_time = 0 + self._auth_expiry = 3600 # По умолчанию 1 час + + def login(self) -> bool: + """Авторизация в API Synology NAS""" + # Проверяем, действительна ли текущая сессия + current_time = time.time() + if self.sid and (current_time - self._last_auth_time) < self._auth_expiry: + logger.debug("Using existing session, still valid") + return True + + logger.info("Attempting to authenticate with Synology NAS...") + logger.debug(f"Base URL: {self.base_url}") + + # Проверяем доступность NAS перед запросом с помощью прямой TCP-проверки + # Избегаем вызова is_online(), чтобы не создавать рекурсию + online_status = self._check_tcp_connection() + if not online_status: + logger.error("Cannot login: Synology NAS is not reachable") + return False + + try: + # Проверка информации API для определения доступных версий API + api_info_url = f"{self.base_url}/entry.cgi" + api_info_params = { + "api": "SYNO.API.Info", + "version": "1", + "method": "query", + "query": "SYNO.API.Auth" + } + + logger.debug("Querying API info to determine optimal auth version") + try: + api_info_response = self.session.get( + api_info_url, + params=api_info_params, + timeout=self.default_timeout, + verify=False + ) + + if api_info_response.status_code == 200: + api_info_data = api_info_response.json() + if api_info_data.get("success"): + auth_info = api_info_data.get("data", {}).get("SYNO.API.Auth", {}) + max_version = auth_info.get("maxVersion", 3) + min_version = auth_info.get("minVersion", 1) + auth_path = auth_info.get("path", "auth.cgi") + logger.debug(f"Auth API versions: min={min_version}, max={max_version}, path={auth_path}") + + # Используем максимально поддерживаемую версию, но не выше 6 + auth_version = min(max_version, 6) + else: + logger.warning("Failed to query API info, using default auth version 3") + auth_version = 3 + auth_path = "auth.cgi" + else: + logger.warning(f"API info request failed with status {api_info_response.status_code}, using default auth version 3") + auth_version = 3 + auth_path = "auth.cgi" + except Exception as e: + logger.warning(f"Error querying API info: {str(e)}, using default auth version 3") + auth_version = 3 + auth_path = "auth.cgi" + + # Основной запрос авторизации + url = f"{self.base_url}/{auth_path}" + params = { + "api": "SYNO.API.Auth", + "version": str(auth_version), + "method": "login", + "account": SYNOLOGY_USERNAME, + "passwd": SYNOLOGY_PASSWORD, + "session": "SynologyPowerControlBot", + "format": "cookie" + } + + # Для версии 6+ используем немного другой формат + if auth_version >= 6: + params["enable_syno_token"] = "yes" + + logger.debug(f"Sending auth request to {url} with API version {auth_version}") + start_time = time.time() + response = self.session.get( + url, + params=params, + timeout=self.default_timeout, + verify=False + ) + elapsed_time = time.time() - start_time + logger.debug(f"Auth request completed in {elapsed_time:.2f}s with status code {response.status_code}") + + # Подробная информация для отладки + logger.debug(f"Auth response: {response.text[:500]}") + + # Проверка статуса HTTP + if response.status_code != 200: + logger.error(f"HTTP error: {response.status_code}") + logger.debug(f"Response content: {response.text[:200]}") + return False + + try: + data = response.json() + except json.JSONDecodeError: + logger.error("Failed to decode JSON response") + logger.debug(f"Response content: {response.text[:200]}") + return False + + if data.get("success"): + self.sid = data.get("data", {}).get("sid") + self._last_auth_time = current_time + logger.info("Successfully logged in to Synology NAS") + logger.debug(f"Session ID obtained: {self.sid[:5] if self.sid else 'None'}...") # Показываем только начало SID для безопасности + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to log in to Synology NAS: Error code {error_code}") + + # Дополнительная диагностика + if error_code == 400: + logger.error("Authentication error: Invalid credentials") + elif error_code == 401: + logger.error("Authentication error: Account disabled") + elif error_code == 402: + logger.error("Authentication error: Permission denied") + elif error_code == 403: + logger.error("Authentication error: 2-factor authentication required") + elif error_code == 404: + logger.error("Authentication error: Failed to authenticate with 2-factor authentication") + + return False + + except requests.exceptions.Timeout: + logger.error(f"Connection timeout after {SYNOLOGY_TIMEOUT} seconds") + return False + except requests.exceptions.ConnectionError as e: + logger.error(f"Connection error: {str(e)}") + logger.debug(f"Connection details: {self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}") + return False + except requests.RequestException as e: + logger.error(f"Request error: {str(e)}") + return False + except Exception as e: + logger.error(f"Unexpected error during login: {str(e)}", exc_info=True) + return False + + def logout(self) -> bool: + """Выход из API Synology NAS""" + if not self.sid: + return True + + try: + url = f"{self.base_url}/auth.cgi" + params = { + "api": "SYNO.API.Auth", + "version": "1", + "method": "logout", + "session": "SynologyPowerControlBot", + "_sid": self.sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + self.sid = None + logger.info("Successfully logged out from Synology NAS") + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to log out from Synology NAS: Error code {error_code}") + return False + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + return False + + def _make_api_request(self, api_name: str, method: str, version: int = 1, + params: Optional[Dict[str, Any]] = None, retry_auth: bool = True) -> Optional[Dict[str, Any]]: + """Обобщенный метод для выполнения API запросов с обработкой ошибок""" + # Проверка наличия авторизации + if not self.sid and not self.login(): + logger.error(f"Not authenticated for API request: {api_name}.{method}") + return None + + # Подготовка базовых параметров запроса + base_params = { + "api": api_name, + "version": str(version), + "method": method, + "sid": self.sid + } + + # Добавление дополнительных параметров, если они заданы + if params: + base_params.update(params) + + url = f"{self.base_url}/entry.cgi" + logger.debug(f"API request: {api_name}.{method} v{version}") + + try: + start_time = time.time() + response = self.session.get( + url, + params=base_params, + timeout=self.default_timeout, + verify=False + ) + elapsed_time = time.time() - start_time + logger.debug(f"API request completed in {elapsed_time:.2f}s with status {response.status_code}") + + # Проверка статуса HTTP + if response.status_code != 200: + logger.error(f"HTTP error: {response.status_code} for {api_name}.{method}") + return None + + try: + data = response.json() + except json.JSONDecodeError: + logger.error(f"Failed to decode JSON response for {api_name}.{method}") + logger.debug(f"Response content: {response.text[:200]}") + return None + + if data.get("success"): + logger.info(f"API request successful for {method}") + return data.get("data", {}) + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"API error for {api_name}.{method}: {error_code}") + + # Ошибки доступа или прав часто встречаются, но они не критичные + # Например, ошибка 102 означает, что нет прав, но NAS доступен + if error_code in [102, 103, 104, 105]: + logger.warning(f"Permission-related error for {api_name}.{method}: {error_code}") + # Возвращаем пустой словарь вместо None, + # чтобы вызывающий код мог понять, что запрос выполнен + return {} + + # Если ошибка связана с авторизацией и нам разрешено повторить попытку + if error_code in [106, 107, 119] and retry_auth: + logger.info("Session may have expired, re-authenticating...") + self.sid = None # Сбрасываем SID + if self.login(): + logger.info("Re-authenticated, retrying API request...") + # Рекурсивный вызов, но без повторной авторизации во избежание зацикливания + return self._make_api_request(api_name, method, version, params, False) + + return None + + except requests.exceptions.Timeout: + logger.error(f"Request timeout for {api_name}.{method}") + return None + except requests.exceptions.ConnectionError as e: + logger.error(f"Connection error for {api_name}.{method}: {str(e)}") + return None + except Exception as e: + logger.error(f"Unexpected error during {api_name}.{method}: {str(e)}") + return None + + def get_system_status(self) -> Dict[str, Any]: + """Получение статуса системы""" + # Проверяем доступность системы + if not self.is_online(): + logger.info("Device is offline, skipping API request") + return {"status": "offline"} + + # Проверяем, есть ли кэшированный результат + cache_key = "system_status" + current_time = time.time() + if cache_key in self._cache and current_time - self._cache_ttl.get(cache_key, 0) < 60: + logger.debug("Using cached system status") + return self._cache[cache_key] + + # Делаем API запрос + result = self._make_api_request("SYNO.DSM.Info", "getinfo") + + if result: + # Формируем расширенный ответ с дополнительной информацией + system_info = { + "status": "online", + "hostname": result.get("hostname", "unknown"), + "model": result.get("model", "unknown"), + "version": result.get("version", "unknown"), + "uptime": result.get("uptime", 0), + "time": current_time, + "is_online": True + } + + # Сохраняем в кэше + self._cache[cache_key] = system_info + self._cache_ttl[cache_key] = current_time + + return system_info + else: + # Если запрос не удался, но система онлайн + return { + "status": "error", + "error": "Failed to fetch system information", + "is_online": True, + "time": current_time + } + + def shutdown_system(self) -> bool: + """Выключение системы""" + # Проверяем, включено ли устройство перед попыткой его выключить + if not self.is_online(force_check=True): + logger.info("Synology NAS is already offline, no need to shut down") + return True + + logger.info("Attempting to shutdown Synology NAS...") + + # Правильный API-вызов для выключения согласно официальной документации DSM API + result = self._make_api_request("SYNO.Core.System.Power", "shutdown", version=1) + if result is not None: + logger.info("Successfully initiated system shutdown using SYNO.Core.System.Power") + + # Даем системе время начать процесс выключения + logger.info("Waiting for shutdown to initialize...") + sleep(5) + + # Проверяем статус + if not self.is_online(force_check=True): + logger.info("System is now offline. Shutdown confirmed successful.") + return True + else: + logger.info("System still appears to be online, but shutdown may be in progress.") + return True + + # Запасной вариант с устаревшим API + logger.info("First shutdown method failed, trying alternative API...") + result = self._make_api_request("SYNO.Core.System", "shutdown", version=3) + if result is not None: + logger.info("Successfully initiated system shutdown using SYNO.Core.System") + + # Даем системе время начать процесс выключения + logger.info("Waiting for shutdown to initialize...") + sleep(5) + + # Проверяем статус + if not self.is_online(force_check=True): + logger.info("System is now offline. Shutdown confirmed successful.") + return True + else: + logger.info("System still appears to be online, but shutdown may be in progress.") + return True + + # Если оба метода не сработали, но система стала недоступна + if not self.is_online(force_check=True): + logger.info("System appears to be shutting down despite API errors") + return True + + logger.error("Failed to shutdown system after trying multiple APIs") + return False + + def reboot_system(self) -> bool: + """Перезагрузка системы""" + # Проверяем, включена ли система + if not self.is_online(force_check=True): + logger.error("Cannot reboot: System is offline") + return False + + logger.info("Attempting to reboot Synology NAS...") + + # Правильный API-вызов для перезагрузки согласно официальной документации DSM API + result = self._make_api_request("SYNO.Core.System.Power", "restart", version=1) + + if result is not None: + logger.info("Successfully initiated system reboot using SYNO.Core.System.Power") + + # Даем системе время начать процесс перезагрузки + logger.info("Waiting for reboot to initialize...") + sleep(5) + + # Ждем, пока система станет недоступна (признак перезагрузки) + reboot_started = False + for i in range(12): # Проверяем в течение 60 секунд (12 * 5сек) + if not self.is_online(force_check=True): + logger.info(f"System went offline after {i*5} seconds, reboot in progress") + reboot_started = True + break + logger.debug(f"System still online, waiting... ({i+1}/12)") + sleep(5) + + if reboot_started: + # Ждем, пока система снова станет доступна + logger.info("Waiting for system to come back online...") + return self.wait_for_boot(max_attempts=30, delay=10) + else: + logger.warning("System did not go offline after reboot command") + return False + + # Запасной вариант, если первый метод не сработал + logger.info("First reboot method failed, trying alternative API...") + result = self._make_api_request("SYNO.Core.System", "reboot", version=3) + if result is not None: + logger.info("Successfully initiated system reboot using SYNO.Core.System") + + # Даем системе время начать процесс перезагрузки + logger.info("Waiting for reboot to initialize...") + sleep(5) + + # Ждем, пока система станет недоступна (признак перезагрузки) + reboot_started = False + for i in range(12): # Проверяем в течение 60 секунд (12 * 5сек) + if not self.is_online(force_check=True): + logger.info(f"System went offline after {i*5} seconds, reboot in progress") + reboot_started = True + break + logger.debug(f"System still online, waiting... ({i+1}/12)") + sleep(5) + + if reboot_started: + # Ждем, пока система снова станет доступна + logger.info("Waiting for system to come back online...") + return self.wait_for_boot(max_attempts=30, delay=10) + else: + logger.warning("System did not go offline after reboot command") + return False + + logger.error("Failed to reboot system after trying multiple APIs") + return False + + def _get_error_description(self, error_code: int) -> str: + """Получение описания ошибки по коду""" + error_descriptions = { + 100: "Unknown error", + 101: "Invalid parameter", + 102: "API does not exist", + 103: "Method does not exist", + 104: "Version does not support", + 105: "Permission denied", + 106: "Session timeout", + 107: "Session interrupted by duplicate login", + 400: "Invalid credentials", + 401: "Account disabled", + 402: "Permission denied", + 403: "2FA required", + 404: "Failed to authenticate with 2FA" + } + return error_descriptions.get(error_code, "Unknown error code") + + def _check_tcp_connection(self) -> bool: + """Проверка базового TCP-соединения с Synology NAS""" + try: + socket_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + socket_obj.settimeout(SYNOLOGY_TIMEOUT) + result = socket_obj.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT)) + socket_obj.close() + + return result == 0 + except socket.error as e: + logger.error(f"Socket error during connection check: {str(e)}") + return False + except Exception as e: + logger.error(f"Unexpected error during connection check: {str(e)}") + return False + + def is_online(self, force_check=False) -> bool: + """Проверка онлайн-статуса Synology NAS""" + # Используем кэшированное значение, если доступно и не устарело + current_time = time.time() + if not force_check and (current_time - self._last_online_check) < self._online_check_interval: + logger.debug(f"Using cached online status: {self._last_online_status}") + return self._last_online_status + + logger.info("Checking if NAS is online...") + + # Проверяем TCP-соединение + online_status = self._check_tcp_connection() + logger.info(f"Detected Synology NAS online status: {online_status}") + + # Если TCP-соединение успешно и у нас есть действующий SID, + # попробуем более детальную проверку через API + if online_status and self.sid: + logger.info("Trying to fetch more detailed online status through API...") + try: + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.DSM.Info", + "version": "1", + "method": "getinfo", + "sid": self.sid + } + + response = self.session.get( + url, + params=params, + timeout=self.default_timeout, + verify=False + ) + + if response.status_code == 200: + data = response.json() + if data.get("success"): + logger.info("API request successful for getinfo") + logger.info("Synology NAS is online with API access") + else: + logger.warning("API response indicates an error, but NAS is reachable") + logger.debug(f"API error: {data.get('error', {}).get('code', -1)}") + else: + logger.warning(f"API returned status code {response.status_code}") + except Exception as e: + logger.warning(f"API check failed, but TCP connection succeeded: {str(e)}") + + # Обновляем кэшированное значение + self._last_online_check = current_time + self._last_online_status = online_status + + return online_status + + def wake_on_lan(self) -> bool: + """Отправка Wake-on-LAN пакета для включения Synology NAS""" + if not SYNOLOGY_MAC: + logger.error("MAC address not configured") + return False + + try: + # Преобразование MAC-адреса в байты + mac_address = SYNOLOGY_MAC.replace(':', '').replace('-', '') + if len(mac_address) != 12: + logger.error(f"Invalid MAC address format: {SYNOLOGY_MAC}") + return False + + try: + mac_bytes = bytes.fromhex(mac_address) + except ValueError as e: + logger.error(f"Failed to parse MAC address: {str(e)}") + logger.error(f"MAC address must contain only hex characters: {SYNOLOGY_MAC}") + return False + + # Создание Magic Packet + magic_packet = b'\xff' * 6 + mac_bytes * 16 + + # Отправка пакета на конкретный адрес + try: + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) + sock.sendto(magic_packet, (SYNOLOGY_HOST, WOL_PORT)) + sock.close() + logger.info(f"Wake-on-LAN packet sent to {SYNOLOGY_MAC} at {SYNOLOGY_HOST}:{WOL_PORT}") + except Exception as e: + logger.error(f"Error sending directed WoL packet: {str(e)}") + return False + + # Для надежности отправляем также широковещательный пакет + try: + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) + + # Используем стандартный широковещательный адрес + broadcast_addr = "255.255.255.255" + sock.sendto(magic_packet, (broadcast_addr, WOL_PORT)) + sock.close() + logger.info(f"Wake-on-LAN broadcast packet sent to {broadcast_addr}:{WOL_PORT}") + except Exception as e: + logger.warning(f"Error sending broadcast WoL packet (non-critical): {str(e)}") + # Не считаем ошибкой, т.к. основной пакет уже отправлен + + return True + + except Exception as e: + logger.error(f"Unexpected error in wake_on_lan: {str(e)}") + return False + + def wait_for_boot(self, max_attempts: int = 30, delay: int = 10) -> bool: + """Ожидание загрузки Synology NAS""" + logger.info(f"Waiting for Synology NAS to boot (timeout: {max_attempts * delay}s)...") + + for attempt in range(max_attempts): + # Принудительно проверяем статус без использования кэша + if self.is_online(force_check=True): + logger.info(f"Synology NAS is online after {attempt + 1} attempts ({(attempt + 1) * delay}s)") + + # Проверяем, что не только сеть доступна, но и API загрузился + api_ready = False + logger.info("Waiting for API services to initialize...") + + for api_check in range(5): # Даем еще до 50 секунд для загрузки API + if self.sid or self.login(): + api_ready = True + logger.info(f"API services are ready after {api_check + 1} attempts") + break + logger.debug(f"API not ready yet, waiting... ({api_check + 1}/5)") + sleep(10) + + if not api_ready: + logger.warning("System is online but API services may not be fully initialized") + + # Дадим дополнительное время для полной загрузки всех сервисов + sleep(delay) + return True + + sleep(delay) + logger.info(f"Waiting for boot... Attempt {attempt + 1}/{max_attempts}") + + logger.error(f"Synology NAS didn't come online after {max_attempts * delay} seconds") + return False + + def power_on(self) -> bool: + """Включение Synology NAS""" + # Принудительная проверка статуса + if self.is_online(force_check=True): + logger.info("Synology NAS is already online") + return True + + logger.info("Powering on Synology NAS via Wake-on-LAN...") + + # Проверяем, настроен ли MAC-адрес + if not SYNOLOGY_MAC: + logger.error("Cannot power on: MAC address not configured in settings") + return False + + # Пробуем отправить несколько WoL пакетов для надежности + success = False + for attempt in range(3): + logger.debug(f"Sending WoL packet, attempt {attempt + 1}/3") + if self.wake_on_lan(): + success = True + break + sleep(1) + + if not success: + logger.error("Failed to send Wake-on-LAN packets") + return False + + # Ожидание загрузки + logger.info("WoL packets sent successfully, waiting for system to boot...") + boot_result = self.wait_for_boot(max_attempts=30, delay=10) + + if boot_result: + # Проверяем доступность API после загрузки + system_status = self.get_system_status() + if system_status.get("status") == "online": + logger.info("System booted successfully with API access") + return True + else: + logger.warning("System appears to be online but API may not be fully ready") + return True + else: + logger.error("System did not come online after WoL") + return False + + def power_off(self) -> bool: + """Выключение Synology NAS""" + if not self.is_online(force_check=True): + logger.info("Synology NAS is already offline") + return True + + logger.info("Powering off Synology NAS...") + result = self.shutdown_system() + + if result: + # Дополнительная проверка, что система действительно выключилась + logger.info("Verifying system is offline...") + for attempt in range(12): # Проверяем в течение 2 минут (12 * 10 сек) + if not self.is_online(force_check=True): + logger.info(f"System confirmed offline after {attempt * 10} seconds") + return True + logger.debug(f"System still shutting down, checking again... ({attempt + 1}/12)") + sleep(10) + + logger.warning("System still appears to be online after shutdown command") + return False + else: + logger.error("Failed to initiate shutdown") + return False + + # Заглушки для расширенных методов + def get_shared_folders(self) -> List[Dict[str, Any]]: + """Получение списка общих папок (заглушка)""" + logger.warning("Function get_shared_folders() is not implemented yet") + return [] + + def get_system_load(self) -> Dict[str, Any]: + """Получение информации о загрузке системы (заглушка)""" + logger.warning("Function get_system_load() is not implemented yet") + return {} + + def get_storage_status(self) -> Dict[str, Any]: + """Получение подробной информации о хранилище (заглушка)""" + logger.warning("Function get_storage_status() is not implemented yet") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for storage status request") + return {"error": "authentication_failed"} + + try: + # Здесь будет реализация после добавления соответствующего API + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.Storage.CGI.Storage", + "version": "1", + "method": "load_info", + "sid": self.sid + } + + # В текущей версии просто возвращаем заглушку + return { + "volumes": [], + "disks": [], + "total_size": 0, + "total_used": 0, + "error": "not_implemented" + } + except Exception as e: + logger.error(f"Error in get_storage_status: {str(e)}") + return {"error": str(e)} + + def get_security_status(self) -> Dict[str, Any]: + """Получение информации о состоянии безопасности (заглушка)""" + logger.warning("Function get_security_status() is not implemented yet") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for security status request") + return {"success": False, "error": "authentication_failed"} + + try: + # Здесь будет реализация после добавления соответствующего API + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.Core.SecurityScan.Status", + "version": "1", + "method": "get", + "sid": self.sid + } + + # В текущей версии просто возвращаем заглушку + return { + "success": False, + "status": "not_implemented", + "last_check": None, + "is_secure": False, + "error": "not_implemented" + } + except Exception as e: + logger.error(f"Error in get_security_status: {str(e)}") + return {"success": False, "error": str(e)} diff --git a/.history/src/api/synology_20250830080825.py b/.history/src/api/synology_20250830080825.py new file mode 100644 index 0000000..416f33a --- /dev/null +++ b/.history/src/api/synology_20250830080825.py @@ -0,0 +1,866 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Модуль для взаимодействия с API Synology NAS +""" + +import requests +from requests.adapters import HTTPAdapter +import json +import logging +import time +import urllib3 +from urllib3.util import Retry +from typing import Dict, Any, Optional, List +import socket +import struct +from time import sleep + +from src.config.config import ( + SYNOLOGY_HOST, + SYNOLOGY_PORT, + SYNOLOGY_USERNAME, + SYNOLOGY_PASSWORD, + SYNOLOGY_SECURE, + SYNOLOGY_TIMEOUT, + SYNOLOGY_MAC, + WOL_PORT +) + +# Отключение предупреждений о небезопасных SSL-соединениях +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + +logger = logging.getLogger(__name__) + +class SynologyAPI: + """Класс для взаимодействия с API Synology NAS""" + + def __init__(self): + """Инициализация класса SynologyAPI""" + logger.info("Creating API with auto-retry and connection pool") + logger.debug(f"Connection details: Host={SYNOLOGY_HOST}, Port={SYNOLOGY_PORT}, Secure={SYNOLOGY_SECURE}") + + self.protocol = "https" if SYNOLOGY_SECURE else "http" + self.base_url = f"{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi" + self.sid = None + self.session = requests.Session() + + # Настройка SSL + if self.protocol == "https": + logger.debug("SSL enabled, disabling certificate verification for internal network") + self.session.verify = False # Отключаем проверку SSL для внутренней сети + + # Добавляем повторные попытки для HTTP-запросов + retry_strategy = Retry( + total=5, # Увеличиваем количество попыток + status_forcelist=[429, 500, 502, 503, 504, 404], + allowed_methods=["GET", "POST"], + backoff_factor=1.5, # Увеличиваем задержку между попытками + respect_retry_after_header=True + ) + adapter = HTTPAdapter( + max_retries=retry_strategy, + pool_connections=3, + pool_maxsize=10 + ) + self.session.mount("http://", adapter) + self.session.mount("https://", adapter) + + # Таймауты будут указаны в запросах + self.default_timeout = (SYNOLOGY_TIMEOUT, SYNOLOGY_TIMEOUT*2) + logger.debug(f"Setting default request timeout: {self.default_timeout}") + + # Кэш для хранения результатов запросов + self._cache = {} + self._cache_ttl = {} + self._last_online_check = 0 + self._last_online_status = False + self._online_check_interval = 30 # Интервал для кэширования проверок онлайн-статуса + + # Время последней успешной аутентификации и срок действия сессии + self._last_auth_time = 0 + self._auth_expiry = 3600 # По умолчанию 1 час + + def login(self) -> bool: + """Авторизация в API Synology NAS""" + # Проверяем, действительна ли текущая сессия + current_time = time.time() + if self.sid and (current_time - self._last_auth_time) < self._auth_expiry: + logger.debug("Using existing session, still valid") + return True + + logger.info("Attempting to authenticate with Synology NAS...") + logger.debug(f"Base URL: {self.base_url}") + + # Проверяем доступность NAS перед запросом с помощью прямой TCP-проверки + # Избегаем вызова is_online(), чтобы не создавать рекурсию + online_status = self._check_tcp_connection() + if not online_status: + logger.error("Cannot login: Synology NAS is not reachable") + return False + + try: + # Проверка информации API для определения доступных версий API + api_info_url = f"{self.base_url}/entry.cgi" + api_info_params = { + "api": "SYNO.API.Info", + "version": "1", + "method": "query", + "query": "SYNO.API.Auth" + } + + logger.debug("Querying API info to determine optimal auth version") + try: + api_info_response = self.session.get( + api_info_url, + params=api_info_params, + timeout=self.default_timeout, + verify=False + ) + + if api_info_response.status_code == 200: + api_info_data = api_info_response.json() + if api_info_data.get("success"): + auth_info = api_info_data.get("data", {}).get("SYNO.API.Auth", {}) + max_version = auth_info.get("maxVersion", 3) + min_version = auth_info.get("minVersion", 1) + auth_path = auth_info.get("path", "auth.cgi") + logger.debug(f"Auth API versions: min={min_version}, max={max_version}, path={auth_path}") + + # Используем максимально поддерживаемую версию, но не выше 6 + auth_version = min(max_version, 6) + else: + logger.warning("Failed to query API info, using default auth version 3") + auth_version = 3 + auth_path = "auth.cgi" + else: + logger.warning(f"API info request failed with status {api_info_response.status_code}, using default auth version 3") + auth_version = 3 + auth_path = "auth.cgi" + except Exception as e: + logger.warning(f"Error querying API info: {str(e)}, using default auth version 3") + auth_version = 3 + auth_path = "auth.cgi" + + # Основной запрос авторизации + url = f"{self.base_url}/{auth_path}" + params = { + "api": "SYNO.API.Auth", + "version": str(auth_version), + "method": "login", + "account": SYNOLOGY_USERNAME, + "passwd": SYNOLOGY_PASSWORD, + "session": "SynologyPowerControlBot", + "format": "cookie" + } + + # Для версии 6+ используем немного другой формат + if auth_version >= 6: + params["enable_syno_token"] = "yes" + + logger.debug(f"Sending auth request to {url} with API version {auth_version}") + start_time = time.time() + response = self.session.get( + url, + params=params, + timeout=self.default_timeout, + verify=False + ) + elapsed_time = time.time() - start_time + logger.debug(f"Auth request completed in {elapsed_time:.2f}s with status code {response.status_code}") + + # Подробная информация для отладки + logger.debug(f"Auth response: {response.text[:500]}") + + # Проверка статуса HTTP + if response.status_code != 200: + logger.error(f"HTTP error: {response.status_code}") + logger.debug(f"Response content: {response.text[:200]}") + return False + + try: + data = response.json() + except json.JSONDecodeError: + logger.error("Failed to decode JSON response") + logger.debug(f"Response content: {response.text[:200]}") + return False + + if data.get("success"): + self.sid = data.get("data", {}).get("sid") + self._last_auth_time = current_time + logger.info("Successfully logged in to Synology NAS") + logger.debug(f"Session ID obtained: {self.sid[:5] if self.sid else 'None'}...") # Показываем только начало SID для безопасности + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to log in to Synology NAS: Error code {error_code}") + + # Дополнительная диагностика + if error_code == 400: + logger.error("Authentication error: Invalid credentials") + elif error_code == 401: + logger.error("Authentication error: Account disabled") + elif error_code == 402: + logger.error("Authentication error: Permission denied") + elif error_code == 403: + logger.error("Authentication error: 2-factor authentication required") + elif error_code == 404: + logger.error("Authentication error: Failed to authenticate with 2-factor authentication") + + return False + + except requests.exceptions.Timeout: + logger.error(f"Connection timeout after {SYNOLOGY_TIMEOUT} seconds") + return False + except requests.exceptions.ConnectionError as e: + logger.error(f"Connection error: {str(e)}") + logger.debug(f"Connection details: {self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}") + return False + except requests.RequestException as e: + logger.error(f"Request error: {str(e)}") + return False + except Exception as e: + logger.error(f"Unexpected error during login: {str(e)}", exc_info=True) + return False + + def logout(self) -> bool: + """Выход из API Synology NAS""" + if not self.sid: + return True + + try: + url = f"{self.base_url}/auth.cgi" + params = { + "api": "SYNO.API.Auth", + "version": "1", + "method": "logout", + "session": "SynologyPowerControlBot", + "_sid": self.sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + self.sid = None + logger.info("Successfully logged out from Synology NAS") + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to log out from Synology NAS: Error code {error_code}") + return False + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + return False + + def _make_api_request(self, api_name: str, method: str, version: int = 1, + params: Optional[Dict[str, Any]] = None, retry_auth: bool = True) -> Optional[Dict[str, Any]]: + """Обобщенный метод для выполнения API запросов с обработкой ошибок""" + # Проверка наличия авторизации + if not self.sid and not self.login(): + logger.error(f"Not authenticated for API request: {api_name}.{method}") + return None + + # Проверка информации API для определения пути и поддерживаемой версии + api_info_url = f"{self.base_url}/entry.cgi" + api_info_params = { + "api": "SYNO.API.Info", + "version": "1", + "method": "query", + "query": api_name + } + + api_path = "entry.cgi" + try: + logger.debug(f"Querying API info for {api_name}") + api_info_response = self.session.get( + api_info_url, + params=api_info_params, + timeout=self.default_timeout, + verify=False + ) + + if api_info_response.status_code == 200: + api_info_data = api_info_response.json() + if api_info_data.get("success"): + api_info = api_info_data.get("data", {}).get(api_name, {}) + if api_info: + max_version = api_info.get("maxVersion", version) + min_version = api_info.get("minVersion", version) + api_path = api_info.get("path", "entry.cgi") + + # Проверка, поддерживается ли запрошенная версия + if version < min_version: + logger.warning(f"API version {version} for {api_name} is below minimum {min_version}, using {min_version}") + version = min_version + elif version > max_version: + logger.warning(f"API version {version} for {api_name} exceeds maximum {max_version}, using {max_version}") + version = max_version + + logger.debug(f"Using API path: {api_path}, version: {version}") + else: + logger.warning(f"API {api_name} not found in API info, using defaults") + except Exception as e: + logger.warning(f"Error querying API info for {api_name}: {str(e)}, using defaults") + + # Подготовка базовых параметров запроса + base_params = { + "api": api_name, + "version": str(version), + "method": method, + "sid": self.sid + } + + # Добавление дополнительных параметров, если они заданы + if params: + base_params.update(params) + + url = f"{self.base_url}/{api_path}" + logger.debug(f"API request: {api_name}.{method} v{version} to {url}") + logger.debug(f"Full request params: {base_params}") + + try: + start_time = time.time() + response = self.session.get( + url, + params=base_params, + timeout=self.default_timeout, + verify=False + ) + elapsed_time = time.time() - start_time + logger.debug(f"API request completed in {elapsed_time:.2f}s with status {response.status_code}") + + # Подробное логирование для отладки + logger.debug(f"Response: {response.text[:500]}") + + # Проверка статуса HTTP + if response.status_code != 200: + logger.error(f"HTTP error: {response.status_code} for {api_name}.{method}") + return None + + try: + data = response.json() + except json.JSONDecodeError: + logger.error(f"Failed to decode JSON response for {api_name}.{method}") + logger.debug(f"Response content: {response.text[:200]}") + return None + + if data.get("success"): + logger.info(f"API request successful for {api_name}.{method}") + return data.get("data", {}) + else: + error_code = data.get("error", {}).get("code", -1) + error_desc = self._get_error_description(error_code) + logger.error(f"API error for {api_name}.{method}: {error_code} - {error_desc}") + + # Ошибки доступа или прав часто встречаются, но они не критичные + # Например, ошибка 102 означает, что нет прав, но NAS доступен + if error_code in [102, 103, 104, 105]: + logger.warning(f"Permission-related error for {api_name}.{method}: {error_code}") + # Возвращаем пустой словарь вместо None, + # чтобы вызывающий код мог понять, что запрос выполнен + return {} + + # Если ошибка связана с авторизацией и нам разрешено повторить попытку + if error_code in [106, 107, 119] and retry_auth: + logger.info("Session may have expired, re-authenticating...") + self.sid = None # Сбрасываем SID + if self.login(): + logger.info("Re-authenticated, retrying API request...") + # Рекурсивный вызов, но без повторной авторизации во избежание зацикливания + return self._make_api_request(api_name, method, version, params, False) + + return None + + except requests.exceptions.Timeout: + logger.error(f"Request timeout for {api_name}.{method}") + return None + except requests.exceptions.ConnectionError as e: + logger.error(f"Connection error for {api_name}.{method}: {str(e)}") + return None + except Exception as e: + logger.error(f"Unexpected error during {api_name}.{method}: {str(e)}") + return None + + def get_system_status(self) -> Dict[str, Any]: + """Получение статуса системы""" + # Проверяем доступность системы + if not self.is_online(): + logger.info("Device is offline, skipping API request") + return {"status": "offline"} + + # Проверяем, есть ли кэшированный результат + cache_key = "system_status" + current_time = time.time() + if cache_key in self._cache and current_time - self._cache_ttl.get(cache_key, 0) < 60: + logger.debug("Using cached system status") + return self._cache[cache_key] + + # Делаем API запрос + result = self._make_api_request("SYNO.DSM.Info", "getinfo") + + if result: + # Формируем расширенный ответ с дополнительной информацией + system_info = { + "status": "online", + "hostname": result.get("hostname", "unknown"), + "model": result.get("model", "unknown"), + "version": result.get("version", "unknown"), + "uptime": result.get("uptime", 0), + "time": current_time, + "is_online": True + } + + # Сохраняем в кэше + self._cache[cache_key] = system_info + self._cache_ttl[cache_key] = current_time + + return system_info + else: + # Если запрос не удался, но система онлайн + return { + "status": "error", + "error": "Failed to fetch system information", + "is_online": True, + "time": current_time + } + + def shutdown_system(self) -> bool: + """Выключение системы""" + # Проверяем, включено ли устройство перед попыткой его выключить + if not self.is_online(force_check=True): + logger.info("Synology NAS is already offline, no need to shut down") + return True + + logger.info("Attempting to shutdown Synology NAS...") + + # Правильный API-вызов для выключения согласно официальной документации DSM API + result = self._make_api_request("SYNO.Core.System.Power", "shutdown", version=1) + if result is not None: + logger.info("Successfully initiated system shutdown using SYNO.Core.System.Power") + + # Даем системе время начать процесс выключения + logger.info("Waiting for shutdown to initialize...") + sleep(5) + + # Проверяем статус + if not self.is_online(force_check=True): + logger.info("System is now offline. Shutdown confirmed successful.") + return True + else: + logger.info("System still appears to be online, but shutdown may be in progress.") + return True + + # Запасной вариант с устаревшим API + logger.info("First shutdown method failed, trying alternative API...") + result = self._make_api_request("SYNO.Core.System", "shutdown", version=3) + if result is not None: + logger.info("Successfully initiated system shutdown using SYNO.Core.System") + + # Даем системе время начать процесс выключения + logger.info("Waiting for shutdown to initialize...") + sleep(5) + + # Проверяем статус + if not self.is_online(force_check=True): + logger.info("System is now offline. Shutdown confirmed successful.") + return True + else: + logger.info("System still appears to be online, but shutdown may be in progress.") + return True + + # Если оба метода не сработали, но система стала недоступна + if not self.is_online(force_check=True): + logger.info("System appears to be shutting down despite API errors") + return True + + logger.error("Failed to shutdown system after trying multiple APIs") + return False + + def reboot_system(self) -> bool: + """Перезагрузка системы""" + # Проверяем, включена ли система + if not self.is_online(force_check=True): + logger.error("Cannot reboot: System is offline") + return False + + logger.info("Attempting to reboot Synology NAS...") + + # Правильный API-вызов для перезагрузки согласно официальной документации DSM API + result = self._make_api_request("SYNO.Core.System.Power", "restart", version=1) + + if result is not None: + logger.info("Successfully initiated system reboot using SYNO.Core.System.Power") + + # Даем системе время начать процесс перезагрузки + logger.info("Waiting for reboot to initialize...") + sleep(5) + + # Ждем, пока система станет недоступна (признак перезагрузки) + reboot_started = False + for i in range(12): # Проверяем в течение 60 секунд (12 * 5сек) + if not self.is_online(force_check=True): + logger.info(f"System went offline after {i*5} seconds, reboot in progress") + reboot_started = True + break + logger.debug(f"System still online, waiting... ({i+1}/12)") + sleep(5) + + if reboot_started: + # Ждем, пока система снова станет доступна + logger.info("Waiting for system to come back online...") + return self.wait_for_boot(max_attempts=30, delay=10) + else: + logger.warning("System did not go offline after reboot command") + return False + + # Запасной вариант, если первый метод не сработал + logger.info("First reboot method failed, trying alternative API...") + result = self._make_api_request("SYNO.Core.System", "reboot", version=3) + if result is not None: + logger.info("Successfully initiated system reboot using SYNO.Core.System") + + # Даем системе время начать процесс перезагрузки + logger.info("Waiting for reboot to initialize...") + sleep(5) + + # Ждем, пока система станет недоступна (признак перезагрузки) + reboot_started = False + for i in range(12): # Проверяем в течение 60 секунд (12 * 5сек) + if not self.is_online(force_check=True): + logger.info(f"System went offline after {i*5} seconds, reboot in progress") + reboot_started = True + break + logger.debug(f"System still online, waiting... ({i+1}/12)") + sleep(5) + + if reboot_started: + # Ждем, пока система снова станет доступна + logger.info("Waiting for system to come back online...") + return self.wait_for_boot(max_attempts=30, delay=10) + else: + logger.warning("System did not go offline after reboot command") + return False + + logger.error("Failed to reboot system after trying multiple APIs") + return False + + def _get_error_description(self, error_code: int) -> str: + """Получение описания ошибки по коду""" + error_descriptions = { + 100: "Unknown error", + 101: "Invalid parameter", + 102: "API does not exist", + 103: "Method does not exist", + 104: "Version does not support", + 105: "Permission denied", + 106: "Session timeout", + 107: "Session interrupted by duplicate login", + 400: "Invalid credentials", + 401: "Account disabled", + 402: "Permission denied", + 403: "2FA required", + 404: "Failed to authenticate with 2FA" + } + return error_descriptions.get(error_code, "Unknown error code") + + def _check_tcp_connection(self) -> bool: + """Проверка базового TCP-соединения с Synology NAS""" + try: + socket_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + socket_obj.settimeout(SYNOLOGY_TIMEOUT) + result = socket_obj.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT)) + socket_obj.close() + + return result == 0 + except socket.error as e: + logger.error(f"Socket error during connection check: {str(e)}") + return False + except Exception as e: + logger.error(f"Unexpected error during connection check: {str(e)}") + return False + + def is_online(self, force_check=False) -> bool: + """Проверка онлайн-статуса Synology NAS""" + # Используем кэшированное значение, если доступно и не устарело + current_time = time.time() + if not force_check and (current_time - self._last_online_check) < self._online_check_interval: + logger.debug(f"Using cached online status: {self._last_online_status}") + return self._last_online_status + + logger.info("Checking if NAS is online...") + + # Проверяем TCP-соединение + online_status = self._check_tcp_connection() + logger.info(f"Detected Synology NAS online status: {online_status}") + + # Если TCP-соединение успешно и у нас есть действующий SID, + # попробуем более детальную проверку через API + if online_status and self.sid: + logger.info("Trying to fetch more detailed online status through API...") + try: + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.DSM.Info", + "version": "1", + "method": "getinfo", + "sid": self.sid + } + + response = self.session.get( + url, + params=params, + timeout=self.default_timeout, + verify=False + ) + + if response.status_code == 200: + data = response.json() + if data.get("success"): + logger.info("API request successful for getinfo") + logger.info("Synology NAS is online with API access") + else: + logger.warning("API response indicates an error, but NAS is reachable") + logger.debug(f"API error: {data.get('error', {}).get('code', -1)}") + else: + logger.warning(f"API returned status code {response.status_code}") + except Exception as e: + logger.warning(f"API check failed, but TCP connection succeeded: {str(e)}") + + # Обновляем кэшированное значение + self._last_online_check = current_time + self._last_online_status = online_status + + return online_status + + def wake_on_lan(self) -> bool: + """Отправка Wake-on-LAN пакета для включения Synology NAS""" + if not SYNOLOGY_MAC: + logger.error("MAC address not configured") + return False + + try: + # Преобразование MAC-адреса в байты + mac_address = SYNOLOGY_MAC.replace(':', '').replace('-', '') + if len(mac_address) != 12: + logger.error(f"Invalid MAC address format: {SYNOLOGY_MAC}") + return False + + try: + mac_bytes = bytes.fromhex(mac_address) + except ValueError as e: + logger.error(f"Failed to parse MAC address: {str(e)}") + logger.error(f"MAC address must contain only hex characters: {SYNOLOGY_MAC}") + return False + + # Создание Magic Packet + magic_packet = b'\xff' * 6 + mac_bytes * 16 + + # Отправка пакета на конкретный адрес + try: + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) + sock.sendto(magic_packet, (SYNOLOGY_HOST, WOL_PORT)) + sock.close() + logger.info(f"Wake-on-LAN packet sent to {SYNOLOGY_MAC} at {SYNOLOGY_HOST}:{WOL_PORT}") + except Exception as e: + logger.error(f"Error sending directed WoL packet: {str(e)}") + return False + + # Для надежности отправляем также широковещательный пакет + try: + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) + + # Используем стандартный широковещательный адрес + broadcast_addr = "255.255.255.255" + sock.sendto(magic_packet, (broadcast_addr, WOL_PORT)) + sock.close() + logger.info(f"Wake-on-LAN broadcast packet sent to {broadcast_addr}:{WOL_PORT}") + except Exception as e: + logger.warning(f"Error sending broadcast WoL packet (non-critical): {str(e)}") + # Не считаем ошибкой, т.к. основной пакет уже отправлен + + return True + + except Exception as e: + logger.error(f"Unexpected error in wake_on_lan: {str(e)}") + return False + + def wait_for_boot(self, max_attempts: int = 30, delay: int = 10) -> bool: + """Ожидание загрузки Synology NAS""" + logger.info(f"Waiting for Synology NAS to boot (timeout: {max_attempts * delay}s)...") + + for attempt in range(max_attempts): + # Принудительно проверяем статус без использования кэша + if self.is_online(force_check=True): + logger.info(f"Synology NAS is online after {attempt + 1} attempts ({(attempt + 1) * delay}s)") + + # Проверяем, что не только сеть доступна, но и API загрузился + api_ready = False + logger.info("Waiting for API services to initialize...") + + for api_check in range(5): # Даем еще до 50 секунд для загрузки API + if self.sid or self.login(): + api_ready = True + logger.info(f"API services are ready after {api_check + 1} attempts") + break + logger.debug(f"API not ready yet, waiting... ({api_check + 1}/5)") + sleep(10) + + if not api_ready: + logger.warning("System is online but API services may not be fully initialized") + + # Дадим дополнительное время для полной загрузки всех сервисов + sleep(delay) + return True + + sleep(delay) + logger.info(f"Waiting for boot... Attempt {attempt + 1}/{max_attempts}") + + logger.error(f"Synology NAS didn't come online after {max_attempts * delay} seconds") + return False + + def power_on(self) -> bool: + """Включение Synology NAS""" + # Принудительная проверка статуса + if self.is_online(force_check=True): + logger.info("Synology NAS is already online") + return True + + logger.info("Powering on Synology NAS via Wake-on-LAN...") + + # Проверяем, настроен ли MAC-адрес + if not SYNOLOGY_MAC: + logger.error("Cannot power on: MAC address not configured in settings") + return False + + # Пробуем отправить несколько WoL пакетов для надежности + success = False + for attempt in range(3): + logger.debug(f"Sending WoL packet, attempt {attempt + 1}/3") + if self.wake_on_lan(): + success = True + break + sleep(1) + + if not success: + logger.error("Failed to send Wake-on-LAN packets") + return False + + # Ожидание загрузки + logger.info("WoL packets sent successfully, waiting for system to boot...") + boot_result = self.wait_for_boot(max_attempts=30, delay=10) + + if boot_result: + # Проверяем доступность API после загрузки + system_status = self.get_system_status() + if system_status.get("status") == "online": + logger.info("System booted successfully with API access") + return True + else: + logger.warning("System appears to be online but API may not be fully ready") + return True + else: + logger.error("System did not come online after WoL") + return False + + def power_off(self) -> bool: + """Выключение Synology NAS""" + if not self.is_online(force_check=True): + logger.info("Synology NAS is already offline") + return True + + logger.info("Powering off Synology NAS...") + result = self.shutdown_system() + + if result: + # Дополнительная проверка, что система действительно выключилась + logger.info("Verifying system is offline...") + for attempt in range(12): # Проверяем в течение 2 минут (12 * 10 сек) + if not self.is_online(force_check=True): + logger.info(f"System confirmed offline after {attempt * 10} seconds") + return True + logger.debug(f"System still shutting down, checking again... ({attempt + 1}/12)") + sleep(10) + + logger.warning("System still appears to be online after shutdown command") + return False + else: + logger.error("Failed to initiate shutdown") + return False + + # Заглушки для расширенных методов + def get_shared_folders(self) -> List[Dict[str, Any]]: + """Получение списка общих папок (заглушка)""" + logger.warning("Function get_shared_folders() is not implemented yet") + return [] + + def get_system_load(self) -> Dict[str, Any]: + """Получение информации о загрузке системы (заглушка)""" + logger.warning("Function get_system_load() is not implemented yet") + return {} + + def get_storage_status(self) -> Dict[str, Any]: + """Получение подробной информации о хранилище (заглушка)""" + logger.warning("Function get_storage_status() is not implemented yet") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for storage status request") + return {"error": "authentication_failed"} + + try: + # Здесь будет реализация после добавления соответствующего API + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.Storage.CGI.Storage", + "version": "1", + "method": "load_info", + "sid": self.sid + } + + # В текущей версии просто возвращаем заглушку + return { + "volumes": [], + "disks": [], + "total_size": 0, + "total_used": 0, + "error": "not_implemented" + } + except Exception as e: + logger.error(f"Error in get_storage_status: {str(e)}") + return {"error": str(e)} + + def get_security_status(self) -> Dict[str, Any]: + """Получение информации о состоянии безопасности (заглушка)""" + logger.warning("Function get_security_status() is not implemented yet") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for security status request") + return {"success": False, "error": "authentication_failed"} + + try: + # Здесь будет реализация после добавления соответствующего API + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.Core.SecurityScan.Status", + "version": "1", + "method": "get", + "sid": self.sid + } + + # В текущей версии просто возвращаем заглушку + return { + "success": False, + "status": "not_implemented", + "last_check": None, + "is_secure": False, + "error": "not_implemented" + } + except Exception as e: + logger.error(f"Error in get_security_status: {str(e)}") + return {"success": False, "error": str(e)} diff --git a/.history/src/api/synology_20250830080858.py b/.history/src/api/synology_20250830080858.py new file mode 100644 index 0000000..416f33a --- /dev/null +++ b/.history/src/api/synology_20250830080858.py @@ -0,0 +1,866 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Модуль для взаимодействия с API Synology NAS +""" + +import requests +from requests.adapters import HTTPAdapter +import json +import logging +import time +import urllib3 +from urllib3.util import Retry +from typing import Dict, Any, Optional, List +import socket +import struct +from time import sleep + +from src.config.config import ( + SYNOLOGY_HOST, + SYNOLOGY_PORT, + SYNOLOGY_USERNAME, + SYNOLOGY_PASSWORD, + SYNOLOGY_SECURE, + SYNOLOGY_TIMEOUT, + SYNOLOGY_MAC, + WOL_PORT +) + +# Отключение предупреждений о небезопасных SSL-соединениях +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + +logger = logging.getLogger(__name__) + +class SynologyAPI: + """Класс для взаимодействия с API Synology NAS""" + + def __init__(self): + """Инициализация класса SynologyAPI""" + logger.info("Creating API with auto-retry and connection pool") + logger.debug(f"Connection details: Host={SYNOLOGY_HOST}, Port={SYNOLOGY_PORT}, Secure={SYNOLOGY_SECURE}") + + self.protocol = "https" if SYNOLOGY_SECURE else "http" + self.base_url = f"{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi" + self.sid = None + self.session = requests.Session() + + # Настройка SSL + if self.protocol == "https": + logger.debug("SSL enabled, disabling certificate verification for internal network") + self.session.verify = False # Отключаем проверку SSL для внутренней сети + + # Добавляем повторные попытки для HTTP-запросов + retry_strategy = Retry( + total=5, # Увеличиваем количество попыток + status_forcelist=[429, 500, 502, 503, 504, 404], + allowed_methods=["GET", "POST"], + backoff_factor=1.5, # Увеличиваем задержку между попытками + respect_retry_after_header=True + ) + adapter = HTTPAdapter( + max_retries=retry_strategy, + pool_connections=3, + pool_maxsize=10 + ) + self.session.mount("http://", adapter) + self.session.mount("https://", adapter) + + # Таймауты будут указаны в запросах + self.default_timeout = (SYNOLOGY_TIMEOUT, SYNOLOGY_TIMEOUT*2) + logger.debug(f"Setting default request timeout: {self.default_timeout}") + + # Кэш для хранения результатов запросов + self._cache = {} + self._cache_ttl = {} + self._last_online_check = 0 + self._last_online_status = False + self._online_check_interval = 30 # Интервал для кэширования проверок онлайн-статуса + + # Время последней успешной аутентификации и срок действия сессии + self._last_auth_time = 0 + self._auth_expiry = 3600 # По умолчанию 1 час + + def login(self) -> bool: + """Авторизация в API Synology NAS""" + # Проверяем, действительна ли текущая сессия + current_time = time.time() + if self.sid and (current_time - self._last_auth_time) < self._auth_expiry: + logger.debug("Using existing session, still valid") + return True + + logger.info("Attempting to authenticate with Synology NAS...") + logger.debug(f"Base URL: {self.base_url}") + + # Проверяем доступность NAS перед запросом с помощью прямой TCP-проверки + # Избегаем вызова is_online(), чтобы не создавать рекурсию + online_status = self._check_tcp_connection() + if not online_status: + logger.error("Cannot login: Synology NAS is not reachable") + return False + + try: + # Проверка информации API для определения доступных версий API + api_info_url = f"{self.base_url}/entry.cgi" + api_info_params = { + "api": "SYNO.API.Info", + "version": "1", + "method": "query", + "query": "SYNO.API.Auth" + } + + logger.debug("Querying API info to determine optimal auth version") + try: + api_info_response = self.session.get( + api_info_url, + params=api_info_params, + timeout=self.default_timeout, + verify=False + ) + + if api_info_response.status_code == 200: + api_info_data = api_info_response.json() + if api_info_data.get("success"): + auth_info = api_info_data.get("data", {}).get("SYNO.API.Auth", {}) + max_version = auth_info.get("maxVersion", 3) + min_version = auth_info.get("minVersion", 1) + auth_path = auth_info.get("path", "auth.cgi") + logger.debug(f"Auth API versions: min={min_version}, max={max_version}, path={auth_path}") + + # Используем максимально поддерживаемую версию, но не выше 6 + auth_version = min(max_version, 6) + else: + logger.warning("Failed to query API info, using default auth version 3") + auth_version = 3 + auth_path = "auth.cgi" + else: + logger.warning(f"API info request failed with status {api_info_response.status_code}, using default auth version 3") + auth_version = 3 + auth_path = "auth.cgi" + except Exception as e: + logger.warning(f"Error querying API info: {str(e)}, using default auth version 3") + auth_version = 3 + auth_path = "auth.cgi" + + # Основной запрос авторизации + url = f"{self.base_url}/{auth_path}" + params = { + "api": "SYNO.API.Auth", + "version": str(auth_version), + "method": "login", + "account": SYNOLOGY_USERNAME, + "passwd": SYNOLOGY_PASSWORD, + "session": "SynologyPowerControlBot", + "format": "cookie" + } + + # Для версии 6+ используем немного другой формат + if auth_version >= 6: + params["enable_syno_token"] = "yes" + + logger.debug(f"Sending auth request to {url} with API version {auth_version}") + start_time = time.time() + response = self.session.get( + url, + params=params, + timeout=self.default_timeout, + verify=False + ) + elapsed_time = time.time() - start_time + logger.debug(f"Auth request completed in {elapsed_time:.2f}s with status code {response.status_code}") + + # Подробная информация для отладки + logger.debug(f"Auth response: {response.text[:500]}") + + # Проверка статуса HTTP + if response.status_code != 200: + logger.error(f"HTTP error: {response.status_code}") + logger.debug(f"Response content: {response.text[:200]}") + return False + + try: + data = response.json() + except json.JSONDecodeError: + logger.error("Failed to decode JSON response") + logger.debug(f"Response content: {response.text[:200]}") + return False + + if data.get("success"): + self.sid = data.get("data", {}).get("sid") + self._last_auth_time = current_time + logger.info("Successfully logged in to Synology NAS") + logger.debug(f"Session ID obtained: {self.sid[:5] if self.sid else 'None'}...") # Показываем только начало SID для безопасности + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to log in to Synology NAS: Error code {error_code}") + + # Дополнительная диагностика + if error_code == 400: + logger.error("Authentication error: Invalid credentials") + elif error_code == 401: + logger.error("Authentication error: Account disabled") + elif error_code == 402: + logger.error("Authentication error: Permission denied") + elif error_code == 403: + logger.error("Authentication error: 2-factor authentication required") + elif error_code == 404: + logger.error("Authentication error: Failed to authenticate with 2-factor authentication") + + return False + + except requests.exceptions.Timeout: + logger.error(f"Connection timeout after {SYNOLOGY_TIMEOUT} seconds") + return False + except requests.exceptions.ConnectionError as e: + logger.error(f"Connection error: {str(e)}") + logger.debug(f"Connection details: {self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}") + return False + except requests.RequestException as e: + logger.error(f"Request error: {str(e)}") + return False + except Exception as e: + logger.error(f"Unexpected error during login: {str(e)}", exc_info=True) + return False + + def logout(self) -> bool: + """Выход из API Synology NAS""" + if not self.sid: + return True + + try: + url = f"{self.base_url}/auth.cgi" + params = { + "api": "SYNO.API.Auth", + "version": "1", + "method": "logout", + "session": "SynologyPowerControlBot", + "_sid": self.sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + self.sid = None + logger.info("Successfully logged out from Synology NAS") + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to log out from Synology NAS: Error code {error_code}") + return False + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + return False + + def _make_api_request(self, api_name: str, method: str, version: int = 1, + params: Optional[Dict[str, Any]] = None, retry_auth: bool = True) -> Optional[Dict[str, Any]]: + """Обобщенный метод для выполнения API запросов с обработкой ошибок""" + # Проверка наличия авторизации + if not self.sid and not self.login(): + logger.error(f"Not authenticated for API request: {api_name}.{method}") + return None + + # Проверка информации API для определения пути и поддерживаемой версии + api_info_url = f"{self.base_url}/entry.cgi" + api_info_params = { + "api": "SYNO.API.Info", + "version": "1", + "method": "query", + "query": api_name + } + + api_path = "entry.cgi" + try: + logger.debug(f"Querying API info for {api_name}") + api_info_response = self.session.get( + api_info_url, + params=api_info_params, + timeout=self.default_timeout, + verify=False + ) + + if api_info_response.status_code == 200: + api_info_data = api_info_response.json() + if api_info_data.get("success"): + api_info = api_info_data.get("data", {}).get(api_name, {}) + if api_info: + max_version = api_info.get("maxVersion", version) + min_version = api_info.get("minVersion", version) + api_path = api_info.get("path", "entry.cgi") + + # Проверка, поддерживается ли запрошенная версия + if version < min_version: + logger.warning(f"API version {version} for {api_name} is below minimum {min_version}, using {min_version}") + version = min_version + elif version > max_version: + logger.warning(f"API version {version} for {api_name} exceeds maximum {max_version}, using {max_version}") + version = max_version + + logger.debug(f"Using API path: {api_path}, version: {version}") + else: + logger.warning(f"API {api_name} not found in API info, using defaults") + except Exception as e: + logger.warning(f"Error querying API info for {api_name}: {str(e)}, using defaults") + + # Подготовка базовых параметров запроса + base_params = { + "api": api_name, + "version": str(version), + "method": method, + "sid": self.sid + } + + # Добавление дополнительных параметров, если они заданы + if params: + base_params.update(params) + + url = f"{self.base_url}/{api_path}" + logger.debug(f"API request: {api_name}.{method} v{version} to {url}") + logger.debug(f"Full request params: {base_params}") + + try: + start_time = time.time() + response = self.session.get( + url, + params=base_params, + timeout=self.default_timeout, + verify=False + ) + elapsed_time = time.time() - start_time + logger.debug(f"API request completed in {elapsed_time:.2f}s with status {response.status_code}") + + # Подробное логирование для отладки + logger.debug(f"Response: {response.text[:500]}") + + # Проверка статуса HTTP + if response.status_code != 200: + logger.error(f"HTTP error: {response.status_code} for {api_name}.{method}") + return None + + try: + data = response.json() + except json.JSONDecodeError: + logger.error(f"Failed to decode JSON response for {api_name}.{method}") + logger.debug(f"Response content: {response.text[:200]}") + return None + + if data.get("success"): + logger.info(f"API request successful for {api_name}.{method}") + return data.get("data", {}) + else: + error_code = data.get("error", {}).get("code", -1) + error_desc = self._get_error_description(error_code) + logger.error(f"API error for {api_name}.{method}: {error_code} - {error_desc}") + + # Ошибки доступа или прав часто встречаются, но они не критичные + # Например, ошибка 102 означает, что нет прав, но NAS доступен + if error_code in [102, 103, 104, 105]: + logger.warning(f"Permission-related error for {api_name}.{method}: {error_code}") + # Возвращаем пустой словарь вместо None, + # чтобы вызывающий код мог понять, что запрос выполнен + return {} + + # Если ошибка связана с авторизацией и нам разрешено повторить попытку + if error_code in [106, 107, 119] and retry_auth: + logger.info("Session may have expired, re-authenticating...") + self.sid = None # Сбрасываем SID + if self.login(): + logger.info("Re-authenticated, retrying API request...") + # Рекурсивный вызов, но без повторной авторизации во избежание зацикливания + return self._make_api_request(api_name, method, version, params, False) + + return None + + except requests.exceptions.Timeout: + logger.error(f"Request timeout for {api_name}.{method}") + return None + except requests.exceptions.ConnectionError as e: + logger.error(f"Connection error for {api_name}.{method}: {str(e)}") + return None + except Exception as e: + logger.error(f"Unexpected error during {api_name}.{method}: {str(e)}") + return None + + def get_system_status(self) -> Dict[str, Any]: + """Получение статуса системы""" + # Проверяем доступность системы + if not self.is_online(): + logger.info("Device is offline, skipping API request") + return {"status": "offline"} + + # Проверяем, есть ли кэшированный результат + cache_key = "system_status" + current_time = time.time() + if cache_key in self._cache and current_time - self._cache_ttl.get(cache_key, 0) < 60: + logger.debug("Using cached system status") + return self._cache[cache_key] + + # Делаем API запрос + result = self._make_api_request("SYNO.DSM.Info", "getinfo") + + if result: + # Формируем расширенный ответ с дополнительной информацией + system_info = { + "status": "online", + "hostname": result.get("hostname", "unknown"), + "model": result.get("model", "unknown"), + "version": result.get("version", "unknown"), + "uptime": result.get("uptime", 0), + "time": current_time, + "is_online": True + } + + # Сохраняем в кэше + self._cache[cache_key] = system_info + self._cache_ttl[cache_key] = current_time + + return system_info + else: + # Если запрос не удался, но система онлайн + return { + "status": "error", + "error": "Failed to fetch system information", + "is_online": True, + "time": current_time + } + + def shutdown_system(self) -> bool: + """Выключение системы""" + # Проверяем, включено ли устройство перед попыткой его выключить + if not self.is_online(force_check=True): + logger.info("Synology NAS is already offline, no need to shut down") + return True + + logger.info("Attempting to shutdown Synology NAS...") + + # Правильный API-вызов для выключения согласно официальной документации DSM API + result = self._make_api_request("SYNO.Core.System.Power", "shutdown", version=1) + if result is not None: + logger.info("Successfully initiated system shutdown using SYNO.Core.System.Power") + + # Даем системе время начать процесс выключения + logger.info("Waiting for shutdown to initialize...") + sleep(5) + + # Проверяем статус + if not self.is_online(force_check=True): + logger.info("System is now offline. Shutdown confirmed successful.") + return True + else: + logger.info("System still appears to be online, but shutdown may be in progress.") + return True + + # Запасной вариант с устаревшим API + logger.info("First shutdown method failed, trying alternative API...") + result = self._make_api_request("SYNO.Core.System", "shutdown", version=3) + if result is not None: + logger.info("Successfully initiated system shutdown using SYNO.Core.System") + + # Даем системе время начать процесс выключения + logger.info("Waiting for shutdown to initialize...") + sleep(5) + + # Проверяем статус + if not self.is_online(force_check=True): + logger.info("System is now offline. Shutdown confirmed successful.") + return True + else: + logger.info("System still appears to be online, but shutdown may be in progress.") + return True + + # Если оба метода не сработали, но система стала недоступна + if not self.is_online(force_check=True): + logger.info("System appears to be shutting down despite API errors") + return True + + logger.error("Failed to shutdown system after trying multiple APIs") + return False + + def reboot_system(self) -> bool: + """Перезагрузка системы""" + # Проверяем, включена ли система + if not self.is_online(force_check=True): + logger.error("Cannot reboot: System is offline") + return False + + logger.info("Attempting to reboot Synology NAS...") + + # Правильный API-вызов для перезагрузки согласно официальной документации DSM API + result = self._make_api_request("SYNO.Core.System.Power", "restart", version=1) + + if result is not None: + logger.info("Successfully initiated system reboot using SYNO.Core.System.Power") + + # Даем системе время начать процесс перезагрузки + logger.info("Waiting for reboot to initialize...") + sleep(5) + + # Ждем, пока система станет недоступна (признак перезагрузки) + reboot_started = False + for i in range(12): # Проверяем в течение 60 секунд (12 * 5сек) + if not self.is_online(force_check=True): + logger.info(f"System went offline after {i*5} seconds, reboot in progress") + reboot_started = True + break + logger.debug(f"System still online, waiting... ({i+1}/12)") + sleep(5) + + if reboot_started: + # Ждем, пока система снова станет доступна + logger.info("Waiting for system to come back online...") + return self.wait_for_boot(max_attempts=30, delay=10) + else: + logger.warning("System did not go offline after reboot command") + return False + + # Запасной вариант, если первый метод не сработал + logger.info("First reboot method failed, trying alternative API...") + result = self._make_api_request("SYNO.Core.System", "reboot", version=3) + if result is not None: + logger.info("Successfully initiated system reboot using SYNO.Core.System") + + # Даем системе время начать процесс перезагрузки + logger.info("Waiting for reboot to initialize...") + sleep(5) + + # Ждем, пока система станет недоступна (признак перезагрузки) + reboot_started = False + for i in range(12): # Проверяем в течение 60 секунд (12 * 5сек) + if not self.is_online(force_check=True): + logger.info(f"System went offline after {i*5} seconds, reboot in progress") + reboot_started = True + break + logger.debug(f"System still online, waiting... ({i+1}/12)") + sleep(5) + + if reboot_started: + # Ждем, пока система снова станет доступна + logger.info("Waiting for system to come back online...") + return self.wait_for_boot(max_attempts=30, delay=10) + else: + logger.warning("System did not go offline after reboot command") + return False + + logger.error("Failed to reboot system after trying multiple APIs") + return False + + def _get_error_description(self, error_code: int) -> str: + """Получение описания ошибки по коду""" + error_descriptions = { + 100: "Unknown error", + 101: "Invalid parameter", + 102: "API does not exist", + 103: "Method does not exist", + 104: "Version does not support", + 105: "Permission denied", + 106: "Session timeout", + 107: "Session interrupted by duplicate login", + 400: "Invalid credentials", + 401: "Account disabled", + 402: "Permission denied", + 403: "2FA required", + 404: "Failed to authenticate with 2FA" + } + return error_descriptions.get(error_code, "Unknown error code") + + def _check_tcp_connection(self) -> bool: + """Проверка базового TCP-соединения с Synology NAS""" + try: + socket_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + socket_obj.settimeout(SYNOLOGY_TIMEOUT) + result = socket_obj.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT)) + socket_obj.close() + + return result == 0 + except socket.error as e: + logger.error(f"Socket error during connection check: {str(e)}") + return False + except Exception as e: + logger.error(f"Unexpected error during connection check: {str(e)}") + return False + + def is_online(self, force_check=False) -> bool: + """Проверка онлайн-статуса Synology NAS""" + # Используем кэшированное значение, если доступно и не устарело + current_time = time.time() + if not force_check and (current_time - self._last_online_check) < self._online_check_interval: + logger.debug(f"Using cached online status: {self._last_online_status}") + return self._last_online_status + + logger.info("Checking if NAS is online...") + + # Проверяем TCP-соединение + online_status = self._check_tcp_connection() + logger.info(f"Detected Synology NAS online status: {online_status}") + + # Если TCP-соединение успешно и у нас есть действующий SID, + # попробуем более детальную проверку через API + if online_status and self.sid: + logger.info("Trying to fetch more detailed online status through API...") + try: + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.DSM.Info", + "version": "1", + "method": "getinfo", + "sid": self.sid + } + + response = self.session.get( + url, + params=params, + timeout=self.default_timeout, + verify=False + ) + + if response.status_code == 200: + data = response.json() + if data.get("success"): + logger.info("API request successful for getinfo") + logger.info("Synology NAS is online with API access") + else: + logger.warning("API response indicates an error, but NAS is reachable") + logger.debug(f"API error: {data.get('error', {}).get('code', -1)}") + else: + logger.warning(f"API returned status code {response.status_code}") + except Exception as e: + logger.warning(f"API check failed, but TCP connection succeeded: {str(e)}") + + # Обновляем кэшированное значение + self._last_online_check = current_time + self._last_online_status = online_status + + return online_status + + def wake_on_lan(self) -> bool: + """Отправка Wake-on-LAN пакета для включения Synology NAS""" + if not SYNOLOGY_MAC: + logger.error("MAC address not configured") + return False + + try: + # Преобразование MAC-адреса в байты + mac_address = SYNOLOGY_MAC.replace(':', '').replace('-', '') + if len(mac_address) != 12: + logger.error(f"Invalid MAC address format: {SYNOLOGY_MAC}") + return False + + try: + mac_bytes = bytes.fromhex(mac_address) + except ValueError as e: + logger.error(f"Failed to parse MAC address: {str(e)}") + logger.error(f"MAC address must contain only hex characters: {SYNOLOGY_MAC}") + return False + + # Создание Magic Packet + magic_packet = b'\xff' * 6 + mac_bytes * 16 + + # Отправка пакета на конкретный адрес + try: + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) + sock.sendto(magic_packet, (SYNOLOGY_HOST, WOL_PORT)) + sock.close() + logger.info(f"Wake-on-LAN packet sent to {SYNOLOGY_MAC} at {SYNOLOGY_HOST}:{WOL_PORT}") + except Exception as e: + logger.error(f"Error sending directed WoL packet: {str(e)}") + return False + + # Для надежности отправляем также широковещательный пакет + try: + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) + + # Используем стандартный широковещательный адрес + broadcast_addr = "255.255.255.255" + sock.sendto(magic_packet, (broadcast_addr, WOL_PORT)) + sock.close() + logger.info(f"Wake-on-LAN broadcast packet sent to {broadcast_addr}:{WOL_PORT}") + except Exception as e: + logger.warning(f"Error sending broadcast WoL packet (non-critical): {str(e)}") + # Не считаем ошибкой, т.к. основной пакет уже отправлен + + return True + + except Exception as e: + logger.error(f"Unexpected error in wake_on_lan: {str(e)}") + return False + + def wait_for_boot(self, max_attempts: int = 30, delay: int = 10) -> bool: + """Ожидание загрузки Synology NAS""" + logger.info(f"Waiting for Synology NAS to boot (timeout: {max_attempts * delay}s)...") + + for attempt in range(max_attempts): + # Принудительно проверяем статус без использования кэша + if self.is_online(force_check=True): + logger.info(f"Synology NAS is online after {attempt + 1} attempts ({(attempt + 1) * delay}s)") + + # Проверяем, что не только сеть доступна, но и API загрузился + api_ready = False + logger.info("Waiting for API services to initialize...") + + for api_check in range(5): # Даем еще до 50 секунд для загрузки API + if self.sid or self.login(): + api_ready = True + logger.info(f"API services are ready after {api_check + 1} attempts") + break + logger.debug(f"API not ready yet, waiting... ({api_check + 1}/5)") + sleep(10) + + if not api_ready: + logger.warning("System is online but API services may not be fully initialized") + + # Дадим дополнительное время для полной загрузки всех сервисов + sleep(delay) + return True + + sleep(delay) + logger.info(f"Waiting for boot... Attempt {attempt + 1}/{max_attempts}") + + logger.error(f"Synology NAS didn't come online after {max_attempts * delay} seconds") + return False + + def power_on(self) -> bool: + """Включение Synology NAS""" + # Принудительная проверка статуса + if self.is_online(force_check=True): + logger.info("Synology NAS is already online") + return True + + logger.info("Powering on Synology NAS via Wake-on-LAN...") + + # Проверяем, настроен ли MAC-адрес + if not SYNOLOGY_MAC: + logger.error("Cannot power on: MAC address not configured in settings") + return False + + # Пробуем отправить несколько WoL пакетов для надежности + success = False + for attempt in range(3): + logger.debug(f"Sending WoL packet, attempt {attempt + 1}/3") + if self.wake_on_lan(): + success = True + break + sleep(1) + + if not success: + logger.error("Failed to send Wake-on-LAN packets") + return False + + # Ожидание загрузки + logger.info("WoL packets sent successfully, waiting for system to boot...") + boot_result = self.wait_for_boot(max_attempts=30, delay=10) + + if boot_result: + # Проверяем доступность API после загрузки + system_status = self.get_system_status() + if system_status.get("status") == "online": + logger.info("System booted successfully with API access") + return True + else: + logger.warning("System appears to be online but API may not be fully ready") + return True + else: + logger.error("System did not come online after WoL") + return False + + def power_off(self) -> bool: + """Выключение Synology NAS""" + if not self.is_online(force_check=True): + logger.info("Synology NAS is already offline") + return True + + logger.info("Powering off Synology NAS...") + result = self.shutdown_system() + + if result: + # Дополнительная проверка, что система действительно выключилась + logger.info("Verifying system is offline...") + for attempt in range(12): # Проверяем в течение 2 минут (12 * 10 сек) + if not self.is_online(force_check=True): + logger.info(f"System confirmed offline after {attempt * 10} seconds") + return True + logger.debug(f"System still shutting down, checking again... ({attempt + 1}/12)") + sleep(10) + + logger.warning("System still appears to be online after shutdown command") + return False + else: + logger.error("Failed to initiate shutdown") + return False + + # Заглушки для расширенных методов + def get_shared_folders(self) -> List[Dict[str, Any]]: + """Получение списка общих папок (заглушка)""" + logger.warning("Function get_shared_folders() is not implemented yet") + return [] + + def get_system_load(self) -> Dict[str, Any]: + """Получение информации о загрузке системы (заглушка)""" + logger.warning("Function get_system_load() is not implemented yet") + return {} + + def get_storage_status(self) -> Dict[str, Any]: + """Получение подробной информации о хранилище (заглушка)""" + logger.warning("Function get_storage_status() is not implemented yet") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for storage status request") + return {"error": "authentication_failed"} + + try: + # Здесь будет реализация после добавления соответствующего API + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.Storage.CGI.Storage", + "version": "1", + "method": "load_info", + "sid": self.sid + } + + # В текущей версии просто возвращаем заглушку + return { + "volumes": [], + "disks": [], + "total_size": 0, + "total_used": 0, + "error": "not_implemented" + } + except Exception as e: + logger.error(f"Error in get_storage_status: {str(e)}") + return {"error": str(e)} + + def get_security_status(self) -> Dict[str, Any]: + """Получение информации о состоянии безопасности (заглушка)""" + logger.warning("Function get_security_status() is not implemented yet") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for security status request") + return {"success": False, "error": "authentication_failed"} + + try: + # Здесь будет реализация после добавления соответствующего API + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.Core.SecurityScan.Status", + "version": "1", + "method": "get", + "sid": self.sid + } + + # В текущей версии просто возвращаем заглушку + return { + "success": False, + "status": "not_implemented", + "last_check": None, + "is_secure": False, + "error": "not_implemented" + } + except Exception as e: + logger.error(f"Error in get_security_status: {str(e)}") + return {"success": False, "error": str(e)} diff --git a/.history/src/api/synology_20250830081426.py b/.history/src/api/synology_20250830081426.py new file mode 100644 index 0000000..d6e595b --- /dev/null +++ b/.history/src/api/synology_20250830081426.py @@ -0,0 +1,910 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Модуль для взаимодействия с API Synology NAS +""" + +import requests +from requests.adapters import HTTPAdapter +import json +import logging +import time +import urllib3 +from urllib3.util import Retry +from typing import Dict, Any, Optional, List +import socket +import struct +from time import sleep + +from src.config.config import ( + SYNOLOGY_HOST, + SYNOLOGY_PORT, + SYNOLOGY_USERNAME, + SYNOLOGY_PASSWORD, + SYNOLOGY_SECURE, + SYNOLOGY_TIMEOUT, + SYNOLOGY_MAC, + WOL_PORT +) + +# Отключение предупреждений о небезопасных SSL-соединениях +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + +logger = logging.getLogger(__name__) + +class SynologyAPI: + """Класс для взаимодействия с API Synology NAS""" + + def __init__(self): + """Инициализация класса SynologyAPI""" + logger.info("Creating API with auto-retry and connection pool") + logger.debug(f"Connection details: Host={SYNOLOGY_HOST}, Port={SYNOLOGY_PORT}, Secure={SYNOLOGY_SECURE}") + + self.protocol = "https" if SYNOLOGY_SECURE else "http" + self.base_url = f"{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi" + self.sid = None + self.session = requests.Session() + + # Настройка SSL + if self.protocol == "https": + logger.debug("SSL enabled, disabling certificate verification for internal network") + self.session.verify = False # Отключаем проверку SSL для внутренней сети + + # Добавляем повторные попытки для HTTP-запросов + retry_strategy = Retry( + total=5, # Увеличиваем количество попыток + status_forcelist=[429, 500, 502, 503, 504, 404], + allowed_methods=["GET", "POST"], + backoff_factor=1.5, # Увеличиваем задержку между попытками + respect_retry_after_header=True + ) + adapter = HTTPAdapter( + max_retries=retry_strategy, + pool_connections=3, + pool_maxsize=10 + ) + self.session.mount("http://", adapter) + self.session.mount("https://", adapter) + + # Таймауты будут указаны в запросах + self.default_timeout = (SYNOLOGY_TIMEOUT, SYNOLOGY_TIMEOUT*2) + logger.debug(f"Setting default request timeout: {self.default_timeout}") + + # Кэш для хранения результатов запросов + self._cache = {} + self._cache_ttl = {} + self._last_online_check = 0 + self._last_online_status = False + self._online_check_interval = 30 # Интервал для кэширования проверок онлайн-статуса + + # Время последней успешной аутентификации и срок действия сессии + self._last_auth_time = 0 + self._auth_expiry = 3600 # По умолчанию 1 час + + def login(self) -> bool: + """Авторизация в API Synology NAS""" + # Проверяем, действительна ли текущая сессия + current_time = time.time() + if self.sid and (current_time - self._last_auth_time) < self._auth_expiry: + logger.debug("Using existing session, still valid") + return True + + logger.info("Attempting to authenticate with Synology NAS...") + logger.debug(f"Base URL: {self.base_url}") + + # Проверяем доступность NAS перед запросом с помощью прямой TCP-проверки + # Избегаем вызова is_online(), чтобы не создавать рекурсию + online_status = self._check_tcp_connection() + if not online_status: + logger.error("Cannot login: Synology NAS is not reachable") + return False + + try: + # Проверка информации API для определения доступных версий API + api_info_url = f"{self.base_url}/entry.cgi" + api_info_params = { + "api": "SYNO.API.Info", + "version": "1", + "method": "query", + "query": "SYNO.API.Auth" + } + + logger.debug("Querying API info to determine optimal auth version") + try: + api_info_response = self.session.get( + api_info_url, + params=api_info_params, + timeout=self.default_timeout, + verify=False + ) + + if api_info_response.status_code == 200: + api_info_data = api_info_response.json() + if api_info_data.get("success"): + auth_info = api_info_data.get("data", {}).get("SYNO.API.Auth", {}) + max_version = auth_info.get("maxVersion", 3) + min_version = auth_info.get("minVersion", 1) + auth_path = auth_info.get("path", "auth.cgi") + logger.debug(f"Auth API versions: min={min_version}, max={max_version}, path={auth_path}") + + # Используем максимально поддерживаемую версию, но не выше 6 + auth_version = min(max_version, 6) + else: + logger.warning("Failed to query API info, using default auth version 3") + auth_version = 3 + auth_path = "auth.cgi" + else: + logger.warning(f"API info request failed with status {api_info_response.status_code}, using default auth version 3") + auth_version = 3 + auth_path = "auth.cgi" + except Exception as e: + logger.warning(f"Error querying API info: {str(e)}, using default auth version 3") + auth_version = 3 + auth_path = "auth.cgi" + + # Основной запрос авторизации + url = f"{self.base_url}/{auth_path}" + params = { + "api": "SYNO.API.Auth", + "version": str(auth_version), + "method": "login", + "account": SYNOLOGY_USERNAME, + "passwd": SYNOLOGY_PASSWORD, + "session": "SynologyPowerControlBot", + "format": "cookie" + } + + # Для версии 6+ используем немного другой формат + if auth_version >= 6: + params["enable_syno_token"] = "yes" + + logger.debug(f"Sending auth request to {url} with API version {auth_version}") + start_time = time.time() + response = self.session.get( + url, + params=params, + timeout=self.default_timeout, + verify=False + ) + elapsed_time = time.time() - start_time + logger.debug(f"Auth request completed in {elapsed_time:.2f}s with status code {response.status_code}") + + # Подробная информация для отладки + logger.debug(f"Auth response: {response.text[:500]}") + + # Проверка статуса HTTP + if response.status_code != 200: + logger.error(f"HTTP error: {response.status_code}") + logger.debug(f"Response content: {response.text[:200]}") + return False + + try: + data = response.json() + except json.JSONDecodeError: + logger.error("Failed to decode JSON response") + logger.debug(f"Response content: {response.text[:200]}") + return False + + if data.get("success"): + self.sid = data.get("data", {}).get("sid") + self._last_auth_time = current_time + logger.info("Successfully logged in to Synology NAS") + logger.debug(f"Session ID obtained: {self.sid[:5] if self.sid else 'None'}...") # Показываем только начало SID для безопасности + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to log in to Synology NAS: Error code {error_code}") + + # Дополнительная диагностика + if error_code == 400: + logger.error("Authentication error: Invalid credentials") + elif error_code == 401: + logger.error("Authentication error: Account disabled") + elif error_code == 402: + logger.error("Authentication error: Permission denied") + elif error_code == 403: + logger.error("Authentication error: 2-factor authentication required") + elif error_code == 404: + logger.error("Authentication error: Failed to authenticate with 2-factor authentication") + + return False + + except requests.exceptions.Timeout: + logger.error(f"Connection timeout after {SYNOLOGY_TIMEOUT} seconds") + return False + except requests.exceptions.ConnectionError as e: + logger.error(f"Connection error: {str(e)}") + logger.debug(f"Connection details: {self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}") + return False + except requests.RequestException as e: + logger.error(f"Request error: {str(e)}") + return False + except Exception as e: + logger.error(f"Unexpected error during login: {str(e)}", exc_info=True) + return False + + def logout(self) -> bool: + """Выход из API Synology NAS""" + if not self.sid: + return True + + try: + url = f"{self.base_url}/auth.cgi" + params = { + "api": "SYNO.API.Auth", + "version": "1", + "method": "logout", + "session": "SynologyPowerControlBot", + "_sid": self.sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + self.sid = None + logger.info("Successfully logged out from Synology NAS") + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to log out from Synology NAS: Error code {error_code}") + return False + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + return False + + def _make_api_request(self, api_name: str, method: str, version: int = 1, + params: Optional[Dict[str, Any]] = None, retry_auth: bool = True) -> Optional[Dict[str, Any]]: + """Обобщенный метод для выполнения API запросов с обработкой ошибок""" + # Проверка наличия авторизации + if not self.sid and not self.login(): + logger.error(f"Not authenticated for API request: {api_name}.{method}") + return None + + # Проверка информации API для определения пути и поддерживаемой версии + api_info_url = f"{self.base_url}/entry.cgi" + api_info_params = { + "api": "SYNO.API.Info", + "version": "1", + "method": "query", + "query": api_name + } + + api_path = "entry.cgi" + try: + logger.debug(f"Querying API info for {api_name}") + api_info_response = self.session.get( + api_info_url, + params=api_info_params, + timeout=self.default_timeout, + verify=False + ) + + if api_info_response.status_code == 200: + api_info_data = api_info_response.json() + if api_info_data.get("success"): + api_info = api_info_data.get("data", {}).get(api_name, {}) + if api_info: + max_version = api_info.get("maxVersion", version) + min_version = api_info.get("minVersion", version) + api_path = api_info.get("path", "entry.cgi") + + # Проверка, поддерживается ли запрошенная версия + if version < min_version: + logger.warning(f"API version {version} for {api_name} is below minimum {min_version}, using {min_version}") + version = min_version + elif version > max_version: + logger.warning(f"API version {version} for {api_name} exceeds maximum {max_version}, using {max_version}") + version = max_version + + logger.debug(f"Using API path: {api_path}, version: {version}") + else: + logger.warning(f"API {api_name} not found in API info, using defaults") + except Exception as e: + logger.warning(f"Error querying API info for {api_name}: {str(e)}, using defaults") + + # Подготовка базовых параметров запроса + base_params = { + "api": api_name, + "version": str(version), + "method": method, + "sid": self.sid + } + + # Добавление дополнительных параметров, если они заданы + if params: + base_params.update(params) + + url = f"{self.base_url}/{api_path}" + logger.debug(f"API request: {api_name}.{method} v{version} to {url}") + logger.debug(f"Full request params: {base_params}") + + try: + start_time = time.time() + response = self.session.get( + url, + params=base_params, + timeout=self.default_timeout, + verify=False + ) + elapsed_time = time.time() - start_time + logger.debug(f"API request completed in {elapsed_time:.2f}s with status {response.status_code}") + + # Подробное логирование для отладки + logger.debug(f"Response: {response.text[:500]}") + + # Проверка статуса HTTP + if response.status_code != 200: + logger.error(f"HTTP error: {response.status_code} for {api_name}.{method}") + return None + + try: + data = response.json() + except json.JSONDecodeError: + logger.error(f"Failed to decode JSON response for {api_name}.{method}") + logger.debug(f"Response content: {response.text[:200]}") + return None + + if data.get("success"): + logger.info(f"API request successful for {api_name}.{method}") + return data.get("data", {}) + else: + error_code = data.get("error", {}).get("code", -1) + error_desc = self._get_error_description(error_code) + logger.error(f"API error for {api_name}.{method}: {error_code} - {error_desc}") + + # Ошибки доступа или прав часто встречаются, но они не критичные + # Например, ошибка 102 означает, что нет прав, но NAS доступен + if error_code in [102, 103, 104, 105]: + logger.warning(f"Permission-related error for {api_name}.{method}: {error_code}") + # Возвращаем пустой словарь вместо None, + # чтобы вызывающий код мог понять, что запрос выполнен + return {} + + # Если ошибка связана с авторизацией и нам разрешено повторить попытку + if error_code in [106, 107, 119] and retry_auth: + logger.info("Session may have expired, re-authenticating...") + self.sid = None # Сбрасываем SID + if self.login(): + logger.info("Re-authenticated, retrying API request...") + # Рекурсивный вызов, но без повторной авторизации во избежание зацикливания + return self._make_api_request(api_name, method, version, params, False) + + return None + + except requests.exceptions.Timeout: + logger.error(f"Request timeout for {api_name}.{method}") + return None + except requests.exceptions.ConnectionError as e: + logger.error(f"Connection error for {api_name}.{method}: {str(e)}") + return None + except Exception as e: + logger.error(f"Unexpected error during {api_name}.{method}: {str(e)}") + return None + + def get_system_status(self) -> Dict[str, Any]: + """Получение статуса системы""" + # Проверяем доступность системы + if not self.is_online(): + logger.info("Device is offline, skipping API request") + return {"status": "offline"} + + # Проверяем, есть ли кэшированный результат + cache_key = "system_status" + current_time = time.time() + if cache_key in self._cache and current_time - self._cache_ttl.get(cache_key, 0) < 60: + logger.debug("Using cached system status") + return self._cache[cache_key] + + # Делаем API запрос + result = self._make_api_request("SYNO.DSM.Info", "getinfo") + + if result: + # Формируем расширенный ответ с дополнительной информацией + system_info = { + "status": "online", + "hostname": result.get("hostname", "unknown"), + "model": result.get("model", "unknown"), + "version": result.get("version", "unknown"), + "uptime": result.get("uptime", 0), + "time": current_time, + "is_online": True + } + + # Сохраняем в кэше + self._cache[cache_key] = system_info + self._cache_ttl[cache_key] = current_time + + return system_info + else: + # Если запрос не удался, но система онлайн + return { + "status": "error", + "error": "Failed to fetch system information", + "is_online": True, + "time": current_time + } + + def shutdown_system(self) -> bool: + """Выключение системы""" + # Проверяем, включено ли устройство перед попыткой его выключить + if not self.is_online(force_check=True): + logger.info("Synology NAS is already offline, no need to shut down") + return True + + logger.info("Attempting to shutdown Synology NAS...") + + # Проверка всех доступных методов API для выключения + # Проверяем наличие API перед использованием + apis_to_try = [ + {"name": "SYNO.Core.System.Power", "method": "shutdown", "version": 1}, + {"name": "SYNO.DSM.Power", "method": "shutdown", "version": 1}, + {"name": "SYNO.Core.System", "method": "shutdown", "version": 3}, + {"name": "SYNO.System.Power", "method": "shutdown", "version": 1}, + {"name": "SYNO.DSM.System", "method": "shutdown", "version": 1} + ] + + # Получаем список доступных API + try: + api_info_url = f"{self.base_url}/entry.cgi" + api_info_params = { + "api": "SYNO.API.Info", + "version": "1", + "method": "query", + "query": "SYNO.Core.System.Power,SYNO.DSM.Power,SYNO.Core.System,SYNO.System.Power,SYNO.DSM.System" + } + + logger.debug("Checking available shutdown APIs") + api_info_response = self.session.get( + api_info_url, + params=api_info_params, + timeout=self.default_timeout, + verify=False + ) + + if api_info_response.status_code == 200: + api_info_data = api_info_response.json() + if api_info_data.get("success"): + available_apis = api_info_data.get("data", {}) + logger.debug(f"Available APIs: {list(available_apis.keys())}") + + # Фильтруем только доступные API + filtered_apis = [] + for api in apis_to_try: + if api["name"] in available_apis: + api_info = available_apis[api["name"]] + # Проверка версии API + if api_info.get("minVersion", 1) <= api["version"] <= api_info.get("maxVersion", 1): + filtered_apis.append(api) + logger.debug(f"Adding {api['name']} to available shutdown APIs") + else: + logger.debug(f"API {api['name']} version {api['version']} not supported. Supported: {api_info.get('minVersion')}-{api_info.get('maxVersion')}") + + if filtered_apis: + apis_to_try = filtered_apis + else: + logger.warning("No compatible APIs found, trying all methods as fallback") + else: + logger.warning("Failed to query API info, using default methods") + else: + logger.warning(f"API info request failed with status {api_info_response.status_code}, using default methods") + except Exception as e: + logger.warning(f"Error querying API info: {str(e)}, using default methods") + + # Пробуем все доступные методы по порядку + for api in apis_to_try: + logger.info(f"Trying shutdown with {api['name']}.{api['method']} v{api['version']}") + result = self._make_api_request(api["name"], api["method"], version=api["version"]) + + if result is not None: + logger.info(f"Successfully initiated system shutdown using {api['name']}") + + # Даем системе время начать процесс выключения + logger.info("Waiting for shutdown to initialize...") + sleep(5) + + # Проверяем статус + if not self.is_online(force_check=True): + logger.info("System is now offline. Shutdown confirmed successful.") + return True + else: + logger.info("System still appears to be online, but shutdown may be in progress.") + return True + else: + logger.warning(f"Failed to shutdown using {api['name']}.{api['method']} v{api['version']}") + + # Если ни один метод не сработал, но система стала недоступна + if not self.is_online(force_check=True): + logger.info("System appears to be shutting down despite API errors") + return True + + logger.error("Failed to shutdown system after trying multiple APIs") + return False + + def reboot_system(self) -> bool: + """Перезагрузка системы""" + # Проверяем, включена ли система + if not self.is_online(force_check=True): + logger.error("Cannot reboot: System is offline") + return False + + logger.info("Attempting to reboot Synology NAS...") + + # Правильный API-вызов для перезагрузки согласно официальной документации DSM API + result = self._make_api_request("SYNO.Core.System.Power", "restart", version=1) + + if result is not None: + logger.info("Successfully initiated system reboot using SYNO.Core.System.Power") + + # Даем системе время начать процесс перезагрузки + logger.info("Waiting for reboot to initialize...") + sleep(5) + + # Ждем, пока система станет недоступна (признак перезагрузки) + reboot_started = False + for i in range(12): # Проверяем в течение 60 секунд (12 * 5сек) + if not self.is_online(force_check=True): + logger.info(f"System went offline after {i*5} seconds, reboot in progress") + reboot_started = True + break + logger.debug(f"System still online, waiting... ({i+1}/12)") + sleep(5) + + if reboot_started: + # Ждем, пока система снова станет доступна + logger.info("Waiting for system to come back online...") + return self.wait_for_boot(max_attempts=30, delay=10) + else: + logger.warning("System did not go offline after reboot command") + return False + + # Запасной вариант, если первый метод не сработал + logger.info("First reboot method failed, trying alternative API...") + result = self._make_api_request("SYNO.Core.System", "reboot", version=3) + if result is not None: + logger.info("Successfully initiated system reboot using SYNO.Core.System") + + # Даем системе время начать процесс перезагрузки + logger.info("Waiting for reboot to initialize...") + sleep(5) + + # Ждем, пока система станет недоступна (признак перезагрузки) + reboot_started = False + for i in range(12): # Проверяем в течение 60 секунд (12 * 5сек) + if not self.is_online(force_check=True): + logger.info(f"System went offline after {i*5} seconds, reboot in progress") + reboot_started = True + break + logger.debug(f"System still online, waiting... ({i+1}/12)") + sleep(5) + + if reboot_started: + # Ждем, пока система снова станет доступна + logger.info("Waiting for system to come back online...") + return self.wait_for_boot(max_attempts=30, delay=10) + else: + logger.warning("System did not go offline after reboot command") + return False + + logger.error("Failed to reboot system after trying multiple APIs") + return False + + def _get_error_description(self, error_code: int) -> str: + """Получение описания ошибки по коду""" + error_descriptions = { + 100: "Unknown error", + 101: "Invalid parameter", + 102: "API does not exist", + 103: "Method does not exist", + 104: "Version does not support", + 105: "Permission denied", + 106: "Session timeout", + 107: "Session interrupted by duplicate login", + 400: "Invalid credentials", + 401: "Account disabled", + 402: "Permission denied", + 403: "2FA required", + 404: "Failed to authenticate with 2FA" + } + return error_descriptions.get(error_code, "Unknown error code") + + def _check_tcp_connection(self) -> bool: + """Проверка базового TCP-соединения с Synology NAS""" + try: + socket_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + socket_obj.settimeout(SYNOLOGY_TIMEOUT) + result = socket_obj.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT)) + socket_obj.close() + + return result == 0 + except socket.error as e: + logger.error(f"Socket error during connection check: {str(e)}") + return False + except Exception as e: + logger.error(f"Unexpected error during connection check: {str(e)}") + return False + + def is_online(self, force_check=False) -> bool: + """Проверка онлайн-статуса Synology NAS""" + # Используем кэшированное значение, если доступно и не устарело + current_time = time.time() + if not force_check and (current_time - self._last_online_check) < self._online_check_interval: + logger.debug(f"Using cached online status: {self._last_online_status}") + return self._last_online_status + + logger.info("Checking if NAS is online...") + + # Проверяем TCP-соединение + online_status = self._check_tcp_connection() + logger.info(f"Detected Synology NAS online status: {online_status}") + + # Если TCP-соединение успешно и у нас есть действующий SID, + # попробуем более детальную проверку через API + if online_status and self.sid: + logger.info("Trying to fetch more detailed online status through API...") + try: + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.DSM.Info", + "version": "1", + "method": "getinfo", + "sid": self.sid + } + + response = self.session.get( + url, + params=params, + timeout=self.default_timeout, + verify=False + ) + + if response.status_code == 200: + data = response.json() + if data.get("success"): + logger.info("API request successful for getinfo") + logger.info("Synology NAS is online with API access") + else: + logger.warning("API response indicates an error, but NAS is reachable") + logger.debug(f"API error: {data.get('error', {}).get('code', -1)}") + else: + logger.warning(f"API returned status code {response.status_code}") + except Exception as e: + logger.warning(f"API check failed, but TCP connection succeeded: {str(e)}") + + # Обновляем кэшированное значение + self._last_online_check = current_time + self._last_online_status = online_status + + return online_status + + def wake_on_lan(self) -> bool: + """Отправка Wake-on-LAN пакета для включения Synology NAS""" + if not SYNOLOGY_MAC: + logger.error("MAC address not configured") + return False + + try: + # Преобразование MAC-адреса в байты + mac_address = SYNOLOGY_MAC.replace(':', '').replace('-', '') + if len(mac_address) != 12: + logger.error(f"Invalid MAC address format: {SYNOLOGY_MAC}") + return False + + try: + mac_bytes = bytes.fromhex(mac_address) + except ValueError as e: + logger.error(f"Failed to parse MAC address: {str(e)}") + logger.error(f"MAC address must contain only hex characters: {SYNOLOGY_MAC}") + return False + + # Создание Magic Packet + magic_packet = b'\xff' * 6 + mac_bytes * 16 + + # Отправка пакета на конкретный адрес + try: + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) + sock.sendto(magic_packet, (SYNOLOGY_HOST, WOL_PORT)) + sock.close() + logger.info(f"Wake-on-LAN packet sent to {SYNOLOGY_MAC} at {SYNOLOGY_HOST}:{WOL_PORT}") + except Exception as e: + logger.error(f"Error sending directed WoL packet: {str(e)}") + return False + + # Для надежности отправляем также широковещательный пакет + try: + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) + + # Используем стандартный широковещательный адрес + broadcast_addr = "255.255.255.255" + sock.sendto(magic_packet, (broadcast_addr, WOL_PORT)) + sock.close() + logger.info(f"Wake-on-LAN broadcast packet sent to {broadcast_addr}:{WOL_PORT}") + except Exception as e: + logger.warning(f"Error sending broadcast WoL packet (non-critical): {str(e)}") + # Не считаем ошибкой, т.к. основной пакет уже отправлен + + return True + + except Exception as e: + logger.error(f"Unexpected error in wake_on_lan: {str(e)}") + return False + + def wait_for_boot(self, max_attempts: int = 30, delay: int = 10) -> bool: + """Ожидание загрузки Synology NAS""" + logger.info(f"Waiting for Synology NAS to boot (timeout: {max_attempts * delay}s)...") + + for attempt in range(max_attempts): + # Принудительно проверяем статус без использования кэша + if self.is_online(force_check=True): + logger.info(f"Synology NAS is online after {attempt + 1} attempts ({(attempt + 1) * delay}s)") + + # Проверяем, что не только сеть доступна, но и API загрузился + api_ready = False + logger.info("Waiting for API services to initialize...") + + for api_check in range(5): # Даем еще до 50 секунд для загрузки API + if self.sid or self.login(): + api_ready = True + logger.info(f"API services are ready after {api_check + 1} attempts") + break + logger.debug(f"API not ready yet, waiting... ({api_check + 1}/5)") + sleep(10) + + if not api_ready: + logger.warning("System is online but API services may not be fully initialized") + + # Дадим дополнительное время для полной загрузки всех сервисов + sleep(delay) + return True + + sleep(delay) + logger.info(f"Waiting for boot... Attempt {attempt + 1}/{max_attempts}") + + logger.error(f"Synology NAS didn't come online after {max_attempts * delay} seconds") + return False + + def power_on(self) -> bool: + """Включение Synology NAS""" + # Принудительная проверка статуса + if self.is_online(force_check=True): + logger.info("Synology NAS is already online") + return True + + logger.info("Powering on Synology NAS via Wake-on-LAN...") + + # Проверяем, настроен ли MAC-адрес + if not SYNOLOGY_MAC: + logger.error("Cannot power on: MAC address not configured in settings") + return False + + # Пробуем отправить несколько WoL пакетов для надежности + success = False + for attempt in range(3): + logger.debug(f"Sending WoL packet, attempt {attempt + 1}/3") + if self.wake_on_lan(): + success = True + break + sleep(1) + + if not success: + logger.error("Failed to send Wake-on-LAN packets") + return False + + # Ожидание загрузки + logger.info("WoL packets sent successfully, waiting for system to boot...") + boot_result = self.wait_for_boot(max_attempts=30, delay=10) + + if boot_result: + # Проверяем доступность API после загрузки + system_status = self.get_system_status() + if system_status.get("status") == "online": + logger.info("System booted successfully with API access") + return True + else: + logger.warning("System appears to be online but API may not be fully ready") + return True + else: + logger.error("System did not come online after WoL") + return False + + def power_off(self) -> bool: + """Выключение Synology NAS""" + if not self.is_online(force_check=True): + logger.info("Synology NAS is already offline") + return True + + logger.info("Powering off Synology NAS...") + result = self.shutdown_system() + + if result: + # Дополнительная проверка, что система действительно выключилась + logger.info("Verifying system is offline...") + for attempt in range(12): # Проверяем в течение 2 минут (12 * 10 сек) + if not self.is_online(force_check=True): + logger.info(f"System confirmed offline after {attempt * 10} seconds") + return True + logger.debug(f"System still shutting down, checking again... ({attempt + 1}/12)") + sleep(10) + + logger.warning("System still appears to be online after shutdown command") + return False + else: + logger.error("Failed to initiate shutdown") + return False + + # Заглушки для расширенных методов + def get_shared_folders(self) -> List[Dict[str, Any]]: + """Получение списка общих папок (заглушка)""" + logger.warning("Function get_shared_folders() is not implemented yet") + return [] + + def get_system_load(self) -> Dict[str, Any]: + """Получение информации о загрузке системы (заглушка)""" + logger.warning("Function get_system_load() is not implemented yet") + return {} + + def get_storage_status(self) -> Dict[str, Any]: + """Получение подробной информации о хранилище (заглушка)""" + logger.warning("Function get_storage_status() is not implemented yet") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for storage status request") + return {"error": "authentication_failed"} + + try: + # Здесь будет реализация после добавления соответствующего API + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.Storage.CGI.Storage", + "version": "1", + "method": "load_info", + "sid": self.sid + } + + # В текущей версии просто возвращаем заглушку + return { + "volumes": [], + "disks": [], + "total_size": 0, + "total_used": 0, + "error": "not_implemented" + } + except Exception as e: + logger.error(f"Error in get_storage_status: {str(e)}") + return {"error": str(e)} + + def get_security_status(self) -> Dict[str, Any]: + """Получение информации о состоянии безопасности (заглушка)""" + logger.warning("Function get_security_status() is not implemented yet") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for security status request") + return {"success": False, "error": "authentication_failed"} + + try: + # Здесь будет реализация после добавления соответствующего API + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.Core.SecurityScan.Status", + "version": "1", + "method": "get", + "sid": self.sid + } + + # В текущей версии просто возвращаем заглушку + return { + "success": False, + "status": "not_implemented", + "last_check": None, + "is_secure": False, + "error": "not_implemented" + } + except Exception as e: + logger.error(f"Error in get_security_status: {str(e)}") + return {"success": False, "error": str(e)} diff --git a/.history/src/api/synology_20250830081505.py b/.history/src/api/synology_20250830081505.py new file mode 100644 index 0000000..1e92c48 --- /dev/null +++ b/.history/src/api/synology_20250830081505.py @@ -0,0 +1,943 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Модуль для взаимодействия с API Synology NAS +""" + +import requests +from requests.adapters import HTTPAdapter +import json +import logging +import time +import urllib3 +from urllib3.util import Retry +from typing import Dict, Any, Optional, List +import socket +import struct +from time import sleep + +from src.config.config import ( + SYNOLOGY_HOST, + SYNOLOGY_PORT, + SYNOLOGY_USERNAME, + SYNOLOGY_PASSWORD, + SYNOLOGY_SECURE, + SYNOLOGY_TIMEOUT, + SYNOLOGY_MAC, + WOL_PORT +) + +# Отключение предупреждений о небезопасных SSL-соединениях +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + +logger = logging.getLogger(__name__) + +class SynologyAPI: + """Класс для взаимодействия с API Synology NAS""" + + def __init__(self): + """Инициализация класса SynologyAPI""" + logger.info("Creating API with auto-retry and connection pool") + logger.debug(f"Connection details: Host={SYNOLOGY_HOST}, Port={SYNOLOGY_PORT}, Secure={SYNOLOGY_SECURE}") + + self.protocol = "https" if SYNOLOGY_SECURE else "http" + self.base_url = f"{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi" + self.sid = None + self.session = requests.Session() + + # Настройка SSL + if self.protocol == "https": + logger.debug("SSL enabled, disabling certificate verification for internal network") + self.session.verify = False # Отключаем проверку SSL для внутренней сети + + # Добавляем повторные попытки для HTTP-запросов + retry_strategy = Retry( + total=5, # Увеличиваем количество попыток + status_forcelist=[429, 500, 502, 503, 504, 404], + allowed_methods=["GET", "POST"], + backoff_factor=1.5, # Увеличиваем задержку между попытками + respect_retry_after_header=True + ) + adapter = HTTPAdapter( + max_retries=retry_strategy, + pool_connections=3, + pool_maxsize=10 + ) + self.session.mount("http://", adapter) + self.session.mount("https://", adapter) + + # Таймауты будут указаны в запросах + self.default_timeout = (SYNOLOGY_TIMEOUT, SYNOLOGY_TIMEOUT*2) + logger.debug(f"Setting default request timeout: {self.default_timeout}") + + # Кэш для хранения результатов запросов + self._cache = {} + self._cache_ttl = {} + self._last_online_check = 0 + self._last_online_status = False + self._online_check_interval = 30 # Интервал для кэширования проверок онлайн-статуса + + # Время последней успешной аутентификации и срок действия сессии + self._last_auth_time = 0 + self._auth_expiry = 3600 # По умолчанию 1 час + + def login(self) -> bool: + """Авторизация в API Synology NAS""" + # Проверяем, действительна ли текущая сессия + current_time = time.time() + if self.sid and (current_time - self._last_auth_time) < self._auth_expiry: + logger.debug("Using existing session, still valid") + return True + + logger.info("Attempting to authenticate with Synology NAS...") + logger.debug(f"Base URL: {self.base_url}") + + # Проверяем доступность NAS перед запросом с помощью прямой TCP-проверки + # Избегаем вызова is_online(), чтобы не создавать рекурсию + online_status = self._check_tcp_connection() + if not online_status: + logger.error("Cannot login: Synology NAS is not reachable") + return False + + try: + # Проверка информации API для определения доступных версий API + api_info_url = f"{self.base_url}/entry.cgi" + api_info_params = { + "api": "SYNO.API.Info", + "version": "1", + "method": "query", + "query": "SYNO.API.Auth" + } + + logger.debug("Querying API info to determine optimal auth version") + try: + api_info_response = self.session.get( + api_info_url, + params=api_info_params, + timeout=self.default_timeout, + verify=False + ) + + if api_info_response.status_code == 200: + api_info_data = api_info_response.json() + if api_info_data.get("success"): + auth_info = api_info_data.get("data", {}).get("SYNO.API.Auth", {}) + max_version = auth_info.get("maxVersion", 3) + min_version = auth_info.get("minVersion", 1) + auth_path = auth_info.get("path", "auth.cgi") + logger.debug(f"Auth API versions: min={min_version}, max={max_version}, path={auth_path}") + + # Используем максимально поддерживаемую версию, но не выше 6 + auth_version = min(max_version, 6) + else: + logger.warning("Failed to query API info, using default auth version 3") + auth_version = 3 + auth_path = "auth.cgi" + else: + logger.warning(f"API info request failed with status {api_info_response.status_code}, using default auth version 3") + auth_version = 3 + auth_path = "auth.cgi" + except Exception as e: + logger.warning(f"Error querying API info: {str(e)}, using default auth version 3") + auth_version = 3 + auth_path = "auth.cgi" + + # Основной запрос авторизации + url = f"{self.base_url}/{auth_path}" + params = { + "api": "SYNO.API.Auth", + "version": str(auth_version), + "method": "login", + "account": SYNOLOGY_USERNAME, + "passwd": SYNOLOGY_PASSWORD, + "session": "SynologyPowerControlBot", + "format": "cookie" + } + + # Для версии 6+ используем немного другой формат + if auth_version >= 6: + params["enable_syno_token"] = "yes" + + logger.debug(f"Sending auth request to {url} with API version {auth_version}") + start_time = time.time() + response = self.session.get( + url, + params=params, + timeout=self.default_timeout, + verify=False + ) + elapsed_time = time.time() - start_time + logger.debug(f"Auth request completed in {elapsed_time:.2f}s with status code {response.status_code}") + + # Подробная информация для отладки + logger.debug(f"Auth response: {response.text[:500]}") + + # Проверка статуса HTTP + if response.status_code != 200: + logger.error(f"HTTP error: {response.status_code}") + logger.debug(f"Response content: {response.text[:200]}") + return False + + try: + data = response.json() + except json.JSONDecodeError: + logger.error("Failed to decode JSON response") + logger.debug(f"Response content: {response.text[:200]}") + return False + + if data.get("success"): + self.sid = data.get("data", {}).get("sid") + self._last_auth_time = current_time + logger.info("Successfully logged in to Synology NAS") + logger.debug(f"Session ID obtained: {self.sid[:5] if self.sid else 'None'}...") # Показываем только начало SID для безопасности + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to log in to Synology NAS: Error code {error_code}") + + # Дополнительная диагностика + if error_code == 400: + logger.error("Authentication error: Invalid credentials") + elif error_code == 401: + logger.error("Authentication error: Account disabled") + elif error_code == 402: + logger.error("Authentication error: Permission denied") + elif error_code == 403: + logger.error("Authentication error: 2-factor authentication required") + elif error_code == 404: + logger.error("Authentication error: Failed to authenticate with 2-factor authentication") + + return False + + except requests.exceptions.Timeout: + logger.error(f"Connection timeout after {SYNOLOGY_TIMEOUT} seconds") + return False + except requests.exceptions.ConnectionError as e: + logger.error(f"Connection error: {str(e)}") + logger.debug(f"Connection details: {self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}") + return False + except requests.RequestException as e: + logger.error(f"Request error: {str(e)}") + return False + except Exception as e: + logger.error(f"Unexpected error during login: {str(e)}", exc_info=True) + return False + + def logout(self) -> bool: + """Выход из API Synology NAS""" + if not self.sid: + return True + + try: + url = f"{self.base_url}/auth.cgi" + params = { + "api": "SYNO.API.Auth", + "version": "1", + "method": "logout", + "session": "SynologyPowerControlBot", + "_sid": self.sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + self.sid = None + logger.info("Successfully logged out from Synology NAS") + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to log out from Synology NAS: Error code {error_code}") + return False + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + return False + + def _make_api_request(self, api_name: str, method: str, version: int = 1, + params: Optional[Dict[str, Any]] = None, retry_auth: bool = True) -> Optional[Dict[str, Any]]: + """Обобщенный метод для выполнения API запросов с обработкой ошибок""" + # Проверка наличия авторизации + if not self.sid and not self.login(): + logger.error(f"Not authenticated for API request: {api_name}.{method}") + return None + + # Проверка информации API для определения пути и поддерживаемой версии + api_info_url = f"{self.base_url}/entry.cgi" + api_info_params = { + "api": "SYNO.API.Info", + "version": "1", + "method": "query", + "query": api_name + } + + api_path = "entry.cgi" + try: + logger.debug(f"Querying API info for {api_name}") + api_info_response = self.session.get( + api_info_url, + params=api_info_params, + timeout=self.default_timeout, + verify=False + ) + + if api_info_response.status_code == 200: + api_info_data = api_info_response.json() + if api_info_data.get("success"): + api_info = api_info_data.get("data", {}).get(api_name, {}) + if api_info: + max_version = api_info.get("maxVersion", version) + min_version = api_info.get("minVersion", version) + api_path = api_info.get("path", "entry.cgi") + + # Проверка, поддерживается ли запрошенная версия + if version < min_version: + logger.warning(f"API version {version} for {api_name} is below minimum {min_version}, using {min_version}") + version = min_version + elif version > max_version: + logger.warning(f"API version {version} for {api_name} exceeds maximum {max_version}, using {max_version}") + version = max_version + + logger.debug(f"Using API path: {api_path}, version: {version}") + else: + logger.warning(f"API {api_name} not found in API info, using defaults") + except Exception as e: + logger.warning(f"Error querying API info for {api_name}: {str(e)}, using defaults") + + # Подготовка базовых параметров запроса + base_params = { + "api": api_name, + "version": str(version), + "method": method, + "sid": self.sid + } + + # Добавление дополнительных параметров, если они заданы + if params: + base_params.update(params) + + url = f"{self.base_url}/{api_path}" + logger.debug(f"API request: {api_name}.{method} v{version} to {url}") + logger.debug(f"Full request params: {base_params}") + + try: + start_time = time.time() + response = self.session.get( + url, + params=base_params, + timeout=self.default_timeout, + verify=False + ) + elapsed_time = time.time() - start_time + logger.debug(f"API request completed in {elapsed_time:.2f}s with status {response.status_code}") + + # Подробное логирование для отладки + logger.debug(f"Response: {response.text[:500]}") + + # Проверка статуса HTTP + if response.status_code != 200: + logger.error(f"HTTP error: {response.status_code} for {api_name}.{method}") + return None + + try: + data = response.json() + except json.JSONDecodeError: + logger.error(f"Failed to decode JSON response for {api_name}.{method}") + logger.debug(f"Response content: {response.text[:200]}") + return None + + if data.get("success"): + logger.info(f"API request successful for {api_name}.{method}") + return data.get("data", {}) + else: + error_code = data.get("error", {}).get("code", -1) + error_desc = self._get_error_description(error_code) + logger.error(f"API error for {api_name}.{method}: {error_code} - {error_desc}") + + # Ошибки доступа или прав часто встречаются, но они не критичные + # Например, ошибка 102 означает, что нет прав, но NAS доступен + if error_code in [102, 103, 104, 105]: + logger.warning(f"Permission-related error for {api_name}.{method}: {error_code}") + # Возвращаем пустой словарь вместо None, + # чтобы вызывающий код мог понять, что запрос выполнен + return {} + + # Если ошибка связана с авторизацией и нам разрешено повторить попытку + if error_code in [106, 107, 119] and retry_auth: + logger.info("Session may have expired, re-authenticating...") + self.sid = None # Сбрасываем SID + if self.login(): + logger.info("Re-authenticated, retrying API request...") + # Рекурсивный вызов, но без повторной авторизации во избежание зацикливания + return self._make_api_request(api_name, method, version, params, False) + + return None + + except requests.exceptions.Timeout: + logger.error(f"Request timeout for {api_name}.{method}") + return None + except requests.exceptions.ConnectionError as e: + logger.error(f"Connection error for {api_name}.{method}: {str(e)}") + return None + except Exception as e: + logger.error(f"Unexpected error during {api_name}.{method}: {str(e)}") + return None + + def get_system_status(self) -> Dict[str, Any]: + """Получение статуса системы""" + # Проверяем доступность системы + if not self.is_online(): + logger.info("Device is offline, skipping API request") + return {"status": "offline"} + + # Проверяем, есть ли кэшированный результат + cache_key = "system_status" + current_time = time.time() + if cache_key in self._cache and current_time - self._cache_ttl.get(cache_key, 0) < 60: + logger.debug("Using cached system status") + return self._cache[cache_key] + + # Делаем API запрос + result = self._make_api_request("SYNO.DSM.Info", "getinfo") + + if result: + # Формируем расширенный ответ с дополнительной информацией + system_info = { + "status": "online", + "hostname": result.get("hostname", "unknown"), + "model": result.get("model", "unknown"), + "version": result.get("version", "unknown"), + "uptime": result.get("uptime", 0), + "time": current_time, + "is_online": True + } + + # Сохраняем в кэше + self._cache[cache_key] = system_info + self._cache_ttl[cache_key] = current_time + + return system_info + else: + # Если запрос не удался, но система онлайн + return { + "status": "error", + "error": "Failed to fetch system information", + "is_online": True, + "time": current_time + } + + def shutdown_system(self) -> bool: + """Выключение системы""" + # Проверяем, включено ли устройство перед попыткой его выключить + if not self.is_online(force_check=True): + logger.info("Synology NAS is already offline, no need to shut down") + return True + + logger.info("Attempting to shutdown Synology NAS...") + + # Проверка всех доступных методов API для выключения + # Проверяем наличие API перед использованием + apis_to_try = [ + {"name": "SYNO.Core.System.Power", "method": "shutdown", "version": 1}, + {"name": "SYNO.DSM.Power", "method": "shutdown", "version": 1}, + {"name": "SYNO.Core.System", "method": "shutdown", "version": 3}, + {"name": "SYNO.System.Power", "method": "shutdown", "version": 1}, + {"name": "SYNO.DSM.System", "method": "shutdown", "version": 1} + ] + + # Получаем список доступных API + try: + api_info_url = f"{self.base_url}/entry.cgi" + api_info_params = { + "api": "SYNO.API.Info", + "version": "1", + "method": "query", + "query": "SYNO.Core.System.Power,SYNO.DSM.Power,SYNO.Core.System,SYNO.System.Power,SYNO.DSM.System" + } + + logger.debug("Checking available shutdown APIs") + api_info_response = self.session.get( + api_info_url, + params=api_info_params, + timeout=self.default_timeout, + verify=False + ) + + if api_info_response.status_code == 200: + api_info_data = api_info_response.json() + if api_info_data.get("success"): + available_apis = api_info_data.get("data", {}) + logger.debug(f"Available APIs: {list(available_apis.keys())}") + + # Фильтруем только доступные API + filtered_apis = [] + for api in apis_to_try: + if api["name"] in available_apis: + api_info = available_apis[api["name"]] + # Проверка версии API + if api_info.get("minVersion", 1) <= api["version"] <= api_info.get("maxVersion", 1): + filtered_apis.append(api) + logger.debug(f"Adding {api['name']} to available shutdown APIs") + else: + logger.debug(f"API {api['name']} version {api['version']} not supported. Supported: {api_info.get('minVersion')}-{api_info.get('maxVersion')}") + + if filtered_apis: + apis_to_try = filtered_apis + else: + logger.warning("No compatible APIs found, trying all methods as fallback") + else: + logger.warning("Failed to query API info, using default methods") + else: + logger.warning(f"API info request failed with status {api_info_response.status_code}, using default methods") + except Exception as e: + logger.warning(f"Error querying API info: {str(e)}, using default methods") + + # Пробуем все доступные методы по порядку + for api in apis_to_try: + logger.info(f"Trying shutdown with {api['name']}.{api['method']} v{api['version']}") + result = self._make_api_request(api["name"], api["method"], version=api["version"]) + + if result is not None: + logger.info(f"Successfully initiated system shutdown using {api['name']}") + + # Даем системе время начать процесс выключения + logger.info("Waiting for shutdown to initialize...") + sleep(5) + + # Проверяем статус + if not self.is_online(force_check=True): + logger.info("System is now offline. Shutdown confirmed successful.") + return True + else: + logger.info("System still appears to be online, but shutdown may be in progress.") + return True + else: + logger.warning(f"Failed to shutdown using {api['name']}.{api['method']} v{api['version']}") + + # Если ни один метод не сработал, но система стала недоступна + if not self.is_online(force_check=True): + logger.info("System appears to be shutting down despite API errors") + return True + + logger.error("Failed to shutdown system after trying multiple APIs") + return False + + def reboot_system(self) -> bool: + """Перезагрузка системы""" + # Проверяем, включена ли система + if not self.is_online(force_check=True): + logger.error("Cannot reboot: System is offline") + return False + + logger.info("Attempting to reboot Synology NAS...") + + # Проверка всех доступных методов API для перезагрузки + apis_to_try = [ + {"name": "SYNO.Core.System.Power", "method": "restart", "version": 1}, + {"name": "SYNO.DSM.Power", "method": "reboot", "version": 1}, + {"name": "SYNO.Core.System", "method": "reboot", "version": 3}, + {"name": "SYNO.System.Power", "method": "reboot", "version": 1}, + {"name": "SYNO.DSM.System", "method": "reboot", "version": 1} + ] + + # Получаем список доступных API + try: + api_info_url = f"{self.base_url}/entry.cgi" + api_info_params = { + "api": "SYNO.API.Info", + "version": "1", + "method": "query", + "query": "SYNO.Core.System.Power,SYNO.DSM.Power,SYNO.Core.System,SYNO.System.Power,SYNO.DSM.System" + } + + logger.debug("Checking available reboot APIs") + api_info_response = self.session.get( + api_info_url, + params=api_info_params, + timeout=self.default_timeout, + verify=False + ) + + if api_info_response.status_code == 200: + api_info_data = api_info_response.json() + if api_info_data.get("success"): + available_apis = api_info_data.get("data", {}) + logger.debug(f"Available APIs: {list(available_apis.keys())}") + + # Фильтруем только доступные API + filtered_apis = [] + for api in apis_to_try: + if api["name"] in available_apis: + api_info = available_apis[api["name"]] + # Проверка версии API + if api_info.get("minVersion", 1) <= api["version"] <= api_info.get("maxVersion", 1): + filtered_apis.append(api) + logger.debug(f"Adding {api['name']} to available reboot APIs") + else: + logger.debug(f"API {api['name']} version {api['version']} not supported. Supported: {api_info.get('minVersion')}-{api_info.get('maxVersion')}") + + if filtered_apis: + apis_to_try = filtered_apis + else: + logger.warning("No compatible APIs found, trying all methods as fallback") + else: + logger.warning("Failed to query API info, using default methods") + else: + logger.warning(f"API info request failed with status {api_info_response.status_code}, using default methods") + except Exception as e: + logger.warning(f"Error querying API info: {str(e)}, using default methods") + + # Пробуем все доступные методы по порядку + for api in apis_to_try: + logger.info(f"Trying reboot with {api['name']}.{api['method']} v{api['version']}") + result = self._make_api_request(api["name"], api["method"], version=api["version"]) + + if result is not None: + logger.info(f"Successfully initiated system reboot using {api['name']}") + + # Даем системе время начать процесс перезагрузки + logger.info("Waiting for reboot to initialize...") + sleep(5) + + # Ждем, пока система станет недоступна (признак перезагрузки) + reboot_started = False + for i in range(12): # Проверяем в течение 60 секунд (12 * 5сек) + if not self.is_online(force_check=True): + logger.info(f"System went offline after {i*5} seconds, reboot in progress") + reboot_started = True + break + logger.debug(f"System still online, waiting... ({i+1}/12)") + sleep(5) + + if reboot_started: + # Ждем, пока система снова станет доступна + logger.info("Waiting for system to come back online...") + return self.wait_for_boot(max_attempts=30, delay=10) + else: + logger.warning("System did not go offline after reboot command, but command was accepted") + # Даже если система не ушла оффлайн, команда могла быть принята + return True + else: + logger.warning(f"Failed to reboot using {api['name']}.{api['method']} v{api['version']}") + + logger.error("Failed to reboot system after trying multiple APIs") + return False + + def _get_error_description(self, error_code: int) -> str: + """Получение описания ошибки по коду""" + error_descriptions = { + 100: "Unknown error", + 101: "Invalid parameter", + 102: "API does not exist", + 103: "Method does not exist", + 104: "Version does not support", + 105: "Permission denied", + 106: "Session timeout", + 107: "Session interrupted by duplicate login", + 400: "Invalid credentials", + 401: "Account disabled", + 402: "Permission denied", + 403: "2FA required", + 404: "Failed to authenticate with 2FA" + } + return error_descriptions.get(error_code, "Unknown error code") + + def _check_tcp_connection(self) -> bool: + """Проверка базового TCP-соединения с Synology NAS""" + try: + socket_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + socket_obj.settimeout(SYNOLOGY_TIMEOUT) + result = socket_obj.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT)) + socket_obj.close() + + return result == 0 + except socket.error as e: + logger.error(f"Socket error during connection check: {str(e)}") + return False + except Exception as e: + logger.error(f"Unexpected error during connection check: {str(e)}") + return False + + def is_online(self, force_check=False) -> bool: + """Проверка онлайн-статуса Synology NAS""" + # Используем кэшированное значение, если доступно и не устарело + current_time = time.time() + if not force_check and (current_time - self._last_online_check) < self._online_check_interval: + logger.debug(f"Using cached online status: {self._last_online_status}") + return self._last_online_status + + logger.info("Checking if NAS is online...") + + # Проверяем TCP-соединение + online_status = self._check_tcp_connection() + logger.info(f"Detected Synology NAS online status: {online_status}") + + # Если TCP-соединение успешно и у нас есть действующий SID, + # попробуем более детальную проверку через API + if online_status and self.sid: + logger.info("Trying to fetch more detailed online status through API...") + try: + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.DSM.Info", + "version": "1", + "method": "getinfo", + "sid": self.sid + } + + response = self.session.get( + url, + params=params, + timeout=self.default_timeout, + verify=False + ) + + if response.status_code == 200: + data = response.json() + if data.get("success"): + logger.info("API request successful for getinfo") + logger.info("Synology NAS is online with API access") + else: + logger.warning("API response indicates an error, but NAS is reachable") + logger.debug(f"API error: {data.get('error', {}).get('code', -1)}") + else: + logger.warning(f"API returned status code {response.status_code}") + except Exception as e: + logger.warning(f"API check failed, but TCP connection succeeded: {str(e)}") + + # Обновляем кэшированное значение + self._last_online_check = current_time + self._last_online_status = online_status + + return online_status + + def wake_on_lan(self) -> bool: + """Отправка Wake-on-LAN пакета для включения Synology NAS""" + if not SYNOLOGY_MAC: + logger.error("MAC address not configured") + return False + + try: + # Преобразование MAC-адреса в байты + mac_address = SYNOLOGY_MAC.replace(':', '').replace('-', '') + if len(mac_address) != 12: + logger.error(f"Invalid MAC address format: {SYNOLOGY_MAC}") + return False + + try: + mac_bytes = bytes.fromhex(mac_address) + except ValueError as e: + logger.error(f"Failed to parse MAC address: {str(e)}") + logger.error(f"MAC address must contain only hex characters: {SYNOLOGY_MAC}") + return False + + # Создание Magic Packet + magic_packet = b'\xff' * 6 + mac_bytes * 16 + + # Отправка пакета на конкретный адрес + try: + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) + sock.sendto(magic_packet, (SYNOLOGY_HOST, WOL_PORT)) + sock.close() + logger.info(f"Wake-on-LAN packet sent to {SYNOLOGY_MAC} at {SYNOLOGY_HOST}:{WOL_PORT}") + except Exception as e: + logger.error(f"Error sending directed WoL packet: {str(e)}") + return False + + # Для надежности отправляем также широковещательный пакет + try: + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) + + # Используем стандартный широковещательный адрес + broadcast_addr = "255.255.255.255" + sock.sendto(magic_packet, (broadcast_addr, WOL_PORT)) + sock.close() + logger.info(f"Wake-on-LAN broadcast packet sent to {broadcast_addr}:{WOL_PORT}") + except Exception as e: + logger.warning(f"Error sending broadcast WoL packet (non-critical): {str(e)}") + # Не считаем ошибкой, т.к. основной пакет уже отправлен + + return True + + except Exception as e: + logger.error(f"Unexpected error in wake_on_lan: {str(e)}") + return False + + def wait_for_boot(self, max_attempts: int = 30, delay: int = 10) -> bool: + """Ожидание загрузки Synology NAS""" + logger.info(f"Waiting for Synology NAS to boot (timeout: {max_attempts * delay}s)...") + + for attempt in range(max_attempts): + # Принудительно проверяем статус без использования кэша + if self.is_online(force_check=True): + logger.info(f"Synology NAS is online after {attempt + 1} attempts ({(attempt + 1) * delay}s)") + + # Проверяем, что не только сеть доступна, но и API загрузился + api_ready = False + logger.info("Waiting for API services to initialize...") + + for api_check in range(5): # Даем еще до 50 секунд для загрузки API + if self.sid or self.login(): + api_ready = True + logger.info(f"API services are ready after {api_check + 1} attempts") + break + logger.debug(f"API not ready yet, waiting... ({api_check + 1}/5)") + sleep(10) + + if not api_ready: + logger.warning("System is online but API services may not be fully initialized") + + # Дадим дополнительное время для полной загрузки всех сервисов + sleep(delay) + return True + + sleep(delay) + logger.info(f"Waiting for boot... Attempt {attempt + 1}/{max_attempts}") + + logger.error(f"Synology NAS didn't come online after {max_attempts * delay} seconds") + return False + + def power_on(self) -> bool: + """Включение Synology NAS""" + # Принудительная проверка статуса + if self.is_online(force_check=True): + logger.info("Synology NAS is already online") + return True + + logger.info("Powering on Synology NAS via Wake-on-LAN...") + + # Проверяем, настроен ли MAC-адрес + if not SYNOLOGY_MAC: + logger.error("Cannot power on: MAC address not configured in settings") + return False + + # Пробуем отправить несколько WoL пакетов для надежности + success = False + for attempt in range(3): + logger.debug(f"Sending WoL packet, attempt {attempt + 1}/3") + if self.wake_on_lan(): + success = True + break + sleep(1) + + if not success: + logger.error("Failed to send Wake-on-LAN packets") + return False + + # Ожидание загрузки + logger.info("WoL packets sent successfully, waiting for system to boot...") + boot_result = self.wait_for_boot(max_attempts=30, delay=10) + + if boot_result: + # Проверяем доступность API после загрузки + system_status = self.get_system_status() + if system_status.get("status") == "online": + logger.info("System booted successfully with API access") + return True + else: + logger.warning("System appears to be online but API may not be fully ready") + return True + else: + logger.error("System did not come online after WoL") + return False + + def power_off(self) -> bool: + """Выключение Synology NAS""" + if not self.is_online(force_check=True): + logger.info("Synology NAS is already offline") + return True + + logger.info("Powering off Synology NAS...") + result = self.shutdown_system() + + if result: + # Дополнительная проверка, что система действительно выключилась + logger.info("Verifying system is offline...") + for attempt in range(12): # Проверяем в течение 2 минут (12 * 10 сек) + if not self.is_online(force_check=True): + logger.info(f"System confirmed offline after {attempt * 10} seconds") + return True + logger.debug(f"System still shutting down, checking again... ({attempt + 1}/12)") + sleep(10) + + logger.warning("System still appears to be online after shutdown command") + return False + else: + logger.error("Failed to initiate shutdown") + return False + + # Заглушки для расширенных методов + def get_shared_folders(self) -> List[Dict[str, Any]]: + """Получение списка общих папок (заглушка)""" + logger.warning("Function get_shared_folders() is not implemented yet") + return [] + + def get_system_load(self) -> Dict[str, Any]: + """Получение информации о загрузке системы (заглушка)""" + logger.warning("Function get_system_load() is not implemented yet") + return {} + + def get_storage_status(self) -> Dict[str, Any]: + """Получение подробной информации о хранилище (заглушка)""" + logger.warning("Function get_storage_status() is not implemented yet") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for storage status request") + return {"error": "authentication_failed"} + + try: + # Здесь будет реализация после добавления соответствующего API + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.Storage.CGI.Storage", + "version": "1", + "method": "load_info", + "sid": self.sid + } + + # В текущей версии просто возвращаем заглушку + return { + "volumes": [], + "disks": [], + "total_size": 0, + "total_used": 0, + "error": "not_implemented" + } + except Exception as e: + logger.error(f"Error in get_storage_status: {str(e)}") + return {"error": str(e)} + + def get_security_status(self) -> Dict[str, Any]: + """Получение информации о состоянии безопасности (заглушка)""" + logger.warning("Function get_security_status() is not implemented yet") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for security status request") + return {"success": False, "error": "authentication_failed"} + + try: + # Здесь будет реализация после добавления соответствующего API + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.Core.SecurityScan.Status", + "version": "1", + "method": "get", + "sid": self.sid + } + + # В текущей версии просто возвращаем заглушку + return { + "success": False, + "status": "not_implemented", + "last_check": None, + "is_secure": False, + "error": "not_implemented" + } + except Exception as e: + logger.error(f"Error in get_security_status: {str(e)}") + return {"success": False, "error": str(e)} diff --git a/.history/src/api/synology_20250830081538.py b/.history/src/api/synology_20250830081538.py new file mode 100644 index 0000000..8b91197 --- /dev/null +++ b/.history/src/api/synology_20250830081538.py @@ -0,0 +1,956 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Модуль для взаимодействия с API Synology NAS +""" + +import requests +from requests.adapters import HTTPAdapter +import json +import logging +import time +import urllib3 +from urllib3.util import Retry +from typing import Dict, Any, Optional, List +import socket +import struct +from time import sleep + +from src.config.config import ( + SYNOLOGY_HOST, + SYNOLOGY_PORT, + SYNOLOGY_USERNAME, + SYNOLOGY_PASSWORD, + SYNOLOGY_SECURE, + SYNOLOGY_TIMEOUT, + SYNOLOGY_MAC, + WOL_PORT +) + +# Отключение предупреждений о небезопасных SSL-соединениях +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + +logger = logging.getLogger(__name__) + +class SynologyAPI: + """Класс для взаимодействия с API Synology NAS""" + + def __init__(self): + """Инициализация класса SynologyAPI""" + logger.info("Creating API with auto-retry and connection pool") + logger.debug(f"Connection details: Host={SYNOLOGY_HOST}, Port={SYNOLOGY_PORT}, Secure={SYNOLOGY_SECURE}") + + self.protocol = "https" if SYNOLOGY_SECURE else "http" + self.base_url = f"{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi" + self.sid = None + self.session = requests.Session() + + # Настройка SSL + if self.protocol == "https": + logger.debug("SSL enabled, disabling certificate verification for internal network") + self.session.verify = False # Отключаем проверку SSL для внутренней сети + + # Добавляем повторные попытки для HTTP-запросов + retry_strategy = Retry( + total=5, # Увеличиваем количество попыток + status_forcelist=[429, 500, 502, 503, 504, 404], + allowed_methods=["GET", "POST"], + backoff_factor=1.5, # Увеличиваем задержку между попытками + respect_retry_after_header=True + ) + adapter = HTTPAdapter( + max_retries=retry_strategy, + pool_connections=3, + pool_maxsize=10 + ) + self.session.mount("http://", adapter) + self.session.mount("https://", adapter) + + # Таймауты будут указаны в запросах + self.default_timeout = (SYNOLOGY_TIMEOUT, SYNOLOGY_TIMEOUT*2) + logger.debug(f"Setting default request timeout: {self.default_timeout}") + + # Кэш для хранения результатов запросов + self._cache = {} + self._cache_ttl = {} + self._last_online_check = 0 + self._last_online_status = False + self._online_check_interval = 30 # Интервал для кэширования проверок онлайн-статуса + + # Время последней успешной аутентификации и срок действия сессии + self._last_auth_time = 0 + self._auth_expiry = 3600 # По умолчанию 1 час + + def login(self) -> bool: + """Авторизация в API Synology NAS""" + # Проверяем, действительна ли текущая сессия + current_time = time.time() + if self.sid and (current_time - self._last_auth_time) < self._auth_expiry: + logger.debug("Using existing session, still valid") + return True + + logger.info("Attempting to authenticate with Synology NAS...") + logger.debug(f"Base URL: {self.base_url}") + + # Проверяем доступность NAS перед запросом с помощью прямой TCP-проверки + # Избегаем вызова is_online(), чтобы не создавать рекурсию + online_status = self._check_tcp_connection() + if not online_status: + logger.error("Cannot login: Synology NAS is not reachable") + return False + + try: + # Проверка информации API для определения доступных версий API + api_info_url = f"{self.base_url}/entry.cgi" + api_info_params = { + "api": "SYNO.API.Info", + "version": "1", + "method": "query", + "query": "SYNO.API.Auth" + } + + logger.debug("Querying API info to determine optimal auth version") + try: + api_info_response = self.session.get( + api_info_url, + params=api_info_params, + timeout=self.default_timeout, + verify=False + ) + + if api_info_response.status_code == 200: + api_info_data = api_info_response.json() + if api_info_data.get("success"): + auth_info = api_info_data.get("data", {}).get("SYNO.API.Auth", {}) + max_version = auth_info.get("maxVersion", 3) + min_version = auth_info.get("minVersion", 1) + auth_path = auth_info.get("path", "auth.cgi") + logger.debug(f"Auth API versions: min={min_version}, max={max_version}, path={auth_path}") + + # Используем максимально поддерживаемую версию, но не выше 6 + auth_version = min(max_version, 6) + else: + logger.warning("Failed to query API info, using default auth version 3") + auth_version = 3 + auth_path = "auth.cgi" + else: + logger.warning(f"API info request failed with status {api_info_response.status_code}, using default auth version 3") + auth_version = 3 + auth_path = "auth.cgi" + except Exception as e: + logger.warning(f"Error querying API info: {str(e)}, using default auth version 3") + auth_version = 3 + auth_path = "auth.cgi" + + # Основной запрос авторизации + url = f"{self.base_url}/{auth_path}" + params = { + "api": "SYNO.API.Auth", + "version": str(auth_version), + "method": "login", + "account": SYNOLOGY_USERNAME, + "passwd": SYNOLOGY_PASSWORD, + "session": "SynologyPowerControlBot", + "format": "cookie" + } + + # Для версии 6+ используем немного другой формат + if auth_version >= 6: + params["enable_syno_token"] = "yes" + + logger.debug(f"Sending auth request to {url} with API version {auth_version}") + start_time = time.time() + response = self.session.get( + url, + params=params, + timeout=self.default_timeout, + verify=False + ) + elapsed_time = time.time() - start_time + logger.debug(f"Auth request completed in {elapsed_time:.2f}s with status code {response.status_code}") + + # Подробная информация для отладки + logger.debug(f"Auth response: {response.text[:500]}") + + # Проверка статуса HTTP + if response.status_code != 200: + logger.error(f"HTTP error: {response.status_code}") + logger.debug(f"Response content: {response.text[:200]}") + return False + + try: + data = response.json() + except json.JSONDecodeError: + logger.error("Failed to decode JSON response") + logger.debug(f"Response content: {response.text[:200]}") + return False + + if data.get("success"): + self.sid = data.get("data", {}).get("sid") + self._last_auth_time = current_time + logger.info("Successfully logged in to Synology NAS") + logger.debug(f"Session ID obtained: {self.sid[:5] if self.sid else 'None'}...") # Показываем только начало SID для безопасности + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to log in to Synology NAS: Error code {error_code}") + + # Дополнительная диагностика + if error_code == 400: + logger.error("Authentication error: Invalid credentials") + elif error_code == 401: + logger.error("Authentication error: Account disabled") + elif error_code == 402: + logger.error("Authentication error: Permission denied") + elif error_code == 403: + logger.error("Authentication error: 2-factor authentication required") + elif error_code == 404: + logger.error("Authentication error: Failed to authenticate with 2-factor authentication") + + return False + + except requests.exceptions.Timeout: + logger.error(f"Connection timeout after {SYNOLOGY_TIMEOUT} seconds") + return False + except requests.exceptions.ConnectionError as e: + logger.error(f"Connection error: {str(e)}") + logger.debug(f"Connection details: {self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}") + return False + except requests.RequestException as e: + logger.error(f"Request error: {str(e)}") + return False + except Exception as e: + logger.error(f"Unexpected error during login: {str(e)}", exc_info=True) + return False + + def logout(self) -> bool: + """Выход из API Synology NAS""" + if not self.sid: + return True + + try: + url = f"{self.base_url}/auth.cgi" + params = { + "api": "SYNO.API.Auth", + "version": "1", + "method": "logout", + "session": "SynologyPowerControlBot", + "_sid": self.sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + self.sid = None + logger.info("Successfully logged out from Synology NAS") + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to log out from Synology NAS: Error code {error_code}") + return False + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + return False + + def _make_api_request(self, api_name: str, method: str, version: int = 1, + params: Optional[Dict[str, Any]] = None, retry_auth: bool = True) -> Optional[Dict[str, Any]]: + """Обобщенный метод для выполнения API запросов с обработкой ошибок""" + # Проверка наличия авторизации + if not self.sid and not self.login(): + logger.error(f"Not authenticated for API request: {api_name}.{method}") + return None + + # Проверка информации API для определения пути и поддерживаемой версии + api_info_url = f"{self.base_url}/entry.cgi" + api_info_params = { + "api": "SYNO.API.Info", + "version": "1", + "method": "query", + "query": api_name + } + + api_path = "entry.cgi" + try: + logger.debug(f"Querying API info for {api_name}") + api_info_response = self.session.get( + api_info_url, + params=api_info_params, + timeout=self.default_timeout, + verify=False + ) + + if api_info_response.status_code == 200: + api_info_data = api_info_response.json() + if api_info_data.get("success"): + api_info = api_info_data.get("data", {}).get(api_name, {}) + if api_info: + max_version = api_info.get("maxVersion", version) + min_version = api_info.get("minVersion", version) + api_path = api_info.get("path", "entry.cgi") + + # Проверка, поддерживается ли запрошенная версия + if version < min_version: + logger.warning(f"API version {version} for {api_name} is below minimum {min_version}, using {min_version}") + version = min_version + elif version > max_version: + logger.warning(f"API version {version} for {api_name} exceeds maximum {max_version}, using {max_version}") + version = max_version + + logger.debug(f"Using API path: {api_path}, version: {version}") + else: + logger.warning(f"API {api_name} not found in API info, using defaults") + except Exception as e: + logger.warning(f"Error querying API info for {api_name}: {str(e)}, using defaults") + + # Подготовка базовых параметров запроса + base_params = { + "api": api_name, + "version": str(version), + "method": method, + "sid": self.sid + } + + # Добавление дополнительных параметров, если они заданы + if params: + base_params.update(params) + + url = f"{self.base_url}/{api_path}" + logger.debug(f"API request: {api_name}.{method} v{version} to {url}") + logger.debug(f"Full request params: {base_params}") + + try: + start_time = time.time() + response = self.session.get( + url, + params=base_params, + timeout=self.default_timeout, + verify=False + ) + elapsed_time = time.time() - start_time + logger.debug(f"API request completed in {elapsed_time:.2f}s with status {response.status_code}") + + # Подробное логирование для отладки + logger.debug(f"Response: {response.text[:500]}") + + # Проверка статуса HTTP + if response.status_code != 200: + logger.error(f"HTTP error: {response.status_code} for {api_name}.{method}") + return None + + try: + data = response.json() + except json.JSONDecodeError: + logger.error(f"Failed to decode JSON response for {api_name}.{method}") + logger.debug(f"Response content: {response.text[:200]}") + return None + + if data.get("success"): + logger.info(f"API request successful for {api_name}.{method}") + return data.get("data", {}) + else: + error_code = data.get("error", {}).get("code", -1) + error_desc = self._get_error_description(error_code) + logger.error(f"API error for {api_name}.{method}: {error_code} - {error_desc}") + + # Ошибки доступа или прав часто встречаются, но они не критичные + # Например, ошибка 102 означает, что нет прав, но NAS доступен + if error_code in [102, 103, 104, 105]: + logger.warning(f"Permission-related error for {api_name}.{method}: {error_code}") + # Возвращаем пустой словарь вместо None, + # чтобы вызывающий код мог понять, что запрос выполнен + return {} + + # Если ошибка связана с авторизацией и нам разрешено повторить попытку + if error_code in [106, 107, 119] and retry_auth: + logger.info("Session may have expired, re-authenticating...") + self.sid = None # Сбрасываем SID + if self.login(): + logger.info("Re-authenticated, retrying API request...") + # Рекурсивный вызов, но без повторной авторизации во избежание зацикливания + return self._make_api_request(api_name, method, version, params, False) + + return None + + except requests.exceptions.Timeout: + logger.error(f"Request timeout for {api_name}.{method}") + return None + except requests.exceptions.ConnectionError as e: + logger.error(f"Connection error for {api_name}.{method}: {str(e)}") + return None + except Exception as e: + logger.error(f"Unexpected error during {api_name}.{method}: {str(e)}") + return None + + def get_system_status(self) -> Dict[str, Any]: + """Получение статуса системы""" + # Проверяем доступность системы + if not self.is_online(): + logger.info("Device is offline, skipping API request") + return {"status": "offline"} + + # Проверяем, есть ли кэшированный результат + cache_key = "system_status" + current_time = time.time() + if cache_key in self._cache and current_time - self._cache_ttl.get(cache_key, 0) < 60: + logger.debug("Using cached system status") + return self._cache[cache_key] + + # Пробуем разные API для получения информации о системе + apis_to_try = [ + {"name": "SYNO.DSM.Info", "method": "getinfo", "version": 2}, + {"name": "SYNO.Core.System", "method": "info", "version": 1}, + {"name": "SYNO.Core.System.Status", "method": "get", "version": 1}, + {"name": "SYNO.Core.System.Info", "method": "get", "version": 1}, + ] + + for api in apis_to_try: + logger.info(f"Trying to get system info using {api['name']}.{api['method']} v{api['version']}") + result = self._make_api_request(api["name"], api["method"], version=api["version"]) + + if result: + logger.info(f"Successfully retrieved system info using {api['name']}") + + # Формируем расширенный ответ с дополнительной информацией + system_info = { + "status": "online", + "hostname": result.get("hostname", "unknown"), + "model": result.get("model", "unknown"), + "version": result.get("version", "unknown"), + "uptime": result.get("uptime", 0), + "time": current_time, + "is_online": True, + "api_used": api["name"] + } + + # Сохраняем в кэше + self._cache[cache_key] = system_info + self._cache_ttl[cache_key] = current_time + + return system_info + + # Если все запросы не удались, но система онлайн, возвращаем базовую информацию + logger.warning("Failed to retrieve system info with all API methods") + return { + "status": "error", + "error": "Failed to fetch system information", + "is_online": True, + "time": current_time + } + + def shutdown_system(self) -> bool: + """Выключение системы""" + # Проверяем, включено ли устройство перед попыткой его выключить + if not self.is_online(force_check=True): + logger.info("Synology NAS is already offline, no need to shut down") + return True + + logger.info("Attempting to shutdown Synology NAS...") + + # Проверка всех доступных методов API для выключения + # Проверяем наличие API перед использованием + apis_to_try = [ + {"name": "SYNO.Core.System.Power", "method": "shutdown", "version": 1}, + {"name": "SYNO.DSM.Power", "method": "shutdown", "version": 1}, + {"name": "SYNO.Core.System", "method": "shutdown", "version": 3}, + {"name": "SYNO.System.Power", "method": "shutdown", "version": 1}, + {"name": "SYNO.DSM.System", "method": "shutdown", "version": 1} + ] + + # Получаем список доступных API + try: + api_info_url = f"{self.base_url}/entry.cgi" + api_info_params = { + "api": "SYNO.API.Info", + "version": "1", + "method": "query", + "query": "SYNO.Core.System.Power,SYNO.DSM.Power,SYNO.Core.System,SYNO.System.Power,SYNO.DSM.System" + } + + logger.debug("Checking available shutdown APIs") + api_info_response = self.session.get( + api_info_url, + params=api_info_params, + timeout=self.default_timeout, + verify=False + ) + + if api_info_response.status_code == 200: + api_info_data = api_info_response.json() + if api_info_data.get("success"): + available_apis = api_info_data.get("data", {}) + logger.debug(f"Available APIs: {list(available_apis.keys())}") + + # Фильтруем только доступные API + filtered_apis = [] + for api in apis_to_try: + if api["name"] in available_apis: + api_info = available_apis[api["name"]] + # Проверка версии API + if api_info.get("minVersion", 1) <= api["version"] <= api_info.get("maxVersion", 1): + filtered_apis.append(api) + logger.debug(f"Adding {api['name']} to available shutdown APIs") + else: + logger.debug(f"API {api['name']} version {api['version']} not supported. Supported: {api_info.get('minVersion')}-{api_info.get('maxVersion')}") + + if filtered_apis: + apis_to_try = filtered_apis + else: + logger.warning("No compatible APIs found, trying all methods as fallback") + else: + logger.warning("Failed to query API info, using default methods") + else: + logger.warning(f"API info request failed with status {api_info_response.status_code}, using default methods") + except Exception as e: + logger.warning(f"Error querying API info: {str(e)}, using default methods") + + # Пробуем все доступные методы по порядку + for api in apis_to_try: + logger.info(f"Trying shutdown with {api['name']}.{api['method']} v{api['version']}") + result = self._make_api_request(api["name"], api["method"], version=api["version"]) + + if result is not None: + logger.info(f"Successfully initiated system shutdown using {api['name']}") + + # Даем системе время начать процесс выключения + logger.info("Waiting for shutdown to initialize...") + sleep(5) + + # Проверяем статус + if not self.is_online(force_check=True): + logger.info("System is now offline. Shutdown confirmed successful.") + return True + else: + logger.info("System still appears to be online, but shutdown may be in progress.") + return True + else: + logger.warning(f"Failed to shutdown using {api['name']}.{api['method']} v{api['version']}") + + # Если ни один метод не сработал, но система стала недоступна + if not self.is_online(force_check=True): + logger.info("System appears to be shutting down despite API errors") + return True + + logger.error("Failed to shutdown system after trying multiple APIs") + return False + + def reboot_system(self) -> bool: + """Перезагрузка системы""" + # Проверяем, включена ли система + if not self.is_online(force_check=True): + logger.error("Cannot reboot: System is offline") + return False + + logger.info("Attempting to reboot Synology NAS...") + + # Проверка всех доступных методов API для перезагрузки + apis_to_try = [ + {"name": "SYNO.Core.System.Power", "method": "restart", "version": 1}, + {"name": "SYNO.DSM.Power", "method": "reboot", "version": 1}, + {"name": "SYNO.Core.System", "method": "reboot", "version": 3}, + {"name": "SYNO.System.Power", "method": "reboot", "version": 1}, + {"name": "SYNO.DSM.System", "method": "reboot", "version": 1} + ] + + # Получаем список доступных API + try: + api_info_url = f"{self.base_url}/entry.cgi" + api_info_params = { + "api": "SYNO.API.Info", + "version": "1", + "method": "query", + "query": "SYNO.Core.System.Power,SYNO.DSM.Power,SYNO.Core.System,SYNO.System.Power,SYNO.DSM.System" + } + + logger.debug("Checking available reboot APIs") + api_info_response = self.session.get( + api_info_url, + params=api_info_params, + timeout=self.default_timeout, + verify=False + ) + + if api_info_response.status_code == 200: + api_info_data = api_info_response.json() + if api_info_data.get("success"): + available_apis = api_info_data.get("data", {}) + logger.debug(f"Available APIs: {list(available_apis.keys())}") + + # Фильтруем только доступные API + filtered_apis = [] + for api in apis_to_try: + if api["name"] in available_apis: + api_info = available_apis[api["name"]] + # Проверка версии API + if api_info.get("minVersion", 1) <= api["version"] <= api_info.get("maxVersion", 1): + filtered_apis.append(api) + logger.debug(f"Adding {api['name']} to available reboot APIs") + else: + logger.debug(f"API {api['name']} version {api['version']} not supported. Supported: {api_info.get('minVersion')}-{api_info.get('maxVersion')}") + + if filtered_apis: + apis_to_try = filtered_apis + else: + logger.warning("No compatible APIs found, trying all methods as fallback") + else: + logger.warning("Failed to query API info, using default methods") + else: + logger.warning(f"API info request failed with status {api_info_response.status_code}, using default methods") + except Exception as e: + logger.warning(f"Error querying API info: {str(e)}, using default methods") + + # Пробуем все доступные методы по порядку + for api in apis_to_try: + logger.info(f"Trying reboot with {api['name']}.{api['method']} v{api['version']}") + result = self._make_api_request(api["name"], api["method"], version=api["version"]) + + if result is not None: + logger.info(f"Successfully initiated system reboot using {api['name']}") + + # Даем системе время начать процесс перезагрузки + logger.info("Waiting for reboot to initialize...") + sleep(5) + + # Ждем, пока система станет недоступна (признак перезагрузки) + reboot_started = False + for i in range(12): # Проверяем в течение 60 секунд (12 * 5сек) + if not self.is_online(force_check=True): + logger.info(f"System went offline after {i*5} seconds, reboot in progress") + reboot_started = True + break + logger.debug(f"System still online, waiting... ({i+1}/12)") + sleep(5) + + if reboot_started: + # Ждем, пока система снова станет доступна + logger.info("Waiting for system to come back online...") + return self.wait_for_boot(max_attempts=30, delay=10) + else: + logger.warning("System did not go offline after reboot command, but command was accepted") + # Даже если система не ушла оффлайн, команда могла быть принята + return True + else: + logger.warning(f"Failed to reboot using {api['name']}.{api['method']} v{api['version']}") + + logger.error("Failed to reboot system after trying multiple APIs") + return False + + def _get_error_description(self, error_code: int) -> str: + """Получение описания ошибки по коду""" + error_descriptions = { + 100: "Unknown error", + 101: "Invalid parameter", + 102: "API does not exist", + 103: "Method does not exist", + 104: "Version does not support", + 105: "Permission denied", + 106: "Session timeout", + 107: "Session interrupted by duplicate login", + 400: "Invalid credentials", + 401: "Account disabled", + 402: "Permission denied", + 403: "2FA required", + 404: "Failed to authenticate with 2FA" + } + return error_descriptions.get(error_code, "Unknown error code") + + def _check_tcp_connection(self) -> bool: + """Проверка базового TCP-соединения с Synology NAS""" + try: + socket_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + socket_obj.settimeout(SYNOLOGY_TIMEOUT) + result = socket_obj.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT)) + socket_obj.close() + + return result == 0 + except socket.error as e: + logger.error(f"Socket error during connection check: {str(e)}") + return False + except Exception as e: + logger.error(f"Unexpected error during connection check: {str(e)}") + return False + + def is_online(self, force_check=False) -> bool: + """Проверка онлайн-статуса Synology NAS""" + # Используем кэшированное значение, если доступно и не устарело + current_time = time.time() + if not force_check and (current_time - self._last_online_check) < self._online_check_interval: + logger.debug(f"Using cached online status: {self._last_online_status}") + return self._last_online_status + + logger.info("Checking if NAS is online...") + + # Проверяем TCP-соединение + online_status = self._check_tcp_connection() + logger.info(f"Detected Synology NAS online status: {online_status}") + + # Если TCP-соединение успешно и у нас есть действующий SID, + # попробуем более детальную проверку через API + if online_status and self.sid: + logger.info("Trying to fetch more detailed online status through API...") + try: + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.DSM.Info", + "version": "1", + "method": "getinfo", + "sid": self.sid + } + + response = self.session.get( + url, + params=params, + timeout=self.default_timeout, + verify=False + ) + + if response.status_code == 200: + data = response.json() + if data.get("success"): + logger.info("API request successful for getinfo") + logger.info("Synology NAS is online with API access") + else: + logger.warning("API response indicates an error, but NAS is reachable") + logger.debug(f"API error: {data.get('error', {}).get('code', -1)}") + else: + logger.warning(f"API returned status code {response.status_code}") + except Exception as e: + logger.warning(f"API check failed, but TCP connection succeeded: {str(e)}") + + # Обновляем кэшированное значение + self._last_online_check = current_time + self._last_online_status = online_status + + return online_status + + def wake_on_lan(self) -> bool: + """Отправка Wake-on-LAN пакета для включения Synology NAS""" + if not SYNOLOGY_MAC: + logger.error("MAC address not configured") + return False + + try: + # Преобразование MAC-адреса в байты + mac_address = SYNOLOGY_MAC.replace(':', '').replace('-', '') + if len(mac_address) != 12: + logger.error(f"Invalid MAC address format: {SYNOLOGY_MAC}") + return False + + try: + mac_bytes = bytes.fromhex(mac_address) + except ValueError as e: + logger.error(f"Failed to parse MAC address: {str(e)}") + logger.error(f"MAC address must contain only hex characters: {SYNOLOGY_MAC}") + return False + + # Создание Magic Packet + magic_packet = b'\xff' * 6 + mac_bytes * 16 + + # Отправка пакета на конкретный адрес + try: + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) + sock.sendto(magic_packet, (SYNOLOGY_HOST, WOL_PORT)) + sock.close() + logger.info(f"Wake-on-LAN packet sent to {SYNOLOGY_MAC} at {SYNOLOGY_HOST}:{WOL_PORT}") + except Exception as e: + logger.error(f"Error sending directed WoL packet: {str(e)}") + return False + + # Для надежности отправляем также широковещательный пакет + try: + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) + + # Используем стандартный широковещательный адрес + broadcast_addr = "255.255.255.255" + sock.sendto(magic_packet, (broadcast_addr, WOL_PORT)) + sock.close() + logger.info(f"Wake-on-LAN broadcast packet sent to {broadcast_addr}:{WOL_PORT}") + except Exception as e: + logger.warning(f"Error sending broadcast WoL packet (non-critical): {str(e)}") + # Не считаем ошибкой, т.к. основной пакет уже отправлен + + return True + + except Exception as e: + logger.error(f"Unexpected error in wake_on_lan: {str(e)}") + return False + + def wait_for_boot(self, max_attempts: int = 30, delay: int = 10) -> bool: + """Ожидание загрузки Synology NAS""" + logger.info(f"Waiting for Synology NAS to boot (timeout: {max_attempts * delay}s)...") + + for attempt in range(max_attempts): + # Принудительно проверяем статус без использования кэша + if self.is_online(force_check=True): + logger.info(f"Synology NAS is online after {attempt + 1} attempts ({(attempt + 1) * delay}s)") + + # Проверяем, что не только сеть доступна, но и API загрузился + api_ready = False + logger.info("Waiting for API services to initialize...") + + for api_check in range(5): # Даем еще до 50 секунд для загрузки API + if self.sid or self.login(): + api_ready = True + logger.info(f"API services are ready after {api_check + 1} attempts") + break + logger.debug(f"API not ready yet, waiting... ({api_check + 1}/5)") + sleep(10) + + if not api_ready: + logger.warning("System is online but API services may not be fully initialized") + + # Дадим дополнительное время для полной загрузки всех сервисов + sleep(delay) + return True + + sleep(delay) + logger.info(f"Waiting for boot... Attempt {attempt + 1}/{max_attempts}") + + logger.error(f"Synology NAS didn't come online after {max_attempts * delay} seconds") + return False + + def power_on(self) -> bool: + """Включение Synology NAS""" + # Принудительная проверка статуса + if self.is_online(force_check=True): + logger.info("Synology NAS is already online") + return True + + logger.info("Powering on Synology NAS via Wake-on-LAN...") + + # Проверяем, настроен ли MAC-адрес + if not SYNOLOGY_MAC: + logger.error("Cannot power on: MAC address not configured in settings") + return False + + # Пробуем отправить несколько WoL пакетов для надежности + success = False + for attempt in range(3): + logger.debug(f"Sending WoL packet, attempt {attempt + 1}/3") + if self.wake_on_lan(): + success = True + break + sleep(1) + + if not success: + logger.error("Failed to send Wake-on-LAN packets") + return False + + # Ожидание загрузки + logger.info("WoL packets sent successfully, waiting for system to boot...") + boot_result = self.wait_for_boot(max_attempts=30, delay=10) + + if boot_result: + # Проверяем доступность API после загрузки + system_status = self.get_system_status() + if system_status.get("status") == "online": + logger.info("System booted successfully with API access") + return True + else: + logger.warning("System appears to be online but API may not be fully ready") + return True + else: + logger.error("System did not come online after WoL") + return False + + def power_off(self) -> bool: + """Выключение Synology NAS""" + if not self.is_online(force_check=True): + logger.info("Synology NAS is already offline") + return True + + logger.info("Powering off Synology NAS...") + result = self.shutdown_system() + + if result: + # Дополнительная проверка, что система действительно выключилась + logger.info("Verifying system is offline...") + for attempt in range(12): # Проверяем в течение 2 минут (12 * 10 сек) + if not self.is_online(force_check=True): + logger.info(f"System confirmed offline after {attempt * 10} seconds") + return True + logger.debug(f"System still shutting down, checking again... ({attempt + 1}/12)") + sleep(10) + + logger.warning("System still appears to be online after shutdown command") + return False + else: + logger.error("Failed to initiate shutdown") + return False + + # Заглушки для расширенных методов + def get_shared_folders(self) -> List[Dict[str, Any]]: + """Получение списка общих папок (заглушка)""" + logger.warning("Function get_shared_folders() is not implemented yet") + return [] + + def get_system_load(self) -> Dict[str, Any]: + """Получение информации о загрузке системы (заглушка)""" + logger.warning("Function get_system_load() is not implemented yet") + return {} + + def get_storage_status(self) -> Dict[str, Any]: + """Получение подробной информации о хранилище (заглушка)""" + logger.warning("Function get_storage_status() is not implemented yet") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for storage status request") + return {"error": "authentication_failed"} + + try: + # Здесь будет реализация после добавления соответствующего API + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.Storage.CGI.Storage", + "version": "1", + "method": "load_info", + "sid": self.sid + } + + # В текущей версии просто возвращаем заглушку + return { + "volumes": [], + "disks": [], + "total_size": 0, + "total_used": 0, + "error": "not_implemented" + } + except Exception as e: + logger.error(f"Error in get_storage_status: {str(e)}") + return {"error": str(e)} + + def get_security_status(self) -> Dict[str, Any]: + """Получение информации о состоянии безопасности (заглушка)""" + logger.warning("Function get_security_status() is not implemented yet") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for security status request") + return {"success": False, "error": "authentication_failed"} + + try: + # Здесь будет реализация после добавления соответствующего API + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.Core.SecurityScan.Status", + "version": "1", + "method": "get", + "sid": self.sid + } + + # В текущей версии просто возвращаем заглушку + return { + "success": False, + "status": "not_implemented", + "last_check": None, + "is_secure": False, + "error": "not_implemented" + } + except Exception as e: + logger.error(f"Error in get_security_status: {str(e)}") + return {"success": False, "error": str(e)} diff --git a/.history/src/api/synology_20250830081615.py b/.history/src/api/synology_20250830081615.py new file mode 100644 index 0000000..8b91197 --- /dev/null +++ b/.history/src/api/synology_20250830081615.py @@ -0,0 +1,956 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Модуль для взаимодействия с API Synology NAS +""" + +import requests +from requests.adapters import HTTPAdapter +import json +import logging +import time +import urllib3 +from urllib3.util import Retry +from typing import Dict, Any, Optional, List +import socket +import struct +from time import sleep + +from src.config.config import ( + SYNOLOGY_HOST, + SYNOLOGY_PORT, + SYNOLOGY_USERNAME, + SYNOLOGY_PASSWORD, + SYNOLOGY_SECURE, + SYNOLOGY_TIMEOUT, + SYNOLOGY_MAC, + WOL_PORT +) + +# Отключение предупреждений о небезопасных SSL-соединениях +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + +logger = logging.getLogger(__name__) + +class SynologyAPI: + """Класс для взаимодействия с API Synology NAS""" + + def __init__(self): + """Инициализация класса SynologyAPI""" + logger.info("Creating API with auto-retry and connection pool") + logger.debug(f"Connection details: Host={SYNOLOGY_HOST}, Port={SYNOLOGY_PORT}, Secure={SYNOLOGY_SECURE}") + + self.protocol = "https" if SYNOLOGY_SECURE else "http" + self.base_url = f"{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi" + self.sid = None + self.session = requests.Session() + + # Настройка SSL + if self.protocol == "https": + logger.debug("SSL enabled, disabling certificate verification for internal network") + self.session.verify = False # Отключаем проверку SSL для внутренней сети + + # Добавляем повторные попытки для HTTP-запросов + retry_strategy = Retry( + total=5, # Увеличиваем количество попыток + status_forcelist=[429, 500, 502, 503, 504, 404], + allowed_methods=["GET", "POST"], + backoff_factor=1.5, # Увеличиваем задержку между попытками + respect_retry_after_header=True + ) + adapter = HTTPAdapter( + max_retries=retry_strategy, + pool_connections=3, + pool_maxsize=10 + ) + self.session.mount("http://", adapter) + self.session.mount("https://", adapter) + + # Таймауты будут указаны в запросах + self.default_timeout = (SYNOLOGY_TIMEOUT, SYNOLOGY_TIMEOUT*2) + logger.debug(f"Setting default request timeout: {self.default_timeout}") + + # Кэш для хранения результатов запросов + self._cache = {} + self._cache_ttl = {} + self._last_online_check = 0 + self._last_online_status = False + self._online_check_interval = 30 # Интервал для кэширования проверок онлайн-статуса + + # Время последней успешной аутентификации и срок действия сессии + self._last_auth_time = 0 + self._auth_expiry = 3600 # По умолчанию 1 час + + def login(self) -> bool: + """Авторизация в API Synology NAS""" + # Проверяем, действительна ли текущая сессия + current_time = time.time() + if self.sid and (current_time - self._last_auth_time) < self._auth_expiry: + logger.debug("Using existing session, still valid") + return True + + logger.info("Attempting to authenticate with Synology NAS...") + logger.debug(f"Base URL: {self.base_url}") + + # Проверяем доступность NAS перед запросом с помощью прямой TCP-проверки + # Избегаем вызова is_online(), чтобы не создавать рекурсию + online_status = self._check_tcp_connection() + if not online_status: + logger.error("Cannot login: Synology NAS is not reachable") + return False + + try: + # Проверка информации API для определения доступных версий API + api_info_url = f"{self.base_url}/entry.cgi" + api_info_params = { + "api": "SYNO.API.Info", + "version": "1", + "method": "query", + "query": "SYNO.API.Auth" + } + + logger.debug("Querying API info to determine optimal auth version") + try: + api_info_response = self.session.get( + api_info_url, + params=api_info_params, + timeout=self.default_timeout, + verify=False + ) + + if api_info_response.status_code == 200: + api_info_data = api_info_response.json() + if api_info_data.get("success"): + auth_info = api_info_data.get("data", {}).get("SYNO.API.Auth", {}) + max_version = auth_info.get("maxVersion", 3) + min_version = auth_info.get("minVersion", 1) + auth_path = auth_info.get("path", "auth.cgi") + logger.debug(f"Auth API versions: min={min_version}, max={max_version}, path={auth_path}") + + # Используем максимально поддерживаемую версию, но не выше 6 + auth_version = min(max_version, 6) + else: + logger.warning("Failed to query API info, using default auth version 3") + auth_version = 3 + auth_path = "auth.cgi" + else: + logger.warning(f"API info request failed with status {api_info_response.status_code}, using default auth version 3") + auth_version = 3 + auth_path = "auth.cgi" + except Exception as e: + logger.warning(f"Error querying API info: {str(e)}, using default auth version 3") + auth_version = 3 + auth_path = "auth.cgi" + + # Основной запрос авторизации + url = f"{self.base_url}/{auth_path}" + params = { + "api": "SYNO.API.Auth", + "version": str(auth_version), + "method": "login", + "account": SYNOLOGY_USERNAME, + "passwd": SYNOLOGY_PASSWORD, + "session": "SynologyPowerControlBot", + "format": "cookie" + } + + # Для версии 6+ используем немного другой формат + if auth_version >= 6: + params["enable_syno_token"] = "yes" + + logger.debug(f"Sending auth request to {url} with API version {auth_version}") + start_time = time.time() + response = self.session.get( + url, + params=params, + timeout=self.default_timeout, + verify=False + ) + elapsed_time = time.time() - start_time + logger.debug(f"Auth request completed in {elapsed_time:.2f}s with status code {response.status_code}") + + # Подробная информация для отладки + logger.debug(f"Auth response: {response.text[:500]}") + + # Проверка статуса HTTP + if response.status_code != 200: + logger.error(f"HTTP error: {response.status_code}") + logger.debug(f"Response content: {response.text[:200]}") + return False + + try: + data = response.json() + except json.JSONDecodeError: + logger.error("Failed to decode JSON response") + logger.debug(f"Response content: {response.text[:200]}") + return False + + if data.get("success"): + self.sid = data.get("data", {}).get("sid") + self._last_auth_time = current_time + logger.info("Successfully logged in to Synology NAS") + logger.debug(f"Session ID obtained: {self.sid[:5] if self.sid else 'None'}...") # Показываем только начало SID для безопасности + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to log in to Synology NAS: Error code {error_code}") + + # Дополнительная диагностика + if error_code == 400: + logger.error("Authentication error: Invalid credentials") + elif error_code == 401: + logger.error("Authentication error: Account disabled") + elif error_code == 402: + logger.error("Authentication error: Permission denied") + elif error_code == 403: + logger.error("Authentication error: 2-factor authentication required") + elif error_code == 404: + logger.error("Authentication error: Failed to authenticate with 2-factor authentication") + + return False + + except requests.exceptions.Timeout: + logger.error(f"Connection timeout after {SYNOLOGY_TIMEOUT} seconds") + return False + except requests.exceptions.ConnectionError as e: + logger.error(f"Connection error: {str(e)}") + logger.debug(f"Connection details: {self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}") + return False + except requests.RequestException as e: + logger.error(f"Request error: {str(e)}") + return False + except Exception as e: + logger.error(f"Unexpected error during login: {str(e)}", exc_info=True) + return False + + def logout(self) -> bool: + """Выход из API Synology NAS""" + if not self.sid: + return True + + try: + url = f"{self.base_url}/auth.cgi" + params = { + "api": "SYNO.API.Auth", + "version": "1", + "method": "logout", + "session": "SynologyPowerControlBot", + "_sid": self.sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + self.sid = None + logger.info("Successfully logged out from Synology NAS") + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to log out from Synology NAS: Error code {error_code}") + return False + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + return False + + def _make_api_request(self, api_name: str, method: str, version: int = 1, + params: Optional[Dict[str, Any]] = None, retry_auth: bool = True) -> Optional[Dict[str, Any]]: + """Обобщенный метод для выполнения API запросов с обработкой ошибок""" + # Проверка наличия авторизации + if not self.sid and not self.login(): + logger.error(f"Not authenticated for API request: {api_name}.{method}") + return None + + # Проверка информации API для определения пути и поддерживаемой версии + api_info_url = f"{self.base_url}/entry.cgi" + api_info_params = { + "api": "SYNO.API.Info", + "version": "1", + "method": "query", + "query": api_name + } + + api_path = "entry.cgi" + try: + logger.debug(f"Querying API info for {api_name}") + api_info_response = self.session.get( + api_info_url, + params=api_info_params, + timeout=self.default_timeout, + verify=False + ) + + if api_info_response.status_code == 200: + api_info_data = api_info_response.json() + if api_info_data.get("success"): + api_info = api_info_data.get("data", {}).get(api_name, {}) + if api_info: + max_version = api_info.get("maxVersion", version) + min_version = api_info.get("minVersion", version) + api_path = api_info.get("path", "entry.cgi") + + # Проверка, поддерживается ли запрошенная версия + if version < min_version: + logger.warning(f"API version {version} for {api_name} is below minimum {min_version}, using {min_version}") + version = min_version + elif version > max_version: + logger.warning(f"API version {version} for {api_name} exceeds maximum {max_version}, using {max_version}") + version = max_version + + logger.debug(f"Using API path: {api_path}, version: {version}") + else: + logger.warning(f"API {api_name} not found in API info, using defaults") + except Exception as e: + logger.warning(f"Error querying API info for {api_name}: {str(e)}, using defaults") + + # Подготовка базовых параметров запроса + base_params = { + "api": api_name, + "version": str(version), + "method": method, + "sid": self.sid + } + + # Добавление дополнительных параметров, если они заданы + if params: + base_params.update(params) + + url = f"{self.base_url}/{api_path}" + logger.debug(f"API request: {api_name}.{method} v{version} to {url}") + logger.debug(f"Full request params: {base_params}") + + try: + start_time = time.time() + response = self.session.get( + url, + params=base_params, + timeout=self.default_timeout, + verify=False + ) + elapsed_time = time.time() - start_time + logger.debug(f"API request completed in {elapsed_time:.2f}s with status {response.status_code}") + + # Подробное логирование для отладки + logger.debug(f"Response: {response.text[:500]}") + + # Проверка статуса HTTP + if response.status_code != 200: + logger.error(f"HTTP error: {response.status_code} for {api_name}.{method}") + return None + + try: + data = response.json() + except json.JSONDecodeError: + logger.error(f"Failed to decode JSON response for {api_name}.{method}") + logger.debug(f"Response content: {response.text[:200]}") + return None + + if data.get("success"): + logger.info(f"API request successful for {api_name}.{method}") + return data.get("data", {}) + else: + error_code = data.get("error", {}).get("code", -1) + error_desc = self._get_error_description(error_code) + logger.error(f"API error for {api_name}.{method}: {error_code} - {error_desc}") + + # Ошибки доступа или прав часто встречаются, но они не критичные + # Например, ошибка 102 означает, что нет прав, но NAS доступен + if error_code in [102, 103, 104, 105]: + logger.warning(f"Permission-related error for {api_name}.{method}: {error_code}") + # Возвращаем пустой словарь вместо None, + # чтобы вызывающий код мог понять, что запрос выполнен + return {} + + # Если ошибка связана с авторизацией и нам разрешено повторить попытку + if error_code in [106, 107, 119] and retry_auth: + logger.info("Session may have expired, re-authenticating...") + self.sid = None # Сбрасываем SID + if self.login(): + logger.info("Re-authenticated, retrying API request...") + # Рекурсивный вызов, но без повторной авторизации во избежание зацикливания + return self._make_api_request(api_name, method, version, params, False) + + return None + + except requests.exceptions.Timeout: + logger.error(f"Request timeout for {api_name}.{method}") + return None + except requests.exceptions.ConnectionError as e: + logger.error(f"Connection error for {api_name}.{method}: {str(e)}") + return None + except Exception as e: + logger.error(f"Unexpected error during {api_name}.{method}: {str(e)}") + return None + + def get_system_status(self) -> Dict[str, Any]: + """Получение статуса системы""" + # Проверяем доступность системы + if not self.is_online(): + logger.info("Device is offline, skipping API request") + return {"status": "offline"} + + # Проверяем, есть ли кэшированный результат + cache_key = "system_status" + current_time = time.time() + if cache_key in self._cache and current_time - self._cache_ttl.get(cache_key, 0) < 60: + logger.debug("Using cached system status") + return self._cache[cache_key] + + # Пробуем разные API для получения информации о системе + apis_to_try = [ + {"name": "SYNO.DSM.Info", "method": "getinfo", "version": 2}, + {"name": "SYNO.Core.System", "method": "info", "version": 1}, + {"name": "SYNO.Core.System.Status", "method": "get", "version": 1}, + {"name": "SYNO.Core.System.Info", "method": "get", "version": 1}, + ] + + for api in apis_to_try: + logger.info(f"Trying to get system info using {api['name']}.{api['method']} v{api['version']}") + result = self._make_api_request(api["name"], api["method"], version=api["version"]) + + if result: + logger.info(f"Successfully retrieved system info using {api['name']}") + + # Формируем расширенный ответ с дополнительной информацией + system_info = { + "status": "online", + "hostname": result.get("hostname", "unknown"), + "model": result.get("model", "unknown"), + "version": result.get("version", "unknown"), + "uptime": result.get("uptime", 0), + "time": current_time, + "is_online": True, + "api_used": api["name"] + } + + # Сохраняем в кэше + self._cache[cache_key] = system_info + self._cache_ttl[cache_key] = current_time + + return system_info + + # Если все запросы не удались, но система онлайн, возвращаем базовую информацию + logger.warning("Failed to retrieve system info with all API methods") + return { + "status": "error", + "error": "Failed to fetch system information", + "is_online": True, + "time": current_time + } + + def shutdown_system(self) -> bool: + """Выключение системы""" + # Проверяем, включено ли устройство перед попыткой его выключить + if not self.is_online(force_check=True): + logger.info("Synology NAS is already offline, no need to shut down") + return True + + logger.info("Attempting to shutdown Synology NAS...") + + # Проверка всех доступных методов API для выключения + # Проверяем наличие API перед использованием + apis_to_try = [ + {"name": "SYNO.Core.System.Power", "method": "shutdown", "version": 1}, + {"name": "SYNO.DSM.Power", "method": "shutdown", "version": 1}, + {"name": "SYNO.Core.System", "method": "shutdown", "version": 3}, + {"name": "SYNO.System.Power", "method": "shutdown", "version": 1}, + {"name": "SYNO.DSM.System", "method": "shutdown", "version": 1} + ] + + # Получаем список доступных API + try: + api_info_url = f"{self.base_url}/entry.cgi" + api_info_params = { + "api": "SYNO.API.Info", + "version": "1", + "method": "query", + "query": "SYNO.Core.System.Power,SYNO.DSM.Power,SYNO.Core.System,SYNO.System.Power,SYNO.DSM.System" + } + + logger.debug("Checking available shutdown APIs") + api_info_response = self.session.get( + api_info_url, + params=api_info_params, + timeout=self.default_timeout, + verify=False + ) + + if api_info_response.status_code == 200: + api_info_data = api_info_response.json() + if api_info_data.get("success"): + available_apis = api_info_data.get("data", {}) + logger.debug(f"Available APIs: {list(available_apis.keys())}") + + # Фильтруем только доступные API + filtered_apis = [] + for api in apis_to_try: + if api["name"] in available_apis: + api_info = available_apis[api["name"]] + # Проверка версии API + if api_info.get("minVersion", 1) <= api["version"] <= api_info.get("maxVersion", 1): + filtered_apis.append(api) + logger.debug(f"Adding {api['name']} to available shutdown APIs") + else: + logger.debug(f"API {api['name']} version {api['version']} not supported. Supported: {api_info.get('minVersion')}-{api_info.get('maxVersion')}") + + if filtered_apis: + apis_to_try = filtered_apis + else: + logger.warning("No compatible APIs found, trying all methods as fallback") + else: + logger.warning("Failed to query API info, using default methods") + else: + logger.warning(f"API info request failed with status {api_info_response.status_code}, using default methods") + except Exception as e: + logger.warning(f"Error querying API info: {str(e)}, using default methods") + + # Пробуем все доступные методы по порядку + for api in apis_to_try: + logger.info(f"Trying shutdown with {api['name']}.{api['method']} v{api['version']}") + result = self._make_api_request(api["name"], api["method"], version=api["version"]) + + if result is not None: + logger.info(f"Successfully initiated system shutdown using {api['name']}") + + # Даем системе время начать процесс выключения + logger.info("Waiting for shutdown to initialize...") + sleep(5) + + # Проверяем статус + if not self.is_online(force_check=True): + logger.info("System is now offline. Shutdown confirmed successful.") + return True + else: + logger.info("System still appears to be online, but shutdown may be in progress.") + return True + else: + logger.warning(f"Failed to shutdown using {api['name']}.{api['method']} v{api['version']}") + + # Если ни один метод не сработал, но система стала недоступна + if not self.is_online(force_check=True): + logger.info("System appears to be shutting down despite API errors") + return True + + logger.error("Failed to shutdown system after trying multiple APIs") + return False + + def reboot_system(self) -> bool: + """Перезагрузка системы""" + # Проверяем, включена ли система + if not self.is_online(force_check=True): + logger.error("Cannot reboot: System is offline") + return False + + logger.info("Attempting to reboot Synology NAS...") + + # Проверка всех доступных методов API для перезагрузки + apis_to_try = [ + {"name": "SYNO.Core.System.Power", "method": "restart", "version": 1}, + {"name": "SYNO.DSM.Power", "method": "reboot", "version": 1}, + {"name": "SYNO.Core.System", "method": "reboot", "version": 3}, + {"name": "SYNO.System.Power", "method": "reboot", "version": 1}, + {"name": "SYNO.DSM.System", "method": "reboot", "version": 1} + ] + + # Получаем список доступных API + try: + api_info_url = f"{self.base_url}/entry.cgi" + api_info_params = { + "api": "SYNO.API.Info", + "version": "1", + "method": "query", + "query": "SYNO.Core.System.Power,SYNO.DSM.Power,SYNO.Core.System,SYNO.System.Power,SYNO.DSM.System" + } + + logger.debug("Checking available reboot APIs") + api_info_response = self.session.get( + api_info_url, + params=api_info_params, + timeout=self.default_timeout, + verify=False + ) + + if api_info_response.status_code == 200: + api_info_data = api_info_response.json() + if api_info_data.get("success"): + available_apis = api_info_data.get("data", {}) + logger.debug(f"Available APIs: {list(available_apis.keys())}") + + # Фильтруем только доступные API + filtered_apis = [] + for api in apis_to_try: + if api["name"] in available_apis: + api_info = available_apis[api["name"]] + # Проверка версии API + if api_info.get("minVersion", 1) <= api["version"] <= api_info.get("maxVersion", 1): + filtered_apis.append(api) + logger.debug(f"Adding {api['name']} to available reboot APIs") + else: + logger.debug(f"API {api['name']} version {api['version']} not supported. Supported: {api_info.get('minVersion')}-{api_info.get('maxVersion')}") + + if filtered_apis: + apis_to_try = filtered_apis + else: + logger.warning("No compatible APIs found, trying all methods as fallback") + else: + logger.warning("Failed to query API info, using default methods") + else: + logger.warning(f"API info request failed with status {api_info_response.status_code}, using default methods") + except Exception as e: + logger.warning(f"Error querying API info: {str(e)}, using default methods") + + # Пробуем все доступные методы по порядку + for api in apis_to_try: + logger.info(f"Trying reboot with {api['name']}.{api['method']} v{api['version']}") + result = self._make_api_request(api["name"], api["method"], version=api["version"]) + + if result is not None: + logger.info(f"Successfully initiated system reboot using {api['name']}") + + # Даем системе время начать процесс перезагрузки + logger.info("Waiting for reboot to initialize...") + sleep(5) + + # Ждем, пока система станет недоступна (признак перезагрузки) + reboot_started = False + for i in range(12): # Проверяем в течение 60 секунд (12 * 5сек) + if not self.is_online(force_check=True): + logger.info(f"System went offline after {i*5} seconds, reboot in progress") + reboot_started = True + break + logger.debug(f"System still online, waiting... ({i+1}/12)") + sleep(5) + + if reboot_started: + # Ждем, пока система снова станет доступна + logger.info("Waiting for system to come back online...") + return self.wait_for_boot(max_attempts=30, delay=10) + else: + logger.warning("System did not go offline after reboot command, but command was accepted") + # Даже если система не ушла оффлайн, команда могла быть принята + return True + else: + logger.warning(f"Failed to reboot using {api['name']}.{api['method']} v{api['version']}") + + logger.error("Failed to reboot system after trying multiple APIs") + return False + + def _get_error_description(self, error_code: int) -> str: + """Получение описания ошибки по коду""" + error_descriptions = { + 100: "Unknown error", + 101: "Invalid parameter", + 102: "API does not exist", + 103: "Method does not exist", + 104: "Version does not support", + 105: "Permission denied", + 106: "Session timeout", + 107: "Session interrupted by duplicate login", + 400: "Invalid credentials", + 401: "Account disabled", + 402: "Permission denied", + 403: "2FA required", + 404: "Failed to authenticate with 2FA" + } + return error_descriptions.get(error_code, "Unknown error code") + + def _check_tcp_connection(self) -> bool: + """Проверка базового TCP-соединения с Synology NAS""" + try: + socket_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + socket_obj.settimeout(SYNOLOGY_TIMEOUT) + result = socket_obj.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT)) + socket_obj.close() + + return result == 0 + except socket.error as e: + logger.error(f"Socket error during connection check: {str(e)}") + return False + except Exception as e: + logger.error(f"Unexpected error during connection check: {str(e)}") + return False + + def is_online(self, force_check=False) -> bool: + """Проверка онлайн-статуса Synology NAS""" + # Используем кэшированное значение, если доступно и не устарело + current_time = time.time() + if not force_check and (current_time - self._last_online_check) < self._online_check_interval: + logger.debug(f"Using cached online status: {self._last_online_status}") + return self._last_online_status + + logger.info("Checking if NAS is online...") + + # Проверяем TCP-соединение + online_status = self._check_tcp_connection() + logger.info(f"Detected Synology NAS online status: {online_status}") + + # Если TCP-соединение успешно и у нас есть действующий SID, + # попробуем более детальную проверку через API + if online_status and self.sid: + logger.info("Trying to fetch more detailed online status through API...") + try: + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.DSM.Info", + "version": "1", + "method": "getinfo", + "sid": self.sid + } + + response = self.session.get( + url, + params=params, + timeout=self.default_timeout, + verify=False + ) + + if response.status_code == 200: + data = response.json() + if data.get("success"): + logger.info("API request successful for getinfo") + logger.info("Synology NAS is online with API access") + else: + logger.warning("API response indicates an error, but NAS is reachable") + logger.debug(f"API error: {data.get('error', {}).get('code', -1)}") + else: + logger.warning(f"API returned status code {response.status_code}") + except Exception as e: + logger.warning(f"API check failed, but TCP connection succeeded: {str(e)}") + + # Обновляем кэшированное значение + self._last_online_check = current_time + self._last_online_status = online_status + + return online_status + + def wake_on_lan(self) -> bool: + """Отправка Wake-on-LAN пакета для включения Synology NAS""" + if not SYNOLOGY_MAC: + logger.error("MAC address not configured") + return False + + try: + # Преобразование MAC-адреса в байты + mac_address = SYNOLOGY_MAC.replace(':', '').replace('-', '') + if len(mac_address) != 12: + logger.error(f"Invalid MAC address format: {SYNOLOGY_MAC}") + return False + + try: + mac_bytes = bytes.fromhex(mac_address) + except ValueError as e: + logger.error(f"Failed to parse MAC address: {str(e)}") + logger.error(f"MAC address must contain only hex characters: {SYNOLOGY_MAC}") + return False + + # Создание Magic Packet + magic_packet = b'\xff' * 6 + mac_bytes * 16 + + # Отправка пакета на конкретный адрес + try: + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) + sock.sendto(magic_packet, (SYNOLOGY_HOST, WOL_PORT)) + sock.close() + logger.info(f"Wake-on-LAN packet sent to {SYNOLOGY_MAC} at {SYNOLOGY_HOST}:{WOL_PORT}") + except Exception as e: + logger.error(f"Error sending directed WoL packet: {str(e)}") + return False + + # Для надежности отправляем также широковещательный пакет + try: + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) + + # Используем стандартный широковещательный адрес + broadcast_addr = "255.255.255.255" + sock.sendto(magic_packet, (broadcast_addr, WOL_PORT)) + sock.close() + logger.info(f"Wake-on-LAN broadcast packet sent to {broadcast_addr}:{WOL_PORT}") + except Exception as e: + logger.warning(f"Error sending broadcast WoL packet (non-critical): {str(e)}") + # Не считаем ошибкой, т.к. основной пакет уже отправлен + + return True + + except Exception as e: + logger.error(f"Unexpected error in wake_on_lan: {str(e)}") + return False + + def wait_for_boot(self, max_attempts: int = 30, delay: int = 10) -> bool: + """Ожидание загрузки Synology NAS""" + logger.info(f"Waiting for Synology NAS to boot (timeout: {max_attempts * delay}s)...") + + for attempt in range(max_attempts): + # Принудительно проверяем статус без использования кэша + if self.is_online(force_check=True): + logger.info(f"Synology NAS is online after {attempt + 1} attempts ({(attempt + 1) * delay}s)") + + # Проверяем, что не только сеть доступна, но и API загрузился + api_ready = False + logger.info("Waiting for API services to initialize...") + + for api_check in range(5): # Даем еще до 50 секунд для загрузки API + if self.sid or self.login(): + api_ready = True + logger.info(f"API services are ready after {api_check + 1} attempts") + break + logger.debug(f"API not ready yet, waiting... ({api_check + 1}/5)") + sleep(10) + + if not api_ready: + logger.warning("System is online but API services may not be fully initialized") + + # Дадим дополнительное время для полной загрузки всех сервисов + sleep(delay) + return True + + sleep(delay) + logger.info(f"Waiting for boot... Attempt {attempt + 1}/{max_attempts}") + + logger.error(f"Synology NAS didn't come online after {max_attempts * delay} seconds") + return False + + def power_on(self) -> bool: + """Включение Synology NAS""" + # Принудительная проверка статуса + if self.is_online(force_check=True): + logger.info("Synology NAS is already online") + return True + + logger.info("Powering on Synology NAS via Wake-on-LAN...") + + # Проверяем, настроен ли MAC-адрес + if not SYNOLOGY_MAC: + logger.error("Cannot power on: MAC address not configured in settings") + return False + + # Пробуем отправить несколько WoL пакетов для надежности + success = False + for attempt in range(3): + logger.debug(f"Sending WoL packet, attempt {attempt + 1}/3") + if self.wake_on_lan(): + success = True + break + sleep(1) + + if not success: + logger.error("Failed to send Wake-on-LAN packets") + return False + + # Ожидание загрузки + logger.info("WoL packets sent successfully, waiting for system to boot...") + boot_result = self.wait_for_boot(max_attempts=30, delay=10) + + if boot_result: + # Проверяем доступность API после загрузки + system_status = self.get_system_status() + if system_status.get("status") == "online": + logger.info("System booted successfully with API access") + return True + else: + logger.warning("System appears to be online but API may not be fully ready") + return True + else: + logger.error("System did not come online after WoL") + return False + + def power_off(self) -> bool: + """Выключение Synology NAS""" + if not self.is_online(force_check=True): + logger.info("Synology NAS is already offline") + return True + + logger.info("Powering off Synology NAS...") + result = self.shutdown_system() + + if result: + # Дополнительная проверка, что система действительно выключилась + logger.info("Verifying system is offline...") + for attempt in range(12): # Проверяем в течение 2 минут (12 * 10 сек) + if not self.is_online(force_check=True): + logger.info(f"System confirmed offline after {attempt * 10} seconds") + return True + logger.debug(f"System still shutting down, checking again... ({attempt + 1}/12)") + sleep(10) + + logger.warning("System still appears to be online after shutdown command") + return False + else: + logger.error("Failed to initiate shutdown") + return False + + # Заглушки для расширенных методов + def get_shared_folders(self) -> List[Dict[str, Any]]: + """Получение списка общих папок (заглушка)""" + logger.warning("Function get_shared_folders() is not implemented yet") + return [] + + def get_system_load(self) -> Dict[str, Any]: + """Получение информации о загрузке системы (заглушка)""" + logger.warning("Function get_system_load() is not implemented yet") + return {} + + def get_storage_status(self) -> Dict[str, Any]: + """Получение подробной информации о хранилище (заглушка)""" + logger.warning("Function get_storage_status() is not implemented yet") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for storage status request") + return {"error": "authentication_failed"} + + try: + # Здесь будет реализация после добавления соответствующего API + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.Storage.CGI.Storage", + "version": "1", + "method": "load_info", + "sid": self.sid + } + + # В текущей версии просто возвращаем заглушку + return { + "volumes": [], + "disks": [], + "total_size": 0, + "total_used": 0, + "error": "not_implemented" + } + except Exception as e: + logger.error(f"Error in get_storage_status: {str(e)}") + return {"error": str(e)} + + def get_security_status(self) -> Dict[str, Any]: + """Получение информации о состоянии безопасности (заглушка)""" + logger.warning("Function get_security_status() is not implemented yet") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for security status request") + return {"success": False, "error": "authentication_failed"} + + try: + # Здесь будет реализация после добавления соответствующего API + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.Core.SecurityScan.Status", + "version": "1", + "method": "get", + "sid": self.sid + } + + # В текущей версии просто возвращаем заглушку + return { + "success": False, + "status": "not_implemented", + "last_check": None, + "is_secure": False, + "error": "not_implemented" + } + except Exception as e: + logger.error(f"Error in get_security_status: {str(e)}") + return {"success": False, "error": str(e)} diff --git a/.history/src/api/synology_20250830081654.py b/.history/src/api/synology_20250830081654.py new file mode 100644 index 0000000..cc303c7 --- /dev/null +++ b/.history/src/api/synology_20250830081654.py @@ -0,0 +1,972 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Модуль для взаимодействия с API Synology NAS +""" + +import requests +from requests.adapters import HTTPAdapter +import json +import logging +import time +import urllib3 +from urllib3.util import Retry +from typing import Dict, Any, Optional, List +import socket +import struct +from time import sleep + +from src.config.config import ( + SYNOLOGY_HOST, + SYNOLOGY_PORT, + SYNOLOGY_USERNAME, + SYNOLOGY_PASSWORD, + SYNOLOGY_SECURE, + SYNOLOGY_TIMEOUT, + SYNOLOGY_MAC, + WOL_PORT +) + +# Отключение предупреждений о небезопасных SSL-соединениях +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + +logger = logging.getLogger(__name__) + +class SynologyAPI: + """Класс для взаимодействия с API Synology NAS""" + + def __init__(self): + """Инициализация класса SynologyAPI""" + logger.info("Creating API with auto-retry and connection pool") + logger.debug(f"Connection details: Host={SYNOLOGY_HOST}, Port={SYNOLOGY_PORT}, Secure={SYNOLOGY_SECURE}") + + self.protocol = "https" if SYNOLOGY_SECURE else "http" + self.base_url = f"{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi" + self.sid = None + self.session = requests.Session() + + # Настройка SSL + if self.protocol == "https": + logger.debug("SSL enabled, disabling certificate verification for internal network") + self.session.verify = False # Отключаем проверку SSL для внутренней сети + + # Добавляем повторные попытки для HTTP-запросов + retry_strategy = Retry( + total=5, # Увеличиваем количество попыток + status_forcelist=[429, 500, 502, 503, 504, 404], + allowed_methods=["GET", "POST"], + backoff_factor=1.5, # Увеличиваем задержку между попытками + respect_retry_after_header=True + ) + adapter = HTTPAdapter( + max_retries=retry_strategy, + pool_connections=3, + pool_maxsize=10 + ) + self.session.mount("http://", adapter) + self.session.mount("https://", adapter) + + # Таймауты будут указаны в запросах + self.default_timeout = (SYNOLOGY_TIMEOUT, SYNOLOGY_TIMEOUT*2) + logger.debug(f"Setting default request timeout: {self.default_timeout}") + + # Кэш для хранения результатов запросов + self._cache = {} + self._cache_ttl = {} + self._last_online_check = 0 + self._last_online_status = False + self._online_check_interval = 30 # Интервал для кэширования проверок онлайн-статуса + + # Время последней успешной аутентификации и срок действия сессии + self._last_auth_time = 0 + self._auth_expiry = 3600 # По умолчанию 1 час + + def login(self) -> bool: + """Авторизация в API Synology NAS""" + # Проверяем, действительна ли текущая сессия + current_time = time.time() + if self.sid and (current_time - self._last_auth_time) < self._auth_expiry: + logger.debug("Using existing session, still valid") + return True + + logger.info("Attempting to authenticate with Synology NAS...") + logger.debug(f"Base URL: {self.base_url}") + + # Проверяем доступность NAS перед запросом с помощью прямой TCP-проверки + # Избегаем вызова is_online(), чтобы не создавать рекурсию + online_status = self._check_tcp_connection() + if not online_status: + logger.error("Cannot login: Synology NAS is not reachable") + return False + + try: + # Проверка информации API для определения доступных версий API + api_info_url = f"{self.base_url}/entry.cgi" + api_info_params = { + "api": "SYNO.API.Info", + "version": "1", + "method": "query", + "query": "SYNO.API.Auth" + } + + logger.debug("Querying API info to determine optimal auth version") + try: + api_info_response = self.session.get( + api_info_url, + params=api_info_params, + timeout=self.default_timeout, + verify=False + ) + + if api_info_response.status_code == 200: + api_info_data = api_info_response.json() + if api_info_data.get("success"): + auth_info = api_info_data.get("data", {}).get("SYNO.API.Auth", {}) + max_version = auth_info.get("maxVersion", 3) + min_version = auth_info.get("minVersion", 1) + auth_path = auth_info.get("path", "auth.cgi") + logger.debug(f"Auth API versions: min={min_version}, max={max_version}, path={auth_path}") + + # Используем максимально поддерживаемую версию, но не выше 6 + auth_version = min(max_version, 6) + else: + logger.warning("Failed to query API info, using default auth version 3") + auth_version = 3 + auth_path = "auth.cgi" + else: + logger.warning(f"API info request failed with status {api_info_response.status_code}, using default auth version 3") + auth_version = 3 + auth_path = "auth.cgi" + except Exception as e: + logger.warning(f"Error querying API info: {str(e)}, using default auth version 3") + auth_version = 3 + auth_path = "auth.cgi" + + # Основной запрос авторизации + url = f"{self.base_url}/{auth_path}" + params = { + "api": "SYNO.API.Auth", + "version": str(auth_version), + "method": "login", + "account": SYNOLOGY_USERNAME, + "passwd": SYNOLOGY_PASSWORD, + "session": "SynologyPowerControlBot", + "format": "cookie" + } + + # Для версии 6+ используем немного другой формат + if auth_version >= 6: + params["enable_syno_token"] = "yes" + + logger.debug(f"Sending auth request to {url} with API version {auth_version}") + start_time = time.time() + response = self.session.get( + url, + params=params, + timeout=self.default_timeout, + verify=False + ) + elapsed_time = time.time() - start_time + logger.debug(f"Auth request completed in {elapsed_time:.2f}s with status code {response.status_code}") + + # Подробная информация для отладки + logger.debug(f"Auth response: {response.text[:500]}") + + # Проверка статуса HTTP + if response.status_code != 200: + logger.error(f"HTTP error: {response.status_code}") + logger.debug(f"Response content: {response.text[:200]}") + return False + + try: + data = response.json() + except json.JSONDecodeError: + logger.error("Failed to decode JSON response") + logger.debug(f"Response content: {response.text[:200]}") + return False + + if data.get("success"): + self.sid = data.get("data", {}).get("sid") + self._last_auth_time = current_time + logger.info("Successfully logged in to Synology NAS") + logger.debug(f"Session ID obtained: {self.sid[:5] if self.sid else 'None'}...") # Показываем только начало SID для безопасности + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to log in to Synology NAS: Error code {error_code}") + + # Дополнительная диагностика + if error_code == 400: + logger.error("Authentication error: Invalid credentials") + elif error_code == 401: + logger.error("Authentication error: Account disabled") + elif error_code == 402: + logger.error("Authentication error: Permission denied") + elif error_code == 403: + logger.error("Authentication error: 2-factor authentication required") + elif error_code == 404: + logger.error("Authentication error: Failed to authenticate with 2-factor authentication") + + return False + + except requests.exceptions.Timeout: + logger.error(f"Connection timeout after {SYNOLOGY_TIMEOUT} seconds") + return False + except requests.exceptions.ConnectionError as e: + logger.error(f"Connection error: {str(e)}") + logger.debug(f"Connection details: {self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}") + return False + except requests.RequestException as e: + logger.error(f"Request error: {str(e)}") + return False + except Exception as e: + logger.error(f"Unexpected error during login: {str(e)}", exc_info=True) + return False + + def logout(self) -> bool: + """Выход из API Synology NAS""" + if not self.sid: + return True + + try: + url = f"{self.base_url}/auth.cgi" + params = { + "api": "SYNO.API.Auth", + "version": "1", + "method": "logout", + "session": "SynologyPowerControlBot", + "_sid": self.sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + self.sid = None + logger.info("Successfully logged out from Synology NAS") + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to log out from Synology NAS: Error code {error_code}") + return False + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + return False + + def _make_api_request(self, api_name: str, method: str, version: int = 1, + params: Optional[Dict[str, Any]] = None, retry_auth: bool = True) -> Optional[Dict[str, Any]]: + """Обобщенный метод для выполнения API запросов с обработкой ошибок""" + # Проверка наличия авторизации + if not self.sid and not self.login(): + logger.error(f"Not authenticated for API request: {api_name}.{method}") + return None + + # Проверка информации API для определения пути и поддерживаемой версии + api_info_url = f"{self.base_url}/entry.cgi" + api_info_params = { + "api": "SYNO.API.Info", + "version": "1", + "method": "query", + "query": api_name + } + + api_path = "entry.cgi" + try: + logger.debug(f"Querying API info for {api_name}") + api_info_response = self.session.get( + api_info_url, + params=api_info_params, + timeout=self.default_timeout, + verify=False + ) + + if api_info_response.status_code == 200: + api_info_data = api_info_response.json() + if api_info_data.get("success"): + api_info = api_info_data.get("data", {}).get(api_name, {}) + if api_info: + max_version = api_info.get("maxVersion", version) + min_version = api_info.get("minVersion", version) + api_path = api_info.get("path", "entry.cgi") + + # Проверка, поддерживается ли запрошенная версия + if version < min_version: + logger.warning(f"API version {version} for {api_name} is below minimum {min_version}, using {min_version}") + version = min_version + elif version > max_version: + logger.warning(f"API version {version} for {api_name} exceeds maximum {max_version}, using {max_version}") + version = max_version + + logger.debug(f"Using API path: {api_path}, version: {version}") + else: + logger.warning(f"API {api_name} not found in API info, using defaults") + except Exception as e: + logger.warning(f"Error querying API info for {api_name}: {str(e)}, using defaults") + + # Подготовка базовых параметров запроса + base_params = { + "api": api_name, + "version": str(version), + "method": method, + "sid": self.sid + } + + # Добавление дополнительных параметров, если они заданы + if params: + base_params.update(params) + + url = f"{self.base_url}/{api_path}" + logger.debug(f"API request: {api_name}.{method} v{version} to {url}") + logger.debug(f"Full request params: {base_params}") + + try: + start_time = time.time() + response = self.session.get( + url, + params=base_params, + timeout=self.default_timeout, + verify=False + ) + elapsed_time = time.time() - start_time + logger.debug(f"API request completed in {elapsed_time:.2f}s with status {response.status_code}") + + # Подробное логирование для отладки + logger.debug(f"Response: {response.text[:500]}") + + # Проверка статуса HTTP + if response.status_code != 200: + logger.error(f"HTTP error: {response.status_code} for {api_name}.{method}") + return None + + try: + data = response.json() + except json.JSONDecodeError: + logger.error(f"Failed to decode JSON response for {api_name}.{method}") + logger.debug(f"Response content: {response.text[:200]}") + return None + + if data.get("success"): + logger.info(f"API request successful for {api_name}.{method}") + return data.get("data", {}) + else: + error_code = data.get("error", {}).get("code", -1) + error_desc = self._get_error_description(error_code) + logger.error(f"API error for {api_name}.{method}: {error_code} - {error_desc}") + + # Ошибки доступа или прав часто встречаются, но они не критичные + # Например, ошибка 102 означает, что нет прав, но NAS доступен + if error_code in [102, 103, 104, 105]: + logger.warning(f"Permission-related error for {api_name}.{method}: {error_code}") + # Возвращаем пустой словарь вместо None, + # чтобы вызывающий код мог понять, что запрос выполнен + return {} + + # Если ошибка связана с авторизацией и нам разрешено повторить попытку + if error_code in [106, 107, 119] and retry_auth: + logger.info("Session may have expired, re-authenticating...") + self.sid = None # Сбрасываем SID + if self.login(): + logger.info("Re-authenticated, retrying API request...") + # Рекурсивный вызов, но без повторной авторизации во избежание зацикливания + return self._make_api_request(api_name, method, version, params, False) + + return None + + except requests.exceptions.Timeout: + logger.error(f"Request timeout for {api_name}.{method}") + return None + except requests.exceptions.ConnectionError as e: + logger.error(f"Connection error for {api_name}.{method}: {str(e)}") + return None + except Exception as e: + logger.error(f"Unexpected error during {api_name}.{method}: {str(e)}") + return None + + def get_system_status(self) -> Dict[str, Any]: + """Получение статуса системы""" + # Проверяем доступность системы + if not self.is_online(): + logger.info("Device is offline, skipping API request") + return {"status": "offline"} + + # Проверяем, есть ли кэшированный результат + cache_key = "system_status" + current_time = time.time() + if cache_key in self._cache and current_time - self._cache_ttl.get(cache_key, 0) < 60: + logger.debug("Using cached system status") + return self._cache[cache_key] + + # Пробуем разные API для получения информации о системе + apis_to_try = [ + {"name": "SYNO.DSM.Info", "method": "getinfo", "version": 2}, + {"name": "SYNO.Core.System", "method": "info", "version": 1}, + {"name": "SYNO.Core.System.Status", "method": "get", "version": 1}, + {"name": "SYNO.Core.System.Info", "method": "get", "version": 1}, + ] + + for api in apis_to_try: + logger.info(f"Trying to get system info using {api['name']}.{api['method']} v{api['version']}") + result = self._make_api_request(api["name"], api["method"], version=api["version"]) + + if result: + logger.info(f"Successfully retrieved system info using {api['name']}") + + # Формируем расширенный ответ с дополнительной информацией + system_info = { + "status": "online", + "hostname": result.get("hostname", "unknown"), + "model": result.get("model", "unknown"), + "version": result.get("version", "unknown"), + "uptime": result.get("uptime", 0), + "time": current_time, + "is_online": True, + "api_used": api["name"] + } + + # Сохраняем в кэше + self._cache[cache_key] = system_info + self._cache_ttl[cache_key] = current_time + + return system_info + + # Если все запросы не удались, но система онлайн, возвращаем базовую информацию + logger.warning("Failed to retrieve system info with all API methods") + return { + "status": "error", + "error": "Failed to fetch system information", + "is_online": True, + "time": current_time + } + + def shutdown_system(self) -> bool: + """Выключение системы""" + # Проверяем, включено ли устройство перед попыткой его выключить + if not self.is_online(force_check=True): + logger.info("Synology NAS is already offline, no need to shut down") + return True + + logger.info("Attempting to shutdown Synology NAS...") + + # Проверка всех доступных методов API для выключения + # Проверяем наличие API перед использованием + apis_to_try = [ + {"name": "SYNO.Core.System.Power", "method": "shutdown", "version": 1}, + {"name": "SYNO.DSM.Power", "method": "shutdown", "version": 1}, + {"name": "SYNO.Core.System", "method": "shutdown", "version": 3}, + {"name": "SYNO.System.Power", "method": "shutdown", "version": 1}, + {"name": "SYNO.DSM.System", "method": "shutdown", "version": 1} + ] + + # Получаем список доступных API + try: + api_info_url = f"{self.base_url}/entry.cgi" + api_info_params = { + "api": "SYNO.API.Info", + "version": "1", + "method": "query", + "query": "SYNO.Core.System.Power,SYNO.DSM.Power,SYNO.Core.System,SYNO.System.Power,SYNO.DSM.System" + } + + logger.debug("Checking available shutdown APIs") + api_info_response = self.session.get( + api_info_url, + params=api_info_params, + timeout=self.default_timeout, + verify=False + ) + + if api_info_response.status_code == 200: + api_info_data = api_info_response.json() + if api_info_data.get("success"): + available_apis = api_info_data.get("data", {}) + logger.debug(f"Available APIs: {list(available_apis.keys())}") + + # Фильтруем только доступные API + filtered_apis = [] + for api in apis_to_try: + if api["name"] in available_apis: + api_info = available_apis[api["name"]] + # Проверка версии API + if api_info.get("minVersion", 1) <= api["version"] <= api_info.get("maxVersion", 1): + filtered_apis.append(api) + logger.debug(f"Adding {api['name']} to available shutdown APIs") + else: + logger.debug(f"API {api['name']} version {api['version']} not supported. Supported: {api_info.get('minVersion')}-{api_info.get('maxVersion')}") + + if filtered_apis: + apis_to_try = filtered_apis + else: + logger.warning("No compatible APIs found, trying all methods as fallback") + else: + logger.warning("Failed to query API info, using default methods") + else: + logger.warning(f"API info request failed with status {api_info_response.status_code}, using default methods") + except Exception as e: + logger.warning(f"Error querying API info: {str(e)}, using default methods") + + # Пробуем все доступные методы по порядку + for api in apis_to_try: + logger.info(f"Trying shutdown with {api['name']}.{api['method']} v{api['version']}") + result = self._make_api_request(api["name"], api["method"], version=api["version"]) + + if result is not None: + logger.info(f"Successfully initiated system shutdown using {api['name']}") + + # Даем системе время начать процесс выключения + logger.info("Waiting for shutdown to initialize...") + sleep(5) + + # Проверяем статус + if not self.is_online(force_check=True): + logger.info("System is now offline. Shutdown confirmed successful.") + return True + else: + logger.info("System still appears to be online, but shutdown may be in progress.") + return True + else: + logger.warning(f"Failed to shutdown using {api['name']}.{api['method']} v{api['version']}") + + # Если ни один метод не сработал, но система стала недоступна + if not self.is_online(force_check=True): + logger.info("System appears to be shutting down despite API errors") + return True + + logger.error("Failed to shutdown system after trying multiple APIs") + return False + + def reboot_system(self) -> bool: + """Перезагрузка системы""" + # Проверяем, включена ли система + if not self.is_online(force_check=True): + logger.error("Cannot reboot: System is offline") + return False + + logger.info("Attempting to reboot Synology NAS...") + + # Проверка всех доступных методов API для перезагрузки + apis_to_try = [ + {"name": "SYNO.Core.System.Power", "method": "restart", "version": 1}, + {"name": "SYNO.DSM.Power", "method": "reboot", "version": 1}, + {"name": "SYNO.Core.System", "method": "reboot", "version": 3}, + {"name": "SYNO.System.Power", "method": "reboot", "version": 1}, + {"name": "SYNO.DSM.System", "method": "reboot", "version": 1} + ] + + # Получаем список доступных API + try: + api_info_url = f"{self.base_url}/entry.cgi" + api_info_params = { + "api": "SYNO.API.Info", + "version": "1", + "method": "query", + "query": "SYNO.Core.System.Power,SYNO.DSM.Power,SYNO.Core.System,SYNO.System.Power,SYNO.DSM.System" + } + + logger.debug("Checking available reboot APIs") + api_info_response = self.session.get( + api_info_url, + params=api_info_params, + timeout=self.default_timeout, + verify=False + ) + + if api_info_response.status_code == 200: + api_info_data = api_info_response.json() + if api_info_data.get("success"): + available_apis = api_info_data.get("data", {}) + logger.debug(f"Available APIs: {list(available_apis.keys())}") + + # Фильтруем только доступные API + filtered_apis = [] + for api in apis_to_try: + if api["name"] in available_apis: + api_info = available_apis[api["name"]] + # Проверка версии API + if api_info.get("minVersion", 1) <= api["version"] <= api_info.get("maxVersion", 1): + filtered_apis.append(api) + logger.debug(f"Adding {api['name']} to available reboot APIs") + else: + logger.debug(f"API {api['name']} version {api['version']} not supported. Supported: {api_info.get('minVersion')}-{api_info.get('maxVersion')}") + + if filtered_apis: + apis_to_try = filtered_apis + else: + logger.warning("No compatible APIs found, trying all methods as fallback") + else: + logger.warning("Failed to query API info, using default methods") + else: + logger.warning(f"API info request failed with status {api_info_response.status_code}, using default methods") + except Exception as e: + logger.warning(f"Error querying API info: {str(e)}, using default methods") + + # Пробуем все доступные методы по порядку + for api in apis_to_try: + logger.info(f"Trying reboot with {api['name']}.{api['method']} v{api['version']}") + result = self._make_api_request(api["name"], api["method"], version=api["version"]) + + if result is not None: + logger.info(f"Successfully initiated system reboot using {api['name']}") + + # Даем системе время начать процесс перезагрузки + logger.info("Waiting for reboot to initialize...") + sleep(5) + + # Ждем, пока система станет недоступна (признак перезагрузки) + reboot_started = False + for i in range(12): # Проверяем в течение 60 секунд (12 * 5сек) + if not self.is_online(force_check=True): + logger.info(f"System went offline after {i*5} seconds, reboot in progress") + reboot_started = True + break + logger.debug(f"System still online, waiting... ({i+1}/12)") + sleep(5) + + if reboot_started: + # Ждем, пока система снова станет доступна + logger.info("Waiting for system to come back online...") + return self.wait_for_boot(max_attempts=30, delay=10) + else: + logger.warning("System did not go offline after reboot command, but command was accepted") + # Даже если система не ушла оффлайн, команда могла быть принята + return True + else: + logger.warning(f"Failed to reboot using {api['name']}.{api['method']} v{api['version']}") + + logger.error("Failed to reboot system after trying multiple APIs") + return False + + def _get_error_description(self, error_code: int) -> str: + """Получение описания ошибки по коду""" + error_descriptions = { + 100: "Unknown error", + 101: "Invalid parameter", + 102: "API does not exist", + 103: "Method does not exist", + 104: "Version does not support", + 105: "Permission denied", + 106: "Session timeout", + 107: "Session interrupted by duplicate login", + 400: "Invalid credentials", + 401: "Account disabled", + 402: "Permission denied", + 403: "2FA required", + 404: "Failed to authenticate with 2FA" + } + return error_descriptions.get(error_code, "Unknown error code") + + def _check_tcp_connection(self) -> bool: + """Проверка базового TCP-соединения с Synology NAS""" + try: + socket_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + socket_obj.settimeout(SYNOLOGY_TIMEOUT) + result = socket_obj.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT)) + socket_obj.close() + + return result == 0 + except socket.error as e: + logger.error(f"Socket error during connection check: {str(e)}") + return False + except Exception as e: + logger.error(f"Unexpected error during connection check: {str(e)}") + return False + + def is_online(self, force_check=False) -> bool: + """Проверка онлайн-статуса Synology NAS""" + # Используем кэшированное значение, если доступно и не устарело + current_time = time.time() + if not force_check and (current_time - self._last_online_check) < self._online_check_interval: + logger.debug(f"Using cached online status: {self._last_online_status}") + return self._last_online_status + + logger.info("Checking if NAS is online...") + + # Проверяем TCP-соединение + online_status = self._check_tcp_connection() + logger.info(f"Detected Synology NAS online status: {online_status}") + + # Если TCP-соединение успешно и у нас есть действующий SID, + # попробуем более детальную проверку через API + if online_status and self.sid: + logger.info("Trying to fetch more detailed online status through API...") + + # Пробуем разные API для проверки онлайн-статуса + api_checks = [ + {"api": "SYNO.DSM.Info", "version": "2", "method": "getinfo"}, + {"api": "SYNO.Core.System", "version": "1", "method": "info"}, + {"api": "SYNO.Core.System.Status", "version": "1", "method": "get"} + ] + + api_success = False + for api_check in api_checks: + try: + url = f"{self.base_url}/entry.cgi" + params = { + "api": api_check["api"], + "version": api_check["version"], + "method": api_check["method"], + "sid": self.sid + } + + logger.debug(f"Trying online status check with {api_check['api']}") + response = self.session.get( + url, + params=params, + timeout=self.default_timeout, + verify=False + ) + + if response.status_code == 200: + data = response.json() + if data.get("success"): + logger.info(f"API request successful for {api_check['api']}") + logger.info("Synology NAS is online with API access") + api_success = True + break + else: + error_code = data.get("error", {}).get("code", -1) + logger.warning(f"API response indicates an error: {error_code}, but NAS is reachable") + else: + logger.warning(f"API returned status code {response.status_code}") + except Exception as e: + logger.warning(f"API check failed for {api_check['api']}, but TCP connection succeeded: {str(e)}") + + if not api_success: + logger.warning("All API checks failed, but TCP connection is successful") + + # Обновляем кэшированное значение + self._last_online_check = current_time + self._last_online_status = online_status + + return online_status + + def wake_on_lan(self) -> bool: + """Отправка Wake-on-LAN пакета для включения Synology NAS""" + if not SYNOLOGY_MAC: + logger.error("MAC address not configured") + return False + + try: + # Преобразование MAC-адреса в байты + mac_address = SYNOLOGY_MAC.replace(':', '').replace('-', '') + if len(mac_address) != 12: + logger.error(f"Invalid MAC address format: {SYNOLOGY_MAC}") + return False + + try: + mac_bytes = bytes.fromhex(mac_address) + except ValueError as e: + logger.error(f"Failed to parse MAC address: {str(e)}") + logger.error(f"MAC address must contain only hex characters: {SYNOLOGY_MAC}") + return False + + # Создание Magic Packet + magic_packet = b'\xff' * 6 + mac_bytes * 16 + + # Отправка пакета на конкретный адрес + try: + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) + sock.sendto(magic_packet, (SYNOLOGY_HOST, WOL_PORT)) + sock.close() + logger.info(f"Wake-on-LAN packet sent to {SYNOLOGY_MAC} at {SYNOLOGY_HOST}:{WOL_PORT}") + except Exception as e: + logger.error(f"Error sending directed WoL packet: {str(e)}") + return False + + # Для надежности отправляем также широковещательный пакет + try: + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) + + # Используем стандартный широковещательный адрес + broadcast_addr = "255.255.255.255" + sock.sendto(magic_packet, (broadcast_addr, WOL_PORT)) + sock.close() + logger.info(f"Wake-on-LAN broadcast packet sent to {broadcast_addr}:{WOL_PORT}") + except Exception as e: + logger.warning(f"Error sending broadcast WoL packet (non-critical): {str(e)}") + # Не считаем ошибкой, т.к. основной пакет уже отправлен + + return True + + except Exception as e: + logger.error(f"Unexpected error in wake_on_lan: {str(e)}") + return False + + def wait_for_boot(self, max_attempts: int = 30, delay: int = 10) -> bool: + """Ожидание загрузки Synology NAS""" + logger.info(f"Waiting for Synology NAS to boot (timeout: {max_attempts * delay}s)...") + + for attempt in range(max_attempts): + # Принудительно проверяем статус без использования кэша + if self.is_online(force_check=True): + logger.info(f"Synology NAS is online after {attempt + 1} attempts ({(attempt + 1) * delay}s)") + + # Проверяем, что не только сеть доступна, но и API загрузился + api_ready = False + logger.info("Waiting for API services to initialize...") + + for api_check in range(5): # Даем еще до 50 секунд для загрузки API + if self.sid or self.login(): + api_ready = True + logger.info(f"API services are ready after {api_check + 1} attempts") + break + logger.debug(f"API not ready yet, waiting... ({api_check + 1}/5)") + sleep(10) + + if not api_ready: + logger.warning("System is online but API services may not be fully initialized") + + # Дадим дополнительное время для полной загрузки всех сервисов + sleep(delay) + return True + + sleep(delay) + logger.info(f"Waiting for boot... Attempt {attempt + 1}/{max_attempts}") + + logger.error(f"Synology NAS didn't come online after {max_attempts * delay} seconds") + return False + + def power_on(self) -> bool: + """Включение Synology NAS""" + # Принудительная проверка статуса + if self.is_online(force_check=True): + logger.info("Synology NAS is already online") + return True + + logger.info("Powering on Synology NAS via Wake-on-LAN...") + + # Проверяем, настроен ли MAC-адрес + if not SYNOLOGY_MAC: + logger.error("Cannot power on: MAC address not configured in settings") + return False + + # Пробуем отправить несколько WoL пакетов для надежности + success = False + for attempt in range(3): + logger.debug(f"Sending WoL packet, attempt {attempt + 1}/3") + if self.wake_on_lan(): + success = True + break + sleep(1) + + if not success: + logger.error("Failed to send Wake-on-LAN packets") + return False + + # Ожидание загрузки + logger.info("WoL packets sent successfully, waiting for system to boot...") + boot_result = self.wait_for_boot(max_attempts=30, delay=10) + + if boot_result: + # Проверяем доступность API после загрузки + system_status = self.get_system_status() + if system_status.get("status") == "online": + logger.info("System booted successfully with API access") + return True + else: + logger.warning("System appears to be online but API may not be fully ready") + return True + else: + logger.error("System did not come online after WoL") + return False + + def power_off(self) -> bool: + """Выключение Synology NAS""" + if not self.is_online(force_check=True): + logger.info("Synology NAS is already offline") + return True + + logger.info("Powering off Synology NAS...") + result = self.shutdown_system() + + if result: + # Дополнительная проверка, что система действительно выключилась + logger.info("Verifying system is offline...") + for attempt in range(12): # Проверяем в течение 2 минут (12 * 10 сек) + if not self.is_online(force_check=True): + logger.info(f"System confirmed offline after {attempt * 10} seconds") + return True + logger.debug(f"System still shutting down, checking again... ({attempt + 1}/12)") + sleep(10) + + logger.warning("System still appears to be online after shutdown command") + return False + else: + logger.error("Failed to initiate shutdown") + return False + + # Заглушки для расширенных методов + def get_shared_folders(self) -> List[Dict[str, Any]]: + """Получение списка общих папок (заглушка)""" + logger.warning("Function get_shared_folders() is not implemented yet") + return [] + + def get_system_load(self) -> Dict[str, Any]: + """Получение информации о загрузке системы (заглушка)""" + logger.warning("Function get_system_load() is not implemented yet") + return {} + + def get_storage_status(self) -> Dict[str, Any]: + """Получение подробной информации о хранилище (заглушка)""" + logger.warning("Function get_storage_status() is not implemented yet") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for storage status request") + return {"error": "authentication_failed"} + + try: + # Здесь будет реализация после добавления соответствующего API + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.Storage.CGI.Storage", + "version": "1", + "method": "load_info", + "sid": self.sid + } + + # В текущей версии просто возвращаем заглушку + return { + "volumes": [], + "disks": [], + "total_size": 0, + "total_used": 0, + "error": "not_implemented" + } + except Exception as e: + logger.error(f"Error in get_storage_status: {str(e)}") + return {"error": str(e)} + + def get_security_status(self) -> Dict[str, Any]: + """Получение информации о состоянии безопасности (заглушка)""" + logger.warning("Function get_security_status() is not implemented yet") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for security status request") + return {"success": False, "error": "authentication_failed"} + + try: + # Здесь будет реализация после добавления соответствующего API + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.Core.SecurityScan.Status", + "version": "1", + "method": "get", + "sid": self.sid + } + + # В текущей версии просто возвращаем заглушку + return { + "success": False, + "status": "not_implemented", + "last_check": None, + "is_secure": False, + "error": "not_implemented" + } + except Exception as e: + logger.error(f"Error in get_security_status: {str(e)}") + return {"success": False, "error": str(e)} diff --git a/.history/src/api/synology_20250830081744.py b/.history/src/api/synology_20250830081744.py new file mode 100644 index 0000000..d4ce873 --- /dev/null +++ b/.history/src/api/synology_20250830081744.py @@ -0,0 +1,976 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Модуль для взаимодействия с API Synology NAS +""" + +import requests +from requests.adapters import HTTPAdapter +import json +import logging +import time +import urllib3 +from urllib3.util import Retry +from typing import Dict, Any, Optional, List +import socket +import struct +from time import sleep + +from src.config.config import ( + SYNOLOGY_HOST, + SYNOLOGY_PORT, + SYNOLOGY_USERNAME, + SYNOLOGY_PASSWORD, + SYNOLOGY_SECURE, + SYNOLOGY_TIMEOUT, + SYNOLOGY_MAC, + WOL_PORT +) + +# Отключение предупреждений о небезопасных SSL-соединениях +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + +logger = logging.getLogger(__name__) + +class SynologyAPI: + """Класс для взаимодействия с API Synology NAS""" + + def __init__(self): + """Инициализация класса SynologyAPI""" + logger.info("Creating API with auto-retry and connection pool") + logger.debug(f"Connection details: Host={SYNOLOGY_HOST}, Port={SYNOLOGY_PORT}, Secure={SYNOLOGY_SECURE}") + + self.protocol = "https" if SYNOLOGY_SECURE else "http" + self.base_url = f"{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi" + self.sid = None + self.session = requests.Session() + + # Настройка SSL + if self.protocol == "https": + logger.debug("SSL enabled, disabling certificate verification for internal network") + self.session.verify = False # Отключаем проверку SSL для внутренней сети + + # Добавляем повторные попытки для HTTP-запросов + retry_strategy = Retry( + total=5, # Увеличиваем количество попыток + status_forcelist=[429, 500, 502, 503, 504, 404], + allowed_methods=["GET", "POST"], + backoff_factor=1.5, # Увеличиваем задержку между попытками + respect_retry_after_header=True + ) + adapter = HTTPAdapter( + max_retries=retry_strategy, + pool_connections=3, + pool_maxsize=10 + ) + self.session.mount("http://", adapter) + self.session.mount("https://", adapter) + + # Таймауты будут указаны в запросах + self.default_timeout = (SYNOLOGY_TIMEOUT, SYNOLOGY_TIMEOUT*2) + logger.debug(f"Setting default request timeout: {self.default_timeout}") + + # Кэш для хранения результатов запросов + self._cache = {} + self._cache_ttl = {} + self._last_online_check = 0 + self._last_online_status = False + self._online_check_interval = 30 # Интервал для кэширования проверок онлайн-статуса + + # Время последней успешной аутентификации и срок действия сессии + self._last_auth_time = 0 + self._auth_expiry = 3600 # По умолчанию 1 час + + # Информация о доступных API + self._available_apis = {} + self._api_info_ttl = 0 + + def login(self) -> bool: + """Авторизация в API Synology NAS""" + # Проверяем, действительна ли текущая сессия + current_time = time.time() + if self.sid and (current_time - self._last_auth_time) < self._auth_expiry: + logger.debug("Using existing session, still valid") + return True + + logger.info("Attempting to authenticate with Synology NAS...") + logger.debug(f"Base URL: {self.base_url}") + + # Проверяем доступность NAS перед запросом с помощью прямой TCP-проверки + # Избегаем вызова is_online(), чтобы не создавать рекурсию + online_status = self._check_tcp_connection() + if not online_status: + logger.error("Cannot login: Synology NAS is not reachable") + return False + + try: + # Проверка информации API для определения доступных версий API + api_info_url = f"{self.base_url}/entry.cgi" + api_info_params = { + "api": "SYNO.API.Info", + "version": "1", + "method": "query", + "query": "SYNO.API.Auth" + } + + logger.debug("Querying API info to determine optimal auth version") + try: + api_info_response = self.session.get( + api_info_url, + params=api_info_params, + timeout=self.default_timeout, + verify=False + ) + + if api_info_response.status_code == 200: + api_info_data = api_info_response.json() + if api_info_data.get("success"): + auth_info = api_info_data.get("data", {}).get("SYNO.API.Auth", {}) + max_version = auth_info.get("maxVersion", 3) + min_version = auth_info.get("minVersion", 1) + auth_path = auth_info.get("path", "auth.cgi") + logger.debug(f"Auth API versions: min={min_version}, max={max_version}, path={auth_path}") + + # Используем максимально поддерживаемую версию, но не выше 6 + auth_version = min(max_version, 6) + else: + logger.warning("Failed to query API info, using default auth version 3") + auth_version = 3 + auth_path = "auth.cgi" + else: + logger.warning(f"API info request failed with status {api_info_response.status_code}, using default auth version 3") + auth_version = 3 + auth_path = "auth.cgi" + except Exception as e: + logger.warning(f"Error querying API info: {str(e)}, using default auth version 3") + auth_version = 3 + auth_path = "auth.cgi" + + # Основной запрос авторизации + url = f"{self.base_url}/{auth_path}" + params = { + "api": "SYNO.API.Auth", + "version": str(auth_version), + "method": "login", + "account": SYNOLOGY_USERNAME, + "passwd": SYNOLOGY_PASSWORD, + "session": "SynologyPowerControlBot", + "format": "cookie" + } + + # Для версии 6+ используем немного другой формат + if auth_version >= 6: + params["enable_syno_token"] = "yes" + + logger.debug(f"Sending auth request to {url} with API version {auth_version}") + start_time = time.time() + response = self.session.get( + url, + params=params, + timeout=self.default_timeout, + verify=False + ) + elapsed_time = time.time() - start_time + logger.debug(f"Auth request completed in {elapsed_time:.2f}s with status code {response.status_code}") + + # Подробная информация для отладки + logger.debug(f"Auth response: {response.text[:500]}") + + # Проверка статуса HTTP + if response.status_code != 200: + logger.error(f"HTTP error: {response.status_code}") + logger.debug(f"Response content: {response.text[:200]}") + return False + + try: + data = response.json() + except json.JSONDecodeError: + logger.error("Failed to decode JSON response") + logger.debug(f"Response content: {response.text[:200]}") + return False + + if data.get("success"): + self.sid = data.get("data", {}).get("sid") + self._last_auth_time = current_time + logger.info("Successfully logged in to Synology NAS") + logger.debug(f"Session ID obtained: {self.sid[:5] if self.sid else 'None'}...") # Показываем только начало SID для безопасности + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to log in to Synology NAS: Error code {error_code}") + + # Дополнительная диагностика + if error_code == 400: + logger.error("Authentication error: Invalid credentials") + elif error_code == 401: + logger.error("Authentication error: Account disabled") + elif error_code == 402: + logger.error("Authentication error: Permission denied") + elif error_code == 403: + logger.error("Authentication error: 2-factor authentication required") + elif error_code == 404: + logger.error("Authentication error: Failed to authenticate with 2-factor authentication") + + return False + + except requests.exceptions.Timeout: + logger.error(f"Connection timeout after {SYNOLOGY_TIMEOUT} seconds") + return False + except requests.exceptions.ConnectionError as e: + logger.error(f"Connection error: {str(e)}") + logger.debug(f"Connection details: {self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}") + return False + except requests.RequestException as e: + logger.error(f"Request error: {str(e)}") + return False + except Exception as e: + logger.error(f"Unexpected error during login: {str(e)}", exc_info=True) + return False + + def logout(self) -> bool: + """Выход из API Synology NAS""" + if not self.sid: + return True + + try: + url = f"{self.base_url}/auth.cgi" + params = { + "api": "SYNO.API.Auth", + "version": "1", + "method": "logout", + "session": "SynologyPowerControlBot", + "_sid": self.sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + self.sid = None + logger.info("Successfully logged out from Synology NAS") + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to log out from Synology NAS: Error code {error_code}") + return False + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + return False + + def _make_api_request(self, api_name: str, method: str, version: int = 1, + params: Optional[Dict[str, Any]] = None, retry_auth: bool = True) -> Optional[Dict[str, Any]]: + """Обобщенный метод для выполнения API запросов с обработкой ошибок""" + # Проверка наличия авторизации + if not self.sid and not self.login(): + logger.error(f"Not authenticated for API request: {api_name}.{method}") + return None + + # Проверка информации API для определения пути и поддерживаемой версии + api_info_url = f"{self.base_url}/entry.cgi" + api_info_params = { + "api": "SYNO.API.Info", + "version": "1", + "method": "query", + "query": api_name + } + + api_path = "entry.cgi" + try: + logger.debug(f"Querying API info for {api_name}") + api_info_response = self.session.get( + api_info_url, + params=api_info_params, + timeout=self.default_timeout, + verify=False + ) + + if api_info_response.status_code == 200: + api_info_data = api_info_response.json() + if api_info_data.get("success"): + api_info = api_info_data.get("data", {}).get(api_name, {}) + if api_info: + max_version = api_info.get("maxVersion", version) + min_version = api_info.get("minVersion", version) + api_path = api_info.get("path", "entry.cgi") + + # Проверка, поддерживается ли запрошенная версия + if version < min_version: + logger.warning(f"API version {version} for {api_name} is below minimum {min_version}, using {min_version}") + version = min_version + elif version > max_version: + logger.warning(f"API version {version} for {api_name} exceeds maximum {max_version}, using {max_version}") + version = max_version + + logger.debug(f"Using API path: {api_path}, version: {version}") + else: + logger.warning(f"API {api_name} not found in API info, using defaults") + except Exception as e: + logger.warning(f"Error querying API info for {api_name}: {str(e)}, using defaults") + + # Подготовка базовых параметров запроса + base_params = { + "api": api_name, + "version": str(version), + "method": method, + "sid": self.sid + } + + # Добавление дополнительных параметров, если они заданы + if params: + base_params.update(params) + + url = f"{self.base_url}/{api_path}" + logger.debug(f"API request: {api_name}.{method} v{version} to {url}") + logger.debug(f"Full request params: {base_params}") + + try: + start_time = time.time() + response = self.session.get( + url, + params=base_params, + timeout=self.default_timeout, + verify=False + ) + elapsed_time = time.time() - start_time + logger.debug(f"API request completed in {elapsed_time:.2f}s with status {response.status_code}") + + # Подробное логирование для отладки + logger.debug(f"Response: {response.text[:500]}") + + # Проверка статуса HTTP + if response.status_code != 200: + logger.error(f"HTTP error: {response.status_code} for {api_name}.{method}") + return None + + try: + data = response.json() + except json.JSONDecodeError: + logger.error(f"Failed to decode JSON response for {api_name}.{method}") + logger.debug(f"Response content: {response.text[:200]}") + return None + + if data.get("success"): + logger.info(f"API request successful for {api_name}.{method}") + return data.get("data", {}) + else: + error_code = data.get("error", {}).get("code", -1) + error_desc = self._get_error_description(error_code) + logger.error(f"API error for {api_name}.{method}: {error_code} - {error_desc}") + + # Ошибки доступа или прав часто встречаются, но они не критичные + # Например, ошибка 102 означает, что нет прав, но NAS доступен + if error_code in [102, 103, 104, 105]: + logger.warning(f"Permission-related error for {api_name}.{method}: {error_code}") + # Возвращаем пустой словарь вместо None, + # чтобы вызывающий код мог понять, что запрос выполнен + return {} + + # Если ошибка связана с авторизацией и нам разрешено повторить попытку + if error_code in [106, 107, 119] and retry_auth: + logger.info("Session may have expired, re-authenticating...") + self.sid = None # Сбрасываем SID + if self.login(): + logger.info("Re-authenticated, retrying API request...") + # Рекурсивный вызов, но без повторной авторизации во избежание зацикливания + return self._make_api_request(api_name, method, version, params, False) + + return None + + except requests.exceptions.Timeout: + logger.error(f"Request timeout for {api_name}.{method}") + return None + except requests.exceptions.ConnectionError as e: + logger.error(f"Connection error for {api_name}.{method}: {str(e)}") + return None + except Exception as e: + logger.error(f"Unexpected error during {api_name}.{method}: {str(e)}") + return None + + def get_system_status(self) -> Dict[str, Any]: + """Получение статуса системы""" + # Проверяем доступность системы + if not self.is_online(): + logger.info("Device is offline, skipping API request") + return {"status": "offline"} + + # Проверяем, есть ли кэшированный результат + cache_key = "system_status" + current_time = time.time() + if cache_key in self._cache and current_time - self._cache_ttl.get(cache_key, 0) < 60: + logger.debug("Using cached system status") + return self._cache[cache_key] + + # Пробуем разные API для получения информации о системе + apis_to_try = [ + {"name": "SYNO.DSM.Info", "method": "getinfo", "version": 2}, + {"name": "SYNO.Core.System", "method": "info", "version": 1}, + {"name": "SYNO.Core.System.Status", "method": "get", "version": 1}, + {"name": "SYNO.Core.System.Info", "method": "get", "version": 1}, + ] + + for api in apis_to_try: + logger.info(f"Trying to get system info using {api['name']}.{api['method']} v{api['version']}") + result = self._make_api_request(api["name"], api["method"], version=api["version"]) + + if result: + logger.info(f"Successfully retrieved system info using {api['name']}") + + # Формируем расширенный ответ с дополнительной информацией + system_info = { + "status": "online", + "hostname": result.get("hostname", "unknown"), + "model": result.get("model", "unknown"), + "version": result.get("version", "unknown"), + "uptime": result.get("uptime", 0), + "time": current_time, + "is_online": True, + "api_used": api["name"] + } + + # Сохраняем в кэше + self._cache[cache_key] = system_info + self._cache_ttl[cache_key] = current_time + + return system_info + + # Если все запросы не удались, но система онлайн, возвращаем базовую информацию + logger.warning("Failed to retrieve system info with all API methods") + return { + "status": "error", + "error": "Failed to fetch system information", + "is_online": True, + "time": current_time + } + + def shutdown_system(self) -> bool: + """Выключение системы""" + # Проверяем, включено ли устройство перед попыткой его выключить + if not self.is_online(force_check=True): + logger.info("Synology NAS is already offline, no need to shut down") + return True + + logger.info("Attempting to shutdown Synology NAS...") + + # Проверка всех доступных методов API для выключения + # Проверяем наличие API перед использованием + apis_to_try = [ + {"name": "SYNO.Core.System.Power", "method": "shutdown", "version": 1}, + {"name": "SYNO.DSM.Power", "method": "shutdown", "version": 1}, + {"name": "SYNO.Core.System", "method": "shutdown", "version": 3}, + {"name": "SYNO.System.Power", "method": "shutdown", "version": 1}, + {"name": "SYNO.DSM.System", "method": "shutdown", "version": 1} + ] + + # Получаем список доступных API + try: + api_info_url = f"{self.base_url}/entry.cgi" + api_info_params = { + "api": "SYNO.API.Info", + "version": "1", + "method": "query", + "query": "SYNO.Core.System.Power,SYNO.DSM.Power,SYNO.Core.System,SYNO.System.Power,SYNO.DSM.System" + } + + logger.debug("Checking available shutdown APIs") + api_info_response = self.session.get( + api_info_url, + params=api_info_params, + timeout=self.default_timeout, + verify=False + ) + + if api_info_response.status_code == 200: + api_info_data = api_info_response.json() + if api_info_data.get("success"): + available_apis = api_info_data.get("data", {}) + logger.debug(f"Available APIs: {list(available_apis.keys())}") + + # Фильтруем только доступные API + filtered_apis = [] + for api in apis_to_try: + if api["name"] in available_apis: + api_info = available_apis[api["name"]] + # Проверка версии API + if api_info.get("minVersion", 1) <= api["version"] <= api_info.get("maxVersion", 1): + filtered_apis.append(api) + logger.debug(f"Adding {api['name']} to available shutdown APIs") + else: + logger.debug(f"API {api['name']} version {api['version']} not supported. Supported: {api_info.get('minVersion')}-{api_info.get('maxVersion')}") + + if filtered_apis: + apis_to_try = filtered_apis + else: + logger.warning("No compatible APIs found, trying all methods as fallback") + else: + logger.warning("Failed to query API info, using default methods") + else: + logger.warning(f"API info request failed with status {api_info_response.status_code}, using default methods") + except Exception as e: + logger.warning(f"Error querying API info: {str(e)}, using default methods") + + # Пробуем все доступные методы по порядку + for api in apis_to_try: + logger.info(f"Trying shutdown with {api['name']}.{api['method']} v{api['version']}") + result = self._make_api_request(api["name"], api["method"], version=api["version"]) + + if result is not None: + logger.info(f"Successfully initiated system shutdown using {api['name']}") + + # Даем системе время начать процесс выключения + logger.info("Waiting for shutdown to initialize...") + sleep(5) + + # Проверяем статус + if not self.is_online(force_check=True): + logger.info("System is now offline. Shutdown confirmed successful.") + return True + else: + logger.info("System still appears to be online, but shutdown may be in progress.") + return True + else: + logger.warning(f"Failed to shutdown using {api['name']}.{api['method']} v{api['version']}") + + # Если ни один метод не сработал, но система стала недоступна + if not self.is_online(force_check=True): + logger.info("System appears to be shutting down despite API errors") + return True + + logger.error("Failed to shutdown system after trying multiple APIs") + return False + + def reboot_system(self) -> bool: + """Перезагрузка системы""" + # Проверяем, включена ли система + if not self.is_online(force_check=True): + logger.error("Cannot reboot: System is offline") + return False + + logger.info("Attempting to reboot Synology NAS...") + + # Проверка всех доступных методов API для перезагрузки + apis_to_try = [ + {"name": "SYNO.Core.System.Power", "method": "restart", "version": 1}, + {"name": "SYNO.DSM.Power", "method": "reboot", "version": 1}, + {"name": "SYNO.Core.System", "method": "reboot", "version": 3}, + {"name": "SYNO.System.Power", "method": "reboot", "version": 1}, + {"name": "SYNO.DSM.System", "method": "reboot", "version": 1} + ] + + # Получаем список доступных API + try: + api_info_url = f"{self.base_url}/entry.cgi" + api_info_params = { + "api": "SYNO.API.Info", + "version": "1", + "method": "query", + "query": "SYNO.Core.System.Power,SYNO.DSM.Power,SYNO.Core.System,SYNO.System.Power,SYNO.DSM.System" + } + + logger.debug("Checking available reboot APIs") + api_info_response = self.session.get( + api_info_url, + params=api_info_params, + timeout=self.default_timeout, + verify=False + ) + + if api_info_response.status_code == 200: + api_info_data = api_info_response.json() + if api_info_data.get("success"): + available_apis = api_info_data.get("data", {}) + logger.debug(f"Available APIs: {list(available_apis.keys())}") + + # Фильтруем только доступные API + filtered_apis = [] + for api in apis_to_try: + if api["name"] in available_apis: + api_info = available_apis[api["name"]] + # Проверка версии API + if api_info.get("minVersion", 1) <= api["version"] <= api_info.get("maxVersion", 1): + filtered_apis.append(api) + logger.debug(f"Adding {api['name']} to available reboot APIs") + else: + logger.debug(f"API {api['name']} version {api['version']} not supported. Supported: {api_info.get('minVersion')}-{api_info.get('maxVersion')}") + + if filtered_apis: + apis_to_try = filtered_apis + else: + logger.warning("No compatible APIs found, trying all methods as fallback") + else: + logger.warning("Failed to query API info, using default methods") + else: + logger.warning(f"API info request failed with status {api_info_response.status_code}, using default methods") + except Exception as e: + logger.warning(f"Error querying API info: {str(e)}, using default methods") + + # Пробуем все доступные методы по порядку + for api in apis_to_try: + logger.info(f"Trying reboot with {api['name']}.{api['method']} v{api['version']}") + result = self._make_api_request(api["name"], api["method"], version=api["version"]) + + if result is not None: + logger.info(f"Successfully initiated system reboot using {api['name']}") + + # Даем системе время начать процесс перезагрузки + logger.info("Waiting for reboot to initialize...") + sleep(5) + + # Ждем, пока система станет недоступна (признак перезагрузки) + reboot_started = False + for i in range(12): # Проверяем в течение 60 секунд (12 * 5сек) + if not self.is_online(force_check=True): + logger.info(f"System went offline after {i*5} seconds, reboot in progress") + reboot_started = True + break + logger.debug(f"System still online, waiting... ({i+1}/12)") + sleep(5) + + if reboot_started: + # Ждем, пока система снова станет доступна + logger.info("Waiting for system to come back online...") + return self.wait_for_boot(max_attempts=30, delay=10) + else: + logger.warning("System did not go offline after reboot command, but command was accepted") + # Даже если система не ушла оффлайн, команда могла быть принята + return True + else: + logger.warning(f"Failed to reboot using {api['name']}.{api['method']} v{api['version']}") + + logger.error("Failed to reboot system after trying multiple APIs") + return False + + def _get_error_description(self, error_code: int) -> str: + """Получение описания ошибки по коду""" + error_descriptions = { + 100: "Unknown error", + 101: "Invalid parameter", + 102: "API does not exist", + 103: "Method does not exist", + 104: "Version does not support", + 105: "Permission denied", + 106: "Session timeout", + 107: "Session interrupted by duplicate login", + 400: "Invalid credentials", + 401: "Account disabled", + 402: "Permission denied", + 403: "2FA required", + 404: "Failed to authenticate with 2FA" + } + return error_descriptions.get(error_code, "Unknown error code") + + def _check_tcp_connection(self) -> bool: + """Проверка базового TCP-соединения с Synology NAS""" + try: + socket_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + socket_obj.settimeout(SYNOLOGY_TIMEOUT) + result = socket_obj.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT)) + socket_obj.close() + + return result == 0 + except socket.error as e: + logger.error(f"Socket error during connection check: {str(e)}") + return False + except Exception as e: + logger.error(f"Unexpected error during connection check: {str(e)}") + return False + + def is_online(self, force_check=False) -> bool: + """Проверка онлайн-статуса Synology NAS""" + # Используем кэшированное значение, если доступно и не устарело + current_time = time.time() + if not force_check and (current_time - self._last_online_check) < self._online_check_interval: + logger.debug(f"Using cached online status: {self._last_online_status}") + return self._last_online_status + + logger.info("Checking if NAS is online...") + + # Проверяем TCP-соединение + online_status = self._check_tcp_connection() + logger.info(f"Detected Synology NAS online status: {online_status}") + + # Если TCP-соединение успешно и у нас есть действующий SID, + # попробуем более детальную проверку через API + if online_status and self.sid: + logger.info("Trying to fetch more detailed online status through API...") + + # Пробуем разные API для проверки онлайн-статуса + api_checks = [ + {"api": "SYNO.DSM.Info", "version": "2", "method": "getinfo"}, + {"api": "SYNO.Core.System", "version": "1", "method": "info"}, + {"api": "SYNO.Core.System.Status", "version": "1", "method": "get"} + ] + + api_success = False + for api_check in api_checks: + try: + url = f"{self.base_url}/entry.cgi" + params = { + "api": api_check["api"], + "version": api_check["version"], + "method": api_check["method"], + "sid": self.sid + } + + logger.debug(f"Trying online status check with {api_check['api']}") + response = self.session.get( + url, + params=params, + timeout=self.default_timeout, + verify=False + ) + + if response.status_code == 200: + data = response.json() + if data.get("success"): + logger.info(f"API request successful for {api_check['api']}") + logger.info("Synology NAS is online with API access") + api_success = True + break + else: + error_code = data.get("error", {}).get("code", -1) + logger.warning(f"API response indicates an error: {error_code}, but NAS is reachable") + else: + logger.warning(f"API returned status code {response.status_code}") + except Exception as e: + logger.warning(f"API check failed for {api_check['api']}, but TCP connection succeeded: {str(e)}") + + if not api_success: + logger.warning("All API checks failed, but TCP connection is successful") + + # Обновляем кэшированное значение + self._last_online_check = current_time + self._last_online_status = online_status + + return online_status + + def wake_on_lan(self) -> bool: + """Отправка Wake-on-LAN пакета для включения Synology NAS""" + if not SYNOLOGY_MAC: + logger.error("MAC address not configured") + return False + + try: + # Преобразование MAC-адреса в байты + mac_address = SYNOLOGY_MAC.replace(':', '').replace('-', '') + if len(mac_address) != 12: + logger.error(f"Invalid MAC address format: {SYNOLOGY_MAC}") + return False + + try: + mac_bytes = bytes.fromhex(mac_address) + except ValueError as e: + logger.error(f"Failed to parse MAC address: {str(e)}") + logger.error(f"MAC address must contain only hex characters: {SYNOLOGY_MAC}") + return False + + # Создание Magic Packet + magic_packet = b'\xff' * 6 + mac_bytes * 16 + + # Отправка пакета на конкретный адрес + try: + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) + sock.sendto(magic_packet, (SYNOLOGY_HOST, WOL_PORT)) + sock.close() + logger.info(f"Wake-on-LAN packet sent to {SYNOLOGY_MAC} at {SYNOLOGY_HOST}:{WOL_PORT}") + except Exception as e: + logger.error(f"Error sending directed WoL packet: {str(e)}") + return False + + # Для надежности отправляем также широковещательный пакет + try: + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) + + # Используем стандартный широковещательный адрес + broadcast_addr = "255.255.255.255" + sock.sendto(magic_packet, (broadcast_addr, WOL_PORT)) + sock.close() + logger.info(f"Wake-on-LAN broadcast packet sent to {broadcast_addr}:{WOL_PORT}") + except Exception as e: + logger.warning(f"Error sending broadcast WoL packet (non-critical): {str(e)}") + # Не считаем ошибкой, т.к. основной пакет уже отправлен + + return True + + except Exception as e: + logger.error(f"Unexpected error in wake_on_lan: {str(e)}") + return False + + def wait_for_boot(self, max_attempts: int = 30, delay: int = 10) -> bool: + """Ожидание загрузки Synology NAS""" + logger.info(f"Waiting for Synology NAS to boot (timeout: {max_attempts * delay}s)...") + + for attempt in range(max_attempts): + # Принудительно проверяем статус без использования кэша + if self.is_online(force_check=True): + logger.info(f"Synology NAS is online after {attempt + 1} attempts ({(attempt + 1) * delay}s)") + + # Проверяем, что не только сеть доступна, но и API загрузился + api_ready = False + logger.info("Waiting for API services to initialize...") + + for api_check in range(5): # Даем еще до 50 секунд для загрузки API + if self.sid or self.login(): + api_ready = True + logger.info(f"API services are ready after {api_check + 1} attempts") + break + logger.debug(f"API not ready yet, waiting... ({api_check + 1}/5)") + sleep(10) + + if not api_ready: + logger.warning("System is online but API services may not be fully initialized") + + # Дадим дополнительное время для полной загрузки всех сервисов + sleep(delay) + return True + + sleep(delay) + logger.info(f"Waiting for boot... Attempt {attempt + 1}/{max_attempts}") + + logger.error(f"Synology NAS didn't come online after {max_attempts * delay} seconds") + return False + + def power_on(self) -> bool: + """Включение Synology NAS""" + # Принудительная проверка статуса + if self.is_online(force_check=True): + logger.info("Synology NAS is already online") + return True + + logger.info("Powering on Synology NAS via Wake-on-LAN...") + + # Проверяем, настроен ли MAC-адрес + if not SYNOLOGY_MAC: + logger.error("Cannot power on: MAC address not configured in settings") + return False + + # Пробуем отправить несколько WoL пакетов для надежности + success = False + for attempt in range(3): + logger.debug(f"Sending WoL packet, attempt {attempt + 1}/3") + if self.wake_on_lan(): + success = True + break + sleep(1) + + if not success: + logger.error("Failed to send Wake-on-LAN packets") + return False + + # Ожидание загрузки + logger.info("WoL packets sent successfully, waiting for system to boot...") + boot_result = self.wait_for_boot(max_attempts=30, delay=10) + + if boot_result: + # Проверяем доступность API после загрузки + system_status = self.get_system_status() + if system_status.get("status") == "online": + logger.info("System booted successfully with API access") + return True + else: + logger.warning("System appears to be online but API may not be fully ready") + return True + else: + logger.error("System did not come online after WoL") + return False + + def power_off(self) -> bool: + """Выключение Synology NAS""" + if not self.is_online(force_check=True): + logger.info("Synology NAS is already offline") + return True + + logger.info("Powering off Synology NAS...") + result = self.shutdown_system() + + if result: + # Дополнительная проверка, что система действительно выключилась + logger.info("Verifying system is offline...") + for attempt in range(12): # Проверяем в течение 2 минут (12 * 10 сек) + if not self.is_online(force_check=True): + logger.info(f"System confirmed offline after {attempt * 10} seconds") + return True + logger.debug(f"System still shutting down, checking again... ({attempt + 1}/12)") + sleep(10) + + logger.warning("System still appears to be online after shutdown command") + return False + else: + logger.error("Failed to initiate shutdown") + return False + + # Заглушки для расширенных методов + def get_shared_folders(self) -> List[Dict[str, Any]]: + """Получение списка общих папок (заглушка)""" + logger.warning("Function get_shared_folders() is not implemented yet") + return [] + + def get_system_load(self) -> Dict[str, Any]: + """Получение информации о загрузке системы (заглушка)""" + logger.warning("Function get_system_load() is not implemented yet") + return {} + + def get_storage_status(self) -> Dict[str, Any]: + """Получение подробной информации о хранилище (заглушка)""" + logger.warning("Function get_storage_status() is not implemented yet") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for storage status request") + return {"error": "authentication_failed"} + + try: + # Здесь будет реализация после добавления соответствующего API + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.Storage.CGI.Storage", + "version": "1", + "method": "load_info", + "sid": self.sid + } + + # В текущей версии просто возвращаем заглушку + return { + "volumes": [], + "disks": [], + "total_size": 0, + "total_used": 0, + "error": "not_implemented" + } + except Exception as e: + logger.error(f"Error in get_storage_status: {str(e)}") + return {"error": str(e)} + + def get_security_status(self) -> Dict[str, Any]: + """Получение информации о состоянии безопасности (заглушка)""" + logger.warning("Function get_security_status() is not implemented yet") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for security status request") + return {"success": False, "error": "authentication_failed"} + + try: + # Здесь будет реализация после добавления соответствующего API + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.Core.SecurityScan.Status", + "version": "1", + "method": "get", + "sid": self.sid + } + + # В текущей версии просто возвращаем заглушку + return { + "success": False, + "status": "not_implemented", + "last_check": None, + "is_secure": False, + "error": "not_implemented" + } + except Exception as e: + logger.error(f"Error in get_security_status: {str(e)}") + return {"success": False, "error": str(e)} diff --git a/.history/src/api/synology_20250830081837.py b/.history/src/api/synology_20250830081837.py new file mode 100644 index 0000000..7b9eff7 --- /dev/null +++ b/.history/src/api/synology_20250830081837.py @@ -0,0 +1,978 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Модуль для взаимодействия с API Synology NAS +""" + +import requests +from requests.adapters import HTTPAdapter +import json +import logging +import time +import urllib3 +from urllib3.util import Retry +from typing import Dict, Any, Optional, List +import socket +import struct +from time import sleep + +from src.config.config import ( + SYNOLOGY_HOST, + SYNOLOGY_PORT, + SYNOLOGY_USERNAME, + SYNOLOGY_PASSWORD, + SYNOLOGY_SECURE, + SYNOLOGY_TIMEOUT, + SYNOLOGY_MAC, + WOL_PORT, + SYNOLOGY_API_VERSION +) +from src.api.api_discovery import discover_available_apis, find_compatible_api + +# Отключение предупреждений о небезопасных SSL-соединениях +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + +logger = logging.getLogger(__name__) + +class SynologyAPI: + """Класс для взаимодействия с API Synology NAS""" + + def __init__(self): + """Инициализация класса SynologyAPI""" + logger.info("Creating API with auto-retry and connection pool") + logger.debug(f"Connection details: Host={SYNOLOGY_HOST}, Port={SYNOLOGY_PORT}, Secure={SYNOLOGY_SECURE}") + + self.protocol = "https" if SYNOLOGY_SECURE else "http" + self.base_url = f"{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi" + self.sid = None + self.session = requests.Session() + + # Настройка SSL + if self.protocol == "https": + logger.debug("SSL enabled, disabling certificate verification for internal network") + self.session.verify = False # Отключаем проверку SSL для внутренней сети + + # Добавляем повторные попытки для HTTP-запросов + retry_strategy = Retry( + total=5, # Увеличиваем количество попыток + status_forcelist=[429, 500, 502, 503, 504, 404], + allowed_methods=["GET", "POST"], + backoff_factor=1.5, # Увеличиваем задержку между попытками + respect_retry_after_header=True + ) + adapter = HTTPAdapter( + max_retries=retry_strategy, + pool_connections=3, + pool_maxsize=10 + ) + self.session.mount("http://", adapter) + self.session.mount("https://", adapter) + + # Таймауты будут указаны в запросах + self.default_timeout = (SYNOLOGY_TIMEOUT, SYNOLOGY_TIMEOUT*2) + logger.debug(f"Setting default request timeout: {self.default_timeout}") + + # Кэш для хранения результатов запросов + self._cache = {} + self._cache_ttl = {} + self._last_online_check = 0 + self._last_online_status = False + self._online_check_interval = 30 # Интервал для кэширования проверок онлайн-статуса + + # Время последней успешной аутентификации и срок действия сессии + self._last_auth_time = 0 + self._auth_expiry = 3600 # По умолчанию 1 час + + # Информация о доступных API + self._available_apis = {} + self._api_info_ttl = 0 + + def login(self) -> bool: + """Авторизация в API Synology NAS""" + # Проверяем, действительна ли текущая сессия + current_time = time.time() + if self.sid and (current_time - self._last_auth_time) < self._auth_expiry: + logger.debug("Using existing session, still valid") + return True + + logger.info("Attempting to authenticate with Synology NAS...") + logger.debug(f"Base URL: {self.base_url}") + + # Проверяем доступность NAS перед запросом с помощью прямой TCP-проверки + # Избегаем вызова is_online(), чтобы не создавать рекурсию + online_status = self._check_tcp_connection() + if not online_status: + logger.error("Cannot login: Synology NAS is not reachable") + return False + + try: + # Проверка информации API для определения доступных версий API + api_info_url = f"{self.base_url}/entry.cgi" + api_info_params = { + "api": "SYNO.API.Info", + "version": "1", + "method": "query", + "query": "SYNO.API.Auth" + } + + logger.debug("Querying API info to determine optimal auth version") + try: + api_info_response = self.session.get( + api_info_url, + params=api_info_params, + timeout=self.default_timeout, + verify=False + ) + + if api_info_response.status_code == 200: + api_info_data = api_info_response.json() + if api_info_data.get("success"): + auth_info = api_info_data.get("data", {}).get("SYNO.API.Auth", {}) + max_version = auth_info.get("maxVersion", 3) + min_version = auth_info.get("minVersion", 1) + auth_path = auth_info.get("path", "auth.cgi") + logger.debug(f"Auth API versions: min={min_version}, max={max_version}, path={auth_path}") + + # Используем максимально поддерживаемую версию, но не выше 6 + auth_version = min(max_version, 6) + else: + logger.warning("Failed to query API info, using default auth version 3") + auth_version = 3 + auth_path = "auth.cgi" + else: + logger.warning(f"API info request failed with status {api_info_response.status_code}, using default auth version 3") + auth_version = 3 + auth_path = "auth.cgi" + except Exception as e: + logger.warning(f"Error querying API info: {str(e)}, using default auth version 3") + auth_version = 3 + auth_path = "auth.cgi" + + # Основной запрос авторизации + url = f"{self.base_url}/{auth_path}" + params = { + "api": "SYNO.API.Auth", + "version": str(auth_version), + "method": "login", + "account": SYNOLOGY_USERNAME, + "passwd": SYNOLOGY_PASSWORD, + "session": "SynologyPowerControlBot", + "format": "cookie" + } + + # Для версии 6+ используем немного другой формат + if auth_version >= 6: + params["enable_syno_token"] = "yes" + + logger.debug(f"Sending auth request to {url} with API version {auth_version}") + start_time = time.time() + response = self.session.get( + url, + params=params, + timeout=self.default_timeout, + verify=False + ) + elapsed_time = time.time() - start_time + logger.debug(f"Auth request completed in {elapsed_time:.2f}s with status code {response.status_code}") + + # Подробная информация для отладки + logger.debug(f"Auth response: {response.text[:500]}") + + # Проверка статуса HTTP + if response.status_code != 200: + logger.error(f"HTTP error: {response.status_code}") + logger.debug(f"Response content: {response.text[:200]}") + return False + + try: + data = response.json() + except json.JSONDecodeError: + logger.error("Failed to decode JSON response") + logger.debug(f"Response content: {response.text[:200]}") + return False + + if data.get("success"): + self.sid = data.get("data", {}).get("sid") + self._last_auth_time = current_time + logger.info("Successfully logged in to Synology NAS") + logger.debug(f"Session ID obtained: {self.sid[:5] if self.sid else 'None'}...") # Показываем только начало SID для безопасности + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to log in to Synology NAS: Error code {error_code}") + + # Дополнительная диагностика + if error_code == 400: + logger.error("Authentication error: Invalid credentials") + elif error_code == 401: + logger.error("Authentication error: Account disabled") + elif error_code == 402: + logger.error("Authentication error: Permission denied") + elif error_code == 403: + logger.error("Authentication error: 2-factor authentication required") + elif error_code == 404: + logger.error("Authentication error: Failed to authenticate with 2-factor authentication") + + return False + + except requests.exceptions.Timeout: + logger.error(f"Connection timeout after {SYNOLOGY_TIMEOUT} seconds") + return False + except requests.exceptions.ConnectionError as e: + logger.error(f"Connection error: {str(e)}") + logger.debug(f"Connection details: {self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}") + return False + except requests.RequestException as e: + logger.error(f"Request error: {str(e)}") + return False + except Exception as e: + logger.error(f"Unexpected error during login: {str(e)}", exc_info=True) + return False + + def logout(self) -> bool: + """Выход из API Synology NAS""" + if not self.sid: + return True + + try: + url = f"{self.base_url}/auth.cgi" + params = { + "api": "SYNO.API.Auth", + "version": "1", + "method": "logout", + "session": "SynologyPowerControlBot", + "_sid": self.sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + self.sid = None + logger.info("Successfully logged out from Synology NAS") + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to log out from Synology NAS: Error code {error_code}") + return False + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + return False + + def _make_api_request(self, api_name: str, method: str, version: int = 1, + params: Optional[Dict[str, Any]] = None, retry_auth: bool = True) -> Optional[Dict[str, Any]]: + """Обобщенный метод для выполнения API запросов с обработкой ошибок""" + # Проверка наличия авторизации + if not self.sid and not self.login(): + logger.error(f"Not authenticated for API request: {api_name}.{method}") + return None + + # Проверка информации API для определения пути и поддерживаемой версии + api_info_url = f"{self.base_url}/entry.cgi" + api_info_params = { + "api": "SYNO.API.Info", + "version": "1", + "method": "query", + "query": api_name + } + + api_path = "entry.cgi" + try: + logger.debug(f"Querying API info for {api_name}") + api_info_response = self.session.get( + api_info_url, + params=api_info_params, + timeout=self.default_timeout, + verify=False + ) + + if api_info_response.status_code == 200: + api_info_data = api_info_response.json() + if api_info_data.get("success"): + api_info = api_info_data.get("data", {}).get(api_name, {}) + if api_info: + max_version = api_info.get("maxVersion", version) + min_version = api_info.get("minVersion", version) + api_path = api_info.get("path", "entry.cgi") + + # Проверка, поддерживается ли запрошенная версия + if version < min_version: + logger.warning(f"API version {version} for {api_name} is below minimum {min_version}, using {min_version}") + version = min_version + elif version > max_version: + logger.warning(f"API version {version} for {api_name} exceeds maximum {max_version}, using {max_version}") + version = max_version + + logger.debug(f"Using API path: {api_path}, version: {version}") + else: + logger.warning(f"API {api_name} not found in API info, using defaults") + except Exception as e: + logger.warning(f"Error querying API info for {api_name}: {str(e)}, using defaults") + + # Подготовка базовых параметров запроса + base_params = { + "api": api_name, + "version": str(version), + "method": method, + "sid": self.sid + } + + # Добавление дополнительных параметров, если они заданы + if params: + base_params.update(params) + + url = f"{self.base_url}/{api_path}" + logger.debug(f"API request: {api_name}.{method} v{version} to {url}") + logger.debug(f"Full request params: {base_params}") + + try: + start_time = time.time() + response = self.session.get( + url, + params=base_params, + timeout=self.default_timeout, + verify=False + ) + elapsed_time = time.time() - start_time + logger.debug(f"API request completed in {elapsed_time:.2f}s with status {response.status_code}") + + # Подробное логирование для отладки + logger.debug(f"Response: {response.text[:500]}") + + # Проверка статуса HTTP + if response.status_code != 200: + logger.error(f"HTTP error: {response.status_code} for {api_name}.{method}") + return None + + try: + data = response.json() + except json.JSONDecodeError: + logger.error(f"Failed to decode JSON response for {api_name}.{method}") + logger.debug(f"Response content: {response.text[:200]}") + return None + + if data.get("success"): + logger.info(f"API request successful for {api_name}.{method}") + return data.get("data", {}) + else: + error_code = data.get("error", {}).get("code", -1) + error_desc = self._get_error_description(error_code) + logger.error(f"API error for {api_name}.{method}: {error_code} - {error_desc}") + + # Ошибки доступа или прав часто встречаются, но они не критичные + # Например, ошибка 102 означает, что нет прав, но NAS доступен + if error_code in [102, 103, 104, 105]: + logger.warning(f"Permission-related error for {api_name}.{method}: {error_code}") + # Возвращаем пустой словарь вместо None, + # чтобы вызывающий код мог понять, что запрос выполнен + return {} + + # Если ошибка связана с авторизацией и нам разрешено повторить попытку + if error_code in [106, 107, 119] and retry_auth: + logger.info("Session may have expired, re-authenticating...") + self.sid = None # Сбрасываем SID + if self.login(): + logger.info("Re-authenticated, retrying API request...") + # Рекурсивный вызов, но без повторной авторизации во избежание зацикливания + return self._make_api_request(api_name, method, version, params, False) + + return None + + except requests.exceptions.Timeout: + logger.error(f"Request timeout for {api_name}.{method}") + return None + except requests.exceptions.ConnectionError as e: + logger.error(f"Connection error for {api_name}.{method}: {str(e)}") + return None + except Exception as e: + logger.error(f"Unexpected error during {api_name}.{method}: {str(e)}") + return None + + def get_system_status(self) -> Dict[str, Any]: + """Получение статуса системы""" + # Проверяем доступность системы + if not self.is_online(): + logger.info("Device is offline, skipping API request") + return {"status": "offline"} + + # Проверяем, есть ли кэшированный результат + cache_key = "system_status" + current_time = time.time() + if cache_key in self._cache and current_time - self._cache_ttl.get(cache_key, 0) < 60: + logger.debug("Using cached system status") + return self._cache[cache_key] + + # Пробуем разные API для получения информации о системе + apis_to_try = [ + {"name": "SYNO.DSM.Info", "method": "getinfo", "version": 2}, + {"name": "SYNO.Core.System", "method": "info", "version": 1}, + {"name": "SYNO.Core.System.Status", "method": "get", "version": 1}, + {"name": "SYNO.Core.System.Info", "method": "get", "version": 1}, + ] + + for api in apis_to_try: + logger.info(f"Trying to get system info using {api['name']}.{api['method']} v{api['version']}") + result = self._make_api_request(api["name"], api["method"], version=api["version"]) + + if result: + logger.info(f"Successfully retrieved system info using {api['name']}") + + # Формируем расширенный ответ с дополнительной информацией + system_info = { + "status": "online", + "hostname": result.get("hostname", "unknown"), + "model": result.get("model", "unknown"), + "version": result.get("version", "unknown"), + "uptime": result.get("uptime", 0), + "time": current_time, + "is_online": True, + "api_used": api["name"] + } + + # Сохраняем в кэше + self._cache[cache_key] = system_info + self._cache_ttl[cache_key] = current_time + + return system_info + + # Если все запросы не удались, но система онлайн, возвращаем базовую информацию + logger.warning("Failed to retrieve system info with all API methods") + return { + "status": "error", + "error": "Failed to fetch system information", + "is_online": True, + "time": current_time + } + + def shutdown_system(self) -> bool: + """Выключение системы""" + # Проверяем, включено ли устройство перед попыткой его выключить + if not self.is_online(force_check=True): + logger.info("Synology NAS is already offline, no need to shut down") + return True + + logger.info("Attempting to shutdown Synology NAS...") + + # Проверка всех доступных методов API для выключения + # Проверяем наличие API перед использованием + apis_to_try = [ + {"name": "SYNO.Core.System.Power", "method": "shutdown", "version": 1}, + {"name": "SYNO.DSM.Power", "method": "shutdown", "version": 1}, + {"name": "SYNO.Core.System", "method": "shutdown", "version": 3}, + {"name": "SYNO.System.Power", "method": "shutdown", "version": 1}, + {"name": "SYNO.DSM.System", "method": "shutdown", "version": 1} + ] + + # Получаем список доступных API + try: + api_info_url = f"{self.base_url}/entry.cgi" + api_info_params = { + "api": "SYNO.API.Info", + "version": "1", + "method": "query", + "query": "SYNO.Core.System.Power,SYNO.DSM.Power,SYNO.Core.System,SYNO.System.Power,SYNO.DSM.System" + } + + logger.debug("Checking available shutdown APIs") + api_info_response = self.session.get( + api_info_url, + params=api_info_params, + timeout=self.default_timeout, + verify=False + ) + + if api_info_response.status_code == 200: + api_info_data = api_info_response.json() + if api_info_data.get("success"): + available_apis = api_info_data.get("data", {}) + logger.debug(f"Available APIs: {list(available_apis.keys())}") + + # Фильтруем только доступные API + filtered_apis = [] + for api in apis_to_try: + if api["name"] in available_apis: + api_info = available_apis[api["name"]] + # Проверка версии API + if api_info.get("minVersion", 1) <= api["version"] <= api_info.get("maxVersion", 1): + filtered_apis.append(api) + logger.debug(f"Adding {api['name']} to available shutdown APIs") + else: + logger.debug(f"API {api['name']} version {api['version']} not supported. Supported: {api_info.get('minVersion')}-{api_info.get('maxVersion')}") + + if filtered_apis: + apis_to_try = filtered_apis + else: + logger.warning("No compatible APIs found, trying all methods as fallback") + else: + logger.warning("Failed to query API info, using default methods") + else: + logger.warning(f"API info request failed with status {api_info_response.status_code}, using default methods") + except Exception as e: + logger.warning(f"Error querying API info: {str(e)}, using default methods") + + # Пробуем все доступные методы по порядку + for api in apis_to_try: + logger.info(f"Trying shutdown with {api['name']}.{api['method']} v{api['version']}") + result = self._make_api_request(api["name"], api["method"], version=api["version"]) + + if result is not None: + logger.info(f"Successfully initiated system shutdown using {api['name']}") + + # Даем системе время начать процесс выключения + logger.info("Waiting for shutdown to initialize...") + sleep(5) + + # Проверяем статус + if not self.is_online(force_check=True): + logger.info("System is now offline. Shutdown confirmed successful.") + return True + else: + logger.info("System still appears to be online, but shutdown may be in progress.") + return True + else: + logger.warning(f"Failed to shutdown using {api['name']}.{api['method']} v{api['version']}") + + # Если ни один метод не сработал, но система стала недоступна + if not self.is_online(force_check=True): + logger.info("System appears to be shutting down despite API errors") + return True + + logger.error("Failed to shutdown system after trying multiple APIs") + return False + + def reboot_system(self) -> bool: + """Перезагрузка системы""" + # Проверяем, включена ли система + if not self.is_online(force_check=True): + logger.error("Cannot reboot: System is offline") + return False + + logger.info("Attempting to reboot Synology NAS...") + + # Проверка всех доступных методов API для перезагрузки + apis_to_try = [ + {"name": "SYNO.Core.System.Power", "method": "restart", "version": 1}, + {"name": "SYNO.DSM.Power", "method": "reboot", "version": 1}, + {"name": "SYNO.Core.System", "method": "reboot", "version": 3}, + {"name": "SYNO.System.Power", "method": "reboot", "version": 1}, + {"name": "SYNO.DSM.System", "method": "reboot", "version": 1} + ] + + # Получаем список доступных API + try: + api_info_url = f"{self.base_url}/entry.cgi" + api_info_params = { + "api": "SYNO.API.Info", + "version": "1", + "method": "query", + "query": "SYNO.Core.System.Power,SYNO.DSM.Power,SYNO.Core.System,SYNO.System.Power,SYNO.DSM.System" + } + + logger.debug("Checking available reboot APIs") + api_info_response = self.session.get( + api_info_url, + params=api_info_params, + timeout=self.default_timeout, + verify=False + ) + + if api_info_response.status_code == 200: + api_info_data = api_info_response.json() + if api_info_data.get("success"): + available_apis = api_info_data.get("data", {}) + logger.debug(f"Available APIs: {list(available_apis.keys())}") + + # Фильтруем только доступные API + filtered_apis = [] + for api in apis_to_try: + if api["name"] in available_apis: + api_info = available_apis[api["name"]] + # Проверка версии API + if api_info.get("minVersion", 1) <= api["version"] <= api_info.get("maxVersion", 1): + filtered_apis.append(api) + logger.debug(f"Adding {api['name']} to available reboot APIs") + else: + logger.debug(f"API {api['name']} version {api['version']} not supported. Supported: {api_info.get('minVersion')}-{api_info.get('maxVersion')}") + + if filtered_apis: + apis_to_try = filtered_apis + else: + logger.warning("No compatible APIs found, trying all methods as fallback") + else: + logger.warning("Failed to query API info, using default methods") + else: + logger.warning(f"API info request failed with status {api_info_response.status_code}, using default methods") + except Exception as e: + logger.warning(f"Error querying API info: {str(e)}, using default methods") + + # Пробуем все доступные методы по порядку + for api in apis_to_try: + logger.info(f"Trying reboot with {api['name']}.{api['method']} v{api['version']}") + result = self._make_api_request(api["name"], api["method"], version=api["version"]) + + if result is not None: + logger.info(f"Successfully initiated system reboot using {api['name']}") + + # Даем системе время начать процесс перезагрузки + logger.info("Waiting for reboot to initialize...") + sleep(5) + + # Ждем, пока система станет недоступна (признак перезагрузки) + reboot_started = False + for i in range(12): # Проверяем в течение 60 секунд (12 * 5сек) + if not self.is_online(force_check=True): + logger.info(f"System went offline after {i*5} seconds, reboot in progress") + reboot_started = True + break + logger.debug(f"System still online, waiting... ({i+1}/12)") + sleep(5) + + if reboot_started: + # Ждем, пока система снова станет доступна + logger.info("Waiting for system to come back online...") + return self.wait_for_boot(max_attempts=30, delay=10) + else: + logger.warning("System did not go offline after reboot command, but command was accepted") + # Даже если система не ушла оффлайн, команда могла быть принята + return True + else: + logger.warning(f"Failed to reboot using {api['name']}.{api['method']} v{api['version']}") + + logger.error("Failed to reboot system after trying multiple APIs") + return False + + def _get_error_description(self, error_code: int) -> str: + """Получение описания ошибки по коду""" + error_descriptions = { + 100: "Unknown error", + 101: "Invalid parameter", + 102: "API does not exist", + 103: "Method does not exist", + 104: "Version does not support", + 105: "Permission denied", + 106: "Session timeout", + 107: "Session interrupted by duplicate login", + 400: "Invalid credentials", + 401: "Account disabled", + 402: "Permission denied", + 403: "2FA required", + 404: "Failed to authenticate with 2FA" + } + return error_descriptions.get(error_code, "Unknown error code") + + def _check_tcp_connection(self) -> bool: + """Проверка базового TCP-соединения с Synology NAS""" + try: + socket_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + socket_obj.settimeout(SYNOLOGY_TIMEOUT) + result = socket_obj.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT)) + socket_obj.close() + + return result == 0 + except socket.error as e: + logger.error(f"Socket error during connection check: {str(e)}") + return False + except Exception as e: + logger.error(f"Unexpected error during connection check: {str(e)}") + return False + + def is_online(self, force_check=False) -> bool: + """Проверка онлайн-статуса Synology NAS""" + # Используем кэшированное значение, если доступно и не устарело + current_time = time.time() + if not force_check and (current_time - self._last_online_check) < self._online_check_interval: + logger.debug(f"Using cached online status: {self._last_online_status}") + return self._last_online_status + + logger.info("Checking if NAS is online...") + + # Проверяем TCP-соединение + online_status = self._check_tcp_connection() + logger.info(f"Detected Synology NAS online status: {online_status}") + + # Если TCP-соединение успешно и у нас есть действующий SID, + # попробуем более детальную проверку через API + if online_status and self.sid: + logger.info("Trying to fetch more detailed online status through API...") + + # Пробуем разные API для проверки онлайн-статуса + api_checks = [ + {"api": "SYNO.DSM.Info", "version": "2", "method": "getinfo"}, + {"api": "SYNO.Core.System", "version": "1", "method": "info"}, + {"api": "SYNO.Core.System.Status", "version": "1", "method": "get"} + ] + + api_success = False + for api_check in api_checks: + try: + url = f"{self.base_url}/entry.cgi" + params = { + "api": api_check["api"], + "version": api_check["version"], + "method": api_check["method"], + "sid": self.sid + } + + logger.debug(f"Trying online status check with {api_check['api']}") + response = self.session.get( + url, + params=params, + timeout=self.default_timeout, + verify=False + ) + + if response.status_code == 200: + data = response.json() + if data.get("success"): + logger.info(f"API request successful for {api_check['api']}") + logger.info("Synology NAS is online with API access") + api_success = True + break + else: + error_code = data.get("error", {}).get("code", -1) + logger.warning(f"API response indicates an error: {error_code}, but NAS is reachable") + else: + logger.warning(f"API returned status code {response.status_code}") + except Exception as e: + logger.warning(f"API check failed for {api_check['api']}, but TCP connection succeeded: {str(e)}") + + if not api_success: + logger.warning("All API checks failed, but TCP connection is successful") + + # Обновляем кэшированное значение + self._last_online_check = current_time + self._last_online_status = online_status + + return online_status + + def wake_on_lan(self) -> bool: + """Отправка Wake-on-LAN пакета для включения Synology NAS""" + if not SYNOLOGY_MAC: + logger.error("MAC address not configured") + return False + + try: + # Преобразование MAC-адреса в байты + mac_address = SYNOLOGY_MAC.replace(':', '').replace('-', '') + if len(mac_address) != 12: + logger.error(f"Invalid MAC address format: {SYNOLOGY_MAC}") + return False + + try: + mac_bytes = bytes.fromhex(mac_address) + except ValueError as e: + logger.error(f"Failed to parse MAC address: {str(e)}") + logger.error(f"MAC address must contain only hex characters: {SYNOLOGY_MAC}") + return False + + # Создание Magic Packet + magic_packet = b'\xff' * 6 + mac_bytes * 16 + + # Отправка пакета на конкретный адрес + try: + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) + sock.sendto(magic_packet, (SYNOLOGY_HOST, WOL_PORT)) + sock.close() + logger.info(f"Wake-on-LAN packet sent to {SYNOLOGY_MAC} at {SYNOLOGY_HOST}:{WOL_PORT}") + except Exception as e: + logger.error(f"Error sending directed WoL packet: {str(e)}") + return False + + # Для надежности отправляем также широковещательный пакет + try: + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) + + # Используем стандартный широковещательный адрес + broadcast_addr = "255.255.255.255" + sock.sendto(magic_packet, (broadcast_addr, WOL_PORT)) + sock.close() + logger.info(f"Wake-on-LAN broadcast packet sent to {broadcast_addr}:{WOL_PORT}") + except Exception as e: + logger.warning(f"Error sending broadcast WoL packet (non-critical): {str(e)}") + # Не считаем ошибкой, т.к. основной пакет уже отправлен + + return True + + except Exception as e: + logger.error(f"Unexpected error in wake_on_lan: {str(e)}") + return False + + def wait_for_boot(self, max_attempts: int = 30, delay: int = 10) -> bool: + """Ожидание загрузки Synology NAS""" + logger.info(f"Waiting for Synology NAS to boot (timeout: {max_attempts * delay}s)...") + + for attempt in range(max_attempts): + # Принудительно проверяем статус без использования кэша + if self.is_online(force_check=True): + logger.info(f"Synology NAS is online after {attempt + 1} attempts ({(attempt + 1) * delay}s)") + + # Проверяем, что не только сеть доступна, но и API загрузился + api_ready = False + logger.info("Waiting for API services to initialize...") + + for api_check in range(5): # Даем еще до 50 секунд для загрузки API + if self.sid or self.login(): + api_ready = True + logger.info(f"API services are ready after {api_check + 1} attempts") + break + logger.debug(f"API not ready yet, waiting... ({api_check + 1}/5)") + sleep(10) + + if not api_ready: + logger.warning("System is online but API services may not be fully initialized") + + # Дадим дополнительное время для полной загрузки всех сервисов + sleep(delay) + return True + + sleep(delay) + logger.info(f"Waiting for boot... Attempt {attempt + 1}/{max_attempts}") + + logger.error(f"Synology NAS didn't come online after {max_attempts * delay} seconds") + return False + + def power_on(self) -> bool: + """Включение Synology NAS""" + # Принудительная проверка статуса + if self.is_online(force_check=True): + logger.info("Synology NAS is already online") + return True + + logger.info("Powering on Synology NAS via Wake-on-LAN...") + + # Проверяем, настроен ли MAC-адрес + if not SYNOLOGY_MAC: + logger.error("Cannot power on: MAC address not configured in settings") + return False + + # Пробуем отправить несколько WoL пакетов для надежности + success = False + for attempt in range(3): + logger.debug(f"Sending WoL packet, attempt {attempt + 1}/3") + if self.wake_on_lan(): + success = True + break + sleep(1) + + if not success: + logger.error("Failed to send Wake-on-LAN packets") + return False + + # Ожидание загрузки + logger.info("WoL packets sent successfully, waiting for system to boot...") + boot_result = self.wait_for_boot(max_attempts=30, delay=10) + + if boot_result: + # Проверяем доступность API после загрузки + system_status = self.get_system_status() + if system_status.get("status") == "online": + logger.info("System booted successfully with API access") + return True + else: + logger.warning("System appears to be online but API may not be fully ready") + return True + else: + logger.error("System did not come online after WoL") + return False + + def power_off(self) -> bool: + """Выключение Synology NAS""" + if not self.is_online(force_check=True): + logger.info("Synology NAS is already offline") + return True + + logger.info("Powering off Synology NAS...") + result = self.shutdown_system() + + if result: + # Дополнительная проверка, что система действительно выключилась + logger.info("Verifying system is offline...") + for attempt in range(12): # Проверяем в течение 2 минут (12 * 10 сек) + if not self.is_online(force_check=True): + logger.info(f"System confirmed offline after {attempt * 10} seconds") + return True + logger.debug(f"System still shutting down, checking again... ({attempt + 1}/12)") + sleep(10) + + logger.warning("System still appears to be online after shutdown command") + return False + else: + logger.error("Failed to initiate shutdown") + return False + + # Заглушки для расширенных методов + def get_shared_folders(self) -> List[Dict[str, Any]]: + """Получение списка общих папок (заглушка)""" + logger.warning("Function get_shared_folders() is not implemented yet") + return [] + + def get_system_load(self) -> Dict[str, Any]: + """Получение информации о загрузке системы (заглушка)""" + logger.warning("Function get_system_load() is not implemented yet") + return {} + + def get_storage_status(self) -> Dict[str, Any]: + """Получение подробной информации о хранилище (заглушка)""" + logger.warning("Function get_storage_status() is not implemented yet") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for storage status request") + return {"error": "authentication_failed"} + + try: + # Здесь будет реализация после добавления соответствующего API + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.Storage.CGI.Storage", + "version": "1", + "method": "load_info", + "sid": self.sid + } + + # В текущей версии просто возвращаем заглушку + return { + "volumes": [], + "disks": [], + "total_size": 0, + "total_used": 0, + "error": "not_implemented" + } + except Exception as e: + logger.error(f"Error in get_storage_status: {str(e)}") + return {"error": str(e)} + + def get_security_status(self) -> Dict[str, Any]: + """Получение информации о состоянии безопасности (заглушка)""" + logger.warning("Function get_security_status() is not implemented yet") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for security status request") + return {"success": False, "error": "authentication_failed"} + + try: + # Здесь будет реализация после добавления соответствующего API + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.Core.SecurityScan.Status", + "version": "1", + "method": "get", + "sid": self.sid + } + + # В текущей версии просто возвращаем заглушку + return { + "success": False, + "status": "not_implemented", + "last_check": None, + "is_secure": False, + "error": "not_implemented" + } + except Exception as e: + logger.error(f"Error in get_security_status: {str(e)}") + return {"success": False, "error": str(e)} diff --git a/.history/src/api/synology_20250830081856.py b/.history/src/api/synology_20250830081856.py new file mode 100644 index 0000000..4d9457d --- /dev/null +++ b/.history/src/api/synology_20250830081856.py @@ -0,0 +1,977 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Модуль для взаимодействия с API Synology NAS +""" + +import requests +from requests.adapters import HTTPAdapter +import json +import logging +import time +import urllib3 +from urllib3.util import Retry +from typing import Dict, Any, Optional, List +import socket +import struct +from time import sleep + +from src.config.config import ( + SYNOLOGY_HOST, + SYNOLOGY_PORT, + SYNOLOGY_USERNAME, + SYNOLOGY_PASSWORD, + SYNOLOGY_SECURE, + SYNOLOGY_TIMEOUT, + SYNOLOGY_MAC, + WOL_PORT +) +from src.api.api_discovery import discover_available_apis, find_compatible_api + +# Отключение предупреждений о небезопасных SSL-соединениях +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + +logger = logging.getLogger(__name__) + +class SynologyAPI: + """Класс для взаимодействия с API Synology NAS""" + + def __init__(self): + """Инициализация класса SynologyAPI""" + logger.info("Creating API with auto-retry and connection pool") + logger.debug(f"Connection details: Host={SYNOLOGY_HOST}, Port={SYNOLOGY_PORT}, Secure={SYNOLOGY_SECURE}") + + self.protocol = "https" if SYNOLOGY_SECURE else "http" + self.base_url = f"{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi" + self.sid = None + self.session = requests.Session() + + # Настройка SSL + if self.protocol == "https": + logger.debug("SSL enabled, disabling certificate verification for internal network") + self.session.verify = False # Отключаем проверку SSL для внутренней сети + + # Добавляем повторные попытки для HTTP-запросов + retry_strategy = Retry( + total=5, # Увеличиваем количество попыток + status_forcelist=[429, 500, 502, 503, 504, 404], + allowed_methods=["GET", "POST"], + backoff_factor=1.5, # Увеличиваем задержку между попытками + respect_retry_after_header=True + ) + adapter = HTTPAdapter( + max_retries=retry_strategy, + pool_connections=3, + pool_maxsize=10 + ) + self.session.mount("http://", adapter) + self.session.mount("https://", adapter) + + # Таймауты будут указаны в запросах + self.default_timeout = (SYNOLOGY_TIMEOUT, SYNOLOGY_TIMEOUT*2) + logger.debug(f"Setting default request timeout: {self.default_timeout}") + + # Кэш для хранения результатов запросов + self._cache = {} + self._cache_ttl = {} + self._last_online_check = 0 + self._last_online_status = False + self._online_check_interval = 30 # Интервал для кэширования проверок онлайн-статуса + + # Время последней успешной аутентификации и срок действия сессии + self._last_auth_time = 0 + self._auth_expiry = 3600 # По умолчанию 1 час + + # Информация о доступных API + self._available_apis = {} + self._api_info_ttl = 0 + + def login(self) -> bool: + """Авторизация в API Synology NAS""" + # Проверяем, действительна ли текущая сессия + current_time = time.time() + if self.sid and (current_time - self._last_auth_time) < self._auth_expiry: + logger.debug("Using existing session, still valid") + return True + + logger.info("Attempting to authenticate with Synology NAS...") + logger.debug(f"Base URL: {self.base_url}") + + # Проверяем доступность NAS перед запросом с помощью прямой TCP-проверки + # Избегаем вызова is_online(), чтобы не создавать рекурсию + online_status = self._check_tcp_connection() + if not online_status: + logger.error("Cannot login: Synology NAS is not reachable") + return False + + try: + # Проверка информации API для определения доступных версий API + api_info_url = f"{self.base_url}/entry.cgi" + api_info_params = { + "api": "SYNO.API.Info", + "version": "1", + "method": "query", + "query": "SYNO.API.Auth" + } + + logger.debug("Querying API info to determine optimal auth version") + try: + api_info_response = self.session.get( + api_info_url, + params=api_info_params, + timeout=self.default_timeout, + verify=False + ) + + if api_info_response.status_code == 200: + api_info_data = api_info_response.json() + if api_info_data.get("success"): + auth_info = api_info_data.get("data", {}).get("SYNO.API.Auth", {}) + max_version = auth_info.get("maxVersion", 3) + min_version = auth_info.get("minVersion", 1) + auth_path = auth_info.get("path", "auth.cgi") + logger.debug(f"Auth API versions: min={min_version}, max={max_version}, path={auth_path}") + + # Используем максимально поддерживаемую версию, но не выше 6 + auth_version = min(max_version, 6) + else: + logger.warning("Failed to query API info, using default auth version 3") + auth_version = 3 + auth_path = "auth.cgi" + else: + logger.warning(f"API info request failed with status {api_info_response.status_code}, using default auth version 3") + auth_version = 3 + auth_path = "auth.cgi" + except Exception as e: + logger.warning(f"Error querying API info: {str(e)}, using default auth version 3") + auth_version = 3 + auth_path = "auth.cgi" + + # Основной запрос авторизации + url = f"{self.base_url}/{auth_path}" + params = { + "api": "SYNO.API.Auth", + "version": str(auth_version), + "method": "login", + "account": SYNOLOGY_USERNAME, + "passwd": SYNOLOGY_PASSWORD, + "session": "SynologyPowerControlBot", + "format": "cookie" + } + + # Для версии 6+ используем немного другой формат + if auth_version >= 6: + params["enable_syno_token"] = "yes" + + logger.debug(f"Sending auth request to {url} with API version {auth_version}") + start_time = time.time() + response = self.session.get( + url, + params=params, + timeout=self.default_timeout, + verify=False + ) + elapsed_time = time.time() - start_time + logger.debug(f"Auth request completed in {elapsed_time:.2f}s with status code {response.status_code}") + + # Подробная информация для отладки + logger.debug(f"Auth response: {response.text[:500]}") + + # Проверка статуса HTTP + if response.status_code != 200: + logger.error(f"HTTP error: {response.status_code}") + logger.debug(f"Response content: {response.text[:200]}") + return False + + try: + data = response.json() + except json.JSONDecodeError: + logger.error("Failed to decode JSON response") + logger.debug(f"Response content: {response.text[:200]}") + return False + + if data.get("success"): + self.sid = data.get("data", {}).get("sid") + self._last_auth_time = current_time + logger.info("Successfully logged in to Synology NAS") + logger.debug(f"Session ID obtained: {self.sid[:5] if self.sid else 'None'}...") # Показываем только начало SID для безопасности + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to log in to Synology NAS: Error code {error_code}") + + # Дополнительная диагностика + if error_code == 400: + logger.error("Authentication error: Invalid credentials") + elif error_code == 401: + logger.error("Authentication error: Account disabled") + elif error_code == 402: + logger.error("Authentication error: Permission denied") + elif error_code == 403: + logger.error("Authentication error: 2-factor authentication required") + elif error_code == 404: + logger.error("Authentication error: Failed to authenticate with 2-factor authentication") + + return False + + except requests.exceptions.Timeout: + logger.error(f"Connection timeout after {SYNOLOGY_TIMEOUT} seconds") + return False + except requests.exceptions.ConnectionError as e: + logger.error(f"Connection error: {str(e)}") + logger.debug(f"Connection details: {self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}") + return False + except requests.RequestException as e: + logger.error(f"Request error: {str(e)}") + return False + except Exception as e: + logger.error(f"Unexpected error during login: {str(e)}", exc_info=True) + return False + + def logout(self) -> bool: + """Выход из API Synology NAS""" + if not self.sid: + return True + + try: + url = f"{self.base_url}/auth.cgi" + params = { + "api": "SYNO.API.Auth", + "version": "1", + "method": "logout", + "session": "SynologyPowerControlBot", + "_sid": self.sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + self.sid = None + logger.info("Successfully logged out from Synology NAS") + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to log out from Synology NAS: Error code {error_code}") + return False + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + return False + + def _make_api_request(self, api_name: str, method: str, version: int = 1, + params: Optional[Dict[str, Any]] = None, retry_auth: bool = True) -> Optional[Dict[str, Any]]: + """Обобщенный метод для выполнения API запросов с обработкой ошибок""" + # Проверка наличия авторизации + if not self.sid and not self.login(): + logger.error(f"Not authenticated for API request: {api_name}.{method}") + return None + + # Проверка информации API для определения пути и поддерживаемой версии + api_info_url = f"{self.base_url}/entry.cgi" + api_info_params = { + "api": "SYNO.API.Info", + "version": "1", + "method": "query", + "query": api_name + } + + api_path = "entry.cgi" + try: + logger.debug(f"Querying API info for {api_name}") + api_info_response = self.session.get( + api_info_url, + params=api_info_params, + timeout=self.default_timeout, + verify=False + ) + + if api_info_response.status_code == 200: + api_info_data = api_info_response.json() + if api_info_data.get("success"): + api_info = api_info_data.get("data", {}).get(api_name, {}) + if api_info: + max_version = api_info.get("maxVersion", version) + min_version = api_info.get("minVersion", version) + api_path = api_info.get("path", "entry.cgi") + + # Проверка, поддерживается ли запрошенная версия + if version < min_version: + logger.warning(f"API version {version} for {api_name} is below minimum {min_version}, using {min_version}") + version = min_version + elif version > max_version: + logger.warning(f"API version {version} for {api_name} exceeds maximum {max_version}, using {max_version}") + version = max_version + + logger.debug(f"Using API path: {api_path}, version: {version}") + else: + logger.warning(f"API {api_name} not found in API info, using defaults") + except Exception as e: + logger.warning(f"Error querying API info for {api_name}: {str(e)}, using defaults") + + # Подготовка базовых параметров запроса + base_params = { + "api": api_name, + "version": str(version), + "method": method, + "sid": self.sid + } + + # Добавление дополнительных параметров, если они заданы + if params: + base_params.update(params) + + url = f"{self.base_url}/{api_path}" + logger.debug(f"API request: {api_name}.{method} v{version} to {url}") + logger.debug(f"Full request params: {base_params}") + + try: + start_time = time.time() + response = self.session.get( + url, + params=base_params, + timeout=self.default_timeout, + verify=False + ) + elapsed_time = time.time() - start_time + logger.debug(f"API request completed in {elapsed_time:.2f}s with status {response.status_code}") + + # Подробное логирование для отладки + logger.debug(f"Response: {response.text[:500]}") + + # Проверка статуса HTTP + if response.status_code != 200: + logger.error(f"HTTP error: {response.status_code} for {api_name}.{method}") + return None + + try: + data = response.json() + except json.JSONDecodeError: + logger.error(f"Failed to decode JSON response for {api_name}.{method}") + logger.debug(f"Response content: {response.text[:200]}") + return None + + if data.get("success"): + logger.info(f"API request successful for {api_name}.{method}") + return data.get("data", {}) + else: + error_code = data.get("error", {}).get("code", -1) + error_desc = self._get_error_description(error_code) + logger.error(f"API error for {api_name}.{method}: {error_code} - {error_desc}") + + # Ошибки доступа или прав часто встречаются, но они не критичные + # Например, ошибка 102 означает, что нет прав, но NAS доступен + if error_code in [102, 103, 104, 105]: + logger.warning(f"Permission-related error for {api_name}.{method}: {error_code}") + # Возвращаем пустой словарь вместо None, + # чтобы вызывающий код мог понять, что запрос выполнен + return {} + + # Если ошибка связана с авторизацией и нам разрешено повторить попытку + if error_code in [106, 107, 119] and retry_auth: + logger.info("Session may have expired, re-authenticating...") + self.sid = None # Сбрасываем SID + if self.login(): + logger.info("Re-authenticated, retrying API request...") + # Рекурсивный вызов, но без повторной авторизации во избежание зацикливания + return self._make_api_request(api_name, method, version, params, False) + + return None + + except requests.exceptions.Timeout: + logger.error(f"Request timeout for {api_name}.{method}") + return None + except requests.exceptions.ConnectionError as e: + logger.error(f"Connection error for {api_name}.{method}: {str(e)}") + return None + except Exception as e: + logger.error(f"Unexpected error during {api_name}.{method}: {str(e)}") + return None + + def get_system_status(self) -> Dict[str, Any]: + """Получение статуса системы""" + # Проверяем доступность системы + if not self.is_online(): + logger.info("Device is offline, skipping API request") + return {"status": "offline"} + + # Проверяем, есть ли кэшированный результат + cache_key = "system_status" + current_time = time.time() + if cache_key in self._cache and current_time - self._cache_ttl.get(cache_key, 0) < 60: + logger.debug("Using cached system status") + return self._cache[cache_key] + + # Пробуем разные API для получения информации о системе + apis_to_try = [ + {"name": "SYNO.DSM.Info", "method": "getinfo", "version": 2}, + {"name": "SYNO.Core.System", "method": "info", "version": 1}, + {"name": "SYNO.Core.System.Status", "method": "get", "version": 1}, + {"name": "SYNO.Core.System.Info", "method": "get", "version": 1}, + ] + + for api in apis_to_try: + logger.info(f"Trying to get system info using {api['name']}.{api['method']} v{api['version']}") + result = self._make_api_request(api["name"], api["method"], version=api["version"]) + + if result: + logger.info(f"Successfully retrieved system info using {api['name']}") + + # Формируем расширенный ответ с дополнительной информацией + system_info = { + "status": "online", + "hostname": result.get("hostname", "unknown"), + "model": result.get("model", "unknown"), + "version": result.get("version", "unknown"), + "uptime": result.get("uptime", 0), + "time": current_time, + "is_online": True, + "api_used": api["name"] + } + + # Сохраняем в кэше + self._cache[cache_key] = system_info + self._cache_ttl[cache_key] = current_time + + return system_info + + # Если все запросы не удались, но система онлайн, возвращаем базовую информацию + logger.warning("Failed to retrieve system info with all API methods") + return { + "status": "error", + "error": "Failed to fetch system information", + "is_online": True, + "time": current_time + } + + def shutdown_system(self) -> bool: + """Выключение системы""" + # Проверяем, включено ли устройство перед попыткой его выключить + if not self.is_online(force_check=True): + logger.info("Synology NAS is already offline, no need to shut down") + return True + + logger.info("Attempting to shutdown Synology NAS...") + + # Проверка всех доступных методов API для выключения + # Проверяем наличие API перед использованием + apis_to_try = [ + {"name": "SYNO.Core.System.Power", "method": "shutdown", "version": 1}, + {"name": "SYNO.DSM.Power", "method": "shutdown", "version": 1}, + {"name": "SYNO.Core.System", "method": "shutdown", "version": 3}, + {"name": "SYNO.System.Power", "method": "shutdown", "version": 1}, + {"name": "SYNO.DSM.System", "method": "shutdown", "version": 1} + ] + + # Получаем список доступных API + try: + api_info_url = f"{self.base_url}/entry.cgi" + api_info_params = { + "api": "SYNO.API.Info", + "version": "1", + "method": "query", + "query": "SYNO.Core.System.Power,SYNO.DSM.Power,SYNO.Core.System,SYNO.System.Power,SYNO.DSM.System" + } + + logger.debug("Checking available shutdown APIs") + api_info_response = self.session.get( + api_info_url, + params=api_info_params, + timeout=self.default_timeout, + verify=False + ) + + if api_info_response.status_code == 200: + api_info_data = api_info_response.json() + if api_info_data.get("success"): + available_apis = api_info_data.get("data", {}) + logger.debug(f"Available APIs: {list(available_apis.keys())}") + + # Фильтруем только доступные API + filtered_apis = [] + for api in apis_to_try: + if api["name"] in available_apis: + api_info = available_apis[api["name"]] + # Проверка версии API + if api_info.get("minVersion", 1) <= api["version"] <= api_info.get("maxVersion", 1): + filtered_apis.append(api) + logger.debug(f"Adding {api['name']} to available shutdown APIs") + else: + logger.debug(f"API {api['name']} version {api['version']} not supported. Supported: {api_info.get('minVersion')}-{api_info.get('maxVersion')}") + + if filtered_apis: + apis_to_try = filtered_apis + else: + logger.warning("No compatible APIs found, trying all methods as fallback") + else: + logger.warning("Failed to query API info, using default methods") + else: + logger.warning(f"API info request failed with status {api_info_response.status_code}, using default methods") + except Exception as e: + logger.warning(f"Error querying API info: {str(e)}, using default methods") + + # Пробуем все доступные методы по порядку + for api in apis_to_try: + logger.info(f"Trying shutdown with {api['name']}.{api['method']} v{api['version']}") + result = self._make_api_request(api["name"], api["method"], version=api["version"]) + + if result is not None: + logger.info(f"Successfully initiated system shutdown using {api['name']}") + + # Даем системе время начать процесс выключения + logger.info("Waiting for shutdown to initialize...") + sleep(5) + + # Проверяем статус + if not self.is_online(force_check=True): + logger.info("System is now offline. Shutdown confirmed successful.") + return True + else: + logger.info("System still appears to be online, but shutdown may be in progress.") + return True + else: + logger.warning(f"Failed to shutdown using {api['name']}.{api['method']} v{api['version']}") + + # Если ни один метод не сработал, но система стала недоступна + if not self.is_online(force_check=True): + logger.info("System appears to be shutting down despite API errors") + return True + + logger.error("Failed to shutdown system after trying multiple APIs") + return False + + def reboot_system(self) -> bool: + """Перезагрузка системы""" + # Проверяем, включена ли система + if not self.is_online(force_check=True): + logger.error("Cannot reboot: System is offline") + return False + + logger.info("Attempting to reboot Synology NAS...") + + # Проверка всех доступных методов API для перезагрузки + apis_to_try = [ + {"name": "SYNO.Core.System.Power", "method": "restart", "version": 1}, + {"name": "SYNO.DSM.Power", "method": "reboot", "version": 1}, + {"name": "SYNO.Core.System", "method": "reboot", "version": 3}, + {"name": "SYNO.System.Power", "method": "reboot", "version": 1}, + {"name": "SYNO.DSM.System", "method": "reboot", "version": 1} + ] + + # Получаем список доступных API + try: + api_info_url = f"{self.base_url}/entry.cgi" + api_info_params = { + "api": "SYNO.API.Info", + "version": "1", + "method": "query", + "query": "SYNO.Core.System.Power,SYNO.DSM.Power,SYNO.Core.System,SYNO.System.Power,SYNO.DSM.System" + } + + logger.debug("Checking available reboot APIs") + api_info_response = self.session.get( + api_info_url, + params=api_info_params, + timeout=self.default_timeout, + verify=False + ) + + if api_info_response.status_code == 200: + api_info_data = api_info_response.json() + if api_info_data.get("success"): + available_apis = api_info_data.get("data", {}) + logger.debug(f"Available APIs: {list(available_apis.keys())}") + + # Фильтруем только доступные API + filtered_apis = [] + for api in apis_to_try: + if api["name"] in available_apis: + api_info = available_apis[api["name"]] + # Проверка версии API + if api_info.get("minVersion", 1) <= api["version"] <= api_info.get("maxVersion", 1): + filtered_apis.append(api) + logger.debug(f"Adding {api['name']} to available reboot APIs") + else: + logger.debug(f"API {api['name']} version {api['version']} not supported. Supported: {api_info.get('minVersion')}-{api_info.get('maxVersion')}") + + if filtered_apis: + apis_to_try = filtered_apis + else: + logger.warning("No compatible APIs found, trying all methods as fallback") + else: + logger.warning("Failed to query API info, using default methods") + else: + logger.warning(f"API info request failed with status {api_info_response.status_code}, using default methods") + except Exception as e: + logger.warning(f"Error querying API info: {str(e)}, using default methods") + + # Пробуем все доступные методы по порядку + for api in apis_to_try: + logger.info(f"Trying reboot with {api['name']}.{api['method']} v{api['version']}") + result = self._make_api_request(api["name"], api["method"], version=api["version"]) + + if result is not None: + logger.info(f"Successfully initiated system reboot using {api['name']}") + + # Даем системе время начать процесс перезагрузки + logger.info("Waiting for reboot to initialize...") + sleep(5) + + # Ждем, пока система станет недоступна (признак перезагрузки) + reboot_started = False + for i in range(12): # Проверяем в течение 60 секунд (12 * 5сек) + if not self.is_online(force_check=True): + logger.info(f"System went offline after {i*5} seconds, reboot in progress") + reboot_started = True + break + logger.debug(f"System still online, waiting... ({i+1}/12)") + sleep(5) + + if reboot_started: + # Ждем, пока система снова станет доступна + logger.info("Waiting for system to come back online...") + return self.wait_for_boot(max_attempts=30, delay=10) + else: + logger.warning("System did not go offline after reboot command, but command was accepted") + # Даже если система не ушла оффлайн, команда могла быть принята + return True + else: + logger.warning(f"Failed to reboot using {api['name']}.{api['method']} v{api['version']}") + + logger.error("Failed to reboot system after trying multiple APIs") + return False + + def _get_error_description(self, error_code: int) -> str: + """Получение описания ошибки по коду""" + error_descriptions = { + 100: "Unknown error", + 101: "Invalid parameter", + 102: "API does not exist", + 103: "Method does not exist", + 104: "Version does not support", + 105: "Permission denied", + 106: "Session timeout", + 107: "Session interrupted by duplicate login", + 400: "Invalid credentials", + 401: "Account disabled", + 402: "Permission denied", + 403: "2FA required", + 404: "Failed to authenticate with 2FA" + } + return error_descriptions.get(error_code, "Unknown error code") + + def _check_tcp_connection(self) -> bool: + """Проверка базового TCP-соединения с Synology NAS""" + try: + socket_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + socket_obj.settimeout(SYNOLOGY_TIMEOUT) + result = socket_obj.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT)) + socket_obj.close() + + return result == 0 + except socket.error as e: + logger.error(f"Socket error during connection check: {str(e)}") + return False + except Exception as e: + logger.error(f"Unexpected error during connection check: {str(e)}") + return False + + def is_online(self, force_check=False) -> bool: + """Проверка онлайн-статуса Synology NAS""" + # Используем кэшированное значение, если доступно и не устарело + current_time = time.time() + if not force_check and (current_time - self._last_online_check) < self._online_check_interval: + logger.debug(f"Using cached online status: {self._last_online_status}") + return self._last_online_status + + logger.info("Checking if NAS is online...") + + # Проверяем TCP-соединение + online_status = self._check_tcp_connection() + logger.info(f"Detected Synology NAS online status: {online_status}") + + # Если TCP-соединение успешно и у нас есть действующий SID, + # попробуем более детальную проверку через API + if online_status and self.sid: + logger.info("Trying to fetch more detailed online status through API...") + + # Пробуем разные API для проверки онлайн-статуса + api_checks = [ + {"api": "SYNO.DSM.Info", "version": "2", "method": "getinfo"}, + {"api": "SYNO.Core.System", "version": "1", "method": "info"}, + {"api": "SYNO.Core.System.Status", "version": "1", "method": "get"} + ] + + api_success = False + for api_check in api_checks: + try: + url = f"{self.base_url}/entry.cgi" + params = { + "api": api_check["api"], + "version": api_check["version"], + "method": api_check["method"], + "sid": self.sid + } + + logger.debug(f"Trying online status check with {api_check['api']}") + response = self.session.get( + url, + params=params, + timeout=self.default_timeout, + verify=False + ) + + if response.status_code == 200: + data = response.json() + if data.get("success"): + logger.info(f"API request successful for {api_check['api']}") + logger.info("Synology NAS is online with API access") + api_success = True + break + else: + error_code = data.get("error", {}).get("code", -1) + logger.warning(f"API response indicates an error: {error_code}, but NAS is reachable") + else: + logger.warning(f"API returned status code {response.status_code}") + except Exception as e: + logger.warning(f"API check failed for {api_check['api']}, but TCP connection succeeded: {str(e)}") + + if not api_success: + logger.warning("All API checks failed, but TCP connection is successful") + + # Обновляем кэшированное значение + self._last_online_check = current_time + self._last_online_status = online_status + + return online_status + + def wake_on_lan(self) -> bool: + """Отправка Wake-on-LAN пакета для включения Synology NAS""" + if not SYNOLOGY_MAC: + logger.error("MAC address not configured") + return False + + try: + # Преобразование MAC-адреса в байты + mac_address = SYNOLOGY_MAC.replace(':', '').replace('-', '') + if len(mac_address) != 12: + logger.error(f"Invalid MAC address format: {SYNOLOGY_MAC}") + return False + + try: + mac_bytes = bytes.fromhex(mac_address) + except ValueError as e: + logger.error(f"Failed to parse MAC address: {str(e)}") + logger.error(f"MAC address must contain only hex characters: {SYNOLOGY_MAC}") + return False + + # Создание Magic Packet + magic_packet = b'\xff' * 6 + mac_bytes * 16 + + # Отправка пакета на конкретный адрес + try: + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) + sock.sendto(magic_packet, (SYNOLOGY_HOST, WOL_PORT)) + sock.close() + logger.info(f"Wake-on-LAN packet sent to {SYNOLOGY_MAC} at {SYNOLOGY_HOST}:{WOL_PORT}") + except Exception as e: + logger.error(f"Error sending directed WoL packet: {str(e)}") + return False + + # Для надежности отправляем также широковещательный пакет + try: + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) + + # Используем стандартный широковещательный адрес + broadcast_addr = "255.255.255.255" + sock.sendto(magic_packet, (broadcast_addr, WOL_PORT)) + sock.close() + logger.info(f"Wake-on-LAN broadcast packet sent to {broadcast_addr}:{WOL_PORT}") + except Exception as e: + logger.warning(f"Error sending broadcast WoL packet (non-critical): {str(e)}") + # Не считаем ошибкой, т.к. основной пакет уже отправлен + + return True + + except Exception as e: + logger.error(f"Unexpected error in wake_on_lan: {str(e)}") + return False + + def wait_for_boot(self, max_attempts: int = 30, delay: int = 10) -> bool: + """Ожидание загрузки Synology NAS""" + logger.info(f"Waiting for Synology NAS to boot (timeout: {max_attempts * delay}s)...") + + for attempt in range(max_attempts): + # Принудительно проверяем статус без использования кэша + if self.is_online(force_check=True): + logger.info(f"Synology NAS is online after {attempt + 1} attempts ({(attempt + 1) * delay}s)") + + # Проверяем, что не только сеть доступна, но и API загрузился + api_ready = False + logger.info("Waiting for API services to initialize...") + + for api_check in range(5): # Даем еще до 50 секунд для загрузки API + if self.sid or self.login(): + api_ready = True + logger.info(f"API services are ready after {api_check + 1} attempts") + break + logger.debug(f"API not ready yet, waiting... ({api_check + 1}/5)") + sleep(10) + + if not api_ready: + logger.warning("System is online but API services may not be fully initialized") + + # Дадим дополнительное время для полной загрузки всех сервисов + sleep(delay) + return True + + sleep(delay) + logger.info(f"Waiting for boot... Attempt {attempt + 1}/{max_attempts}") + + logger.error(f"Synology NAS didn't come online after {max_attempts * delay} seconds") + return False + + def power_on(self) -> bool: + """Включение Synology NAS""" + # Принудительная проверка статуса + if self.is_online(force_check=True): + logger.info("Synology NAS is already online") + return True + + logger.info("Powering on Synology NAS via Wake-on-LAN...") + + # Проверяем, настроен ли MAC-адрес + if not SYNOLOGY_MAC: + logger.error("Cannot power on: MAC address not configured in settings") + return False + + # Пробуем отправить несколько WoL пакетов для надежности + success = False + for attempt in range(3): + logger.debug(f"Sending WoL packet, attempt {attempt + 1}/3") + if self.wake_on_lan(): + success = True + break + sleep(1) + + if not success: + logger.error("Failed to send Wake-on-LAN packets") + return False + + # Ожидание загрузки + logger.info("WoL packets sent successfully, waiting for system to boot...") + boot_result = self.wait_for_boot(max_attempts=30, delay=10) + + if boot_result: + # Проверяем доступность API после загрузки + system_status = self.get_system_status() + if system_status.get("status") == "online": + logger.info("System booted successfully with API access") + return True + else: + logger.warning("System appears to be online but API may not be fully ready") + return True + else: + logger.error("System did not come online after WoL") + return False + + def power_off(self) -> bool: + """Выключение Synology NAS""" + if not self.is_online(force_check=True): + logger.info("Synology NAS is already offline") + return True + + logger.info("Powering off Synology NAS...") + result = self.shutdown_system() + + if result: + # Дополнительная проверка, что система действительно выключилась + logger.info("Verifying system is offline...") + for attempt in range(12): # Проверяем в течение 2 минут (12 * 10 сек) + if not self.is_online(force_check=True): + logger.info(f"System confirmed offline after {attempt * 10} seconds") + return True + logger.debug(f"System still shutting down, checking again... ({attempt + 1}/12)") + sleep(10) + + logger.warning("System still appears to be online after shutdown command") + return False + else: + logger.error("Failed to initiate shutdown") + return False + + # Заглушки для расширенных методов + def get_shared_folders(self) -> List[Dict[str, Any]]: + """Получение списка общих папок (заглушка)""" + logger.warning("Function get_shared_folders() is not implemented yet") + return [] + + def get_system_load(self) -> Dict[str, Any]: + """Получение информации о загрузке системы (заглушка)""" + logger.warning("Function get_system_load() is not implemented yet") + return {} + + def get_storage_status(self) -> Dict[str, Any]: + """Получение подробной информации о хранилище (заглушка)""" + logger.warning("Function get_storage_status() is not implemented yet") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for storage status request") + return {"error": "authentication_failed"} + + try: + # Здесь будет реализация после добавления соответствующего API + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.Storage.CGI.Storage", + "version": "1", + "method": "load_info", + "sid": self.sid + } + + # В текущей версии просто возвращаем заглушку + return { + "volumes": [], + "disks": [], + "total_size": 0, + "total_used": 0, + "error": "not_implemented" + } + except Exception as e: + logger.error(f"Error in get_storage_status: {str(e)}") + return {"error": str(e)} + + def get_security_status(self) -> Dict[str, Any]: + """Получение информации о состоянии безопасности (заглушка)""" + logger.warning("Function get_security_status() is not implemented yet") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for security status request") + return {"success": False, "error": "authentication_failed"} + + try: + # Здесь будет реализация после добавления соответствующего API + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.Core.SecurityScan.Status", + "version": "1", + "method": "get", + "sid": self.sid + } + + # В текущей версии просто возвращаем заглушку + return { + "success": False, + "status": "not_implemented", + "last_check": None, + "is_secure": False, + "error": "not_implemented" + } + except Exception as e: + logger.error(f"Error in get_security_status: {str(e)}") + return {"success": False, "error": str(e)} diff --git a/.history/src/api/synology_20250830081957.py b/.history/src/api/synology_20250830081957.py new file mode 100644 index 0000000..4d9457d --- /dev/null +++ b/.history/src/api/synology_20250830081957.py @@ -0,0 +1,977 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Модуль для взаимодействия с API Synology NAS +""" + +import requests +from requests.adapters import HTTPAdapter +import json +import logging +import time +import urllib3 +from urllib3.util import Retry +from typing import Dict, Any, Optional, List +import socket +import struct +from time import sleep + +from src.config.config import ( + SYNOLOGY_HOST, + SYNOLOGY_PORT, + SYNOLOGY_USERNAME, + SYNOLOGY_PASSWORD, + SYNOLOGY_SECURE, + SYNOLOGY_TIMEOUT, + SYNOLOGY_MAC, + WOL_PORT +) +from src.api.api_discovery import discover_available_apis, find_compatible_api + +# Отключение предупреждений о небезопасных SSL-соединениях +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + +logger = logging.getLogger(__name__) + +class SynologyAPI: + """Класс для взаимодействия с API Synology NAS""" + + def __init__(self): + """Инициализация класса SynologyAPI""" + logger.info("Creating API with auto-retry and connection pool") + logger.debug(f"Connection details: Host={SYNOLOGY_HOST}, Port={SYNOLOGY_PORT}, Secure={SYNOLOGY_SECURE}") + + self.protocol = "https" if SYNOLOGY_SECURE else "http" + self.base_url = f"{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi" + self.sid = None + self.session = requests.Session() + + # Настройка SSL + if self.protocol == "https": + logger.debug("SSL enabled, disabling certificate verification for internal network") + self.session.verify = False # Отключаем проверку SSL для внутренней сети + + # Добавляем повторные попытки для HTTP-запросов + retry_strategy = Retry( + total=5, # Увеличиваем количество попыток + status_forcelist=[429, 500, 502, 503, 504, 404], + allowed_methods=["GET", "POST"], + backoff_factor=1.5, # Увеличиваем задержку между попытками + respect_retry_after_header=True + ) + adapter = HTTPAdapter( + max_retries=retry_strategy, + pool_connections=3, + pool_maxsize=10 + ) + self.session.mount("http://", adapter) + self.session.mount("https://", adapter) + + # Таймауты будут указаны в запросах + self.default_timeout = (SYNOLOGY_TIMEOUT, SYNOLOGY_TIMEOUT*2) + logger.debug(f"Setting default request timeout: {self.default_timeout}") + + # Кэш для хранения результатов запросов + self._cache = {} + self._cache_ttl = {} + self._last_online_check = 0 + self._last_online_status = False + self._online_check_interval = 30 # Интервал для кэширования проверок онлайн-статуса + + # Время последней успешной аутентификации и срок действия сессии + self._last_auth_time = 0 + self._auth_expiry = 3600 # По умолчанию 1 час + + # Информация о доступных API + self._available_apis = {} + self._api_info_ttl = 0 + + def login(self) -> bool: + """Авторизация в API Synology NAS""" + # Проверяем, действительна ли текущая сессия + current_time = time.time() + if self.sid and (current_time - self._last_auth_time) < self._auth_expiry: + logger.debug("Using existing session, still valid") + return True + + logger.info("Attempting to authenticate with Synology NAS...") + logger.debug(f"Base URL: {self.base_url}") + + # Проверяем доступность NAS перед запросом с помощью прямой TCP-проверки + # Избегаем вызова is_online(), чтобы не создавать рекурсию + online_status = self._check_tcp_connection() + if not online_status: + logger.error("Cannot login: Synology NAS is not reachable") + return False + + try: + # Проверка информации API для определения доступных версий API + api_info_url = f"{self.base_url}/entry.cgi" + api_info_params = { + "api": "SYNO.API.Info", + "version": "1", + "method": "query", + "query": "SYNO.API.Auth" + } + + logger.debug("Querying API info to determine optimal auth version") + try: + api_info_response = self.session.get( + api_info_url, + params=api_info_params, + timeout=self.default_timeout, + verify=False + ) + + if api_info_response.status_code == 200: + api_info_data = api_info_response.json() + if api_info_data.get("success"): + auth_info = api_info_data.get("data", {}).get("SYNO.API.Auth", {}) + max_version = auth_info.get("maxVersion", 3) + min_version = auth_info.get("minVersion", 1) + auth_path = auth_info.get("path", "auth.cgi") + logger.debug(f"Auth API versions: min={min_version}, max={max_version}, path={auth_path}") + + # Используем максимально поддерживаемую версию, но не выше 6 + auth_version = min(max_version, 6) + else: + logger.warning("Failed to query API info, using default auth version 3") + auth_version = 3 + auth_path = "auth.cgi" + else: + logger.warning(f"API info request failed with status {api_info_response.status_code}, using default auth version 3") + auth_version = 3 + auth_path = "auth.cgi" + except Exception as e: + logger.warning(f"Error querying API info: {str(e)}, using default auth version 3") + auth_version = 3 + auth_path = "auth.cgi" + + # Основной запрос авторизации + url = f"{self.base_url}/{auth_path}" + params = { + "api": "SYNO.API.Auth", + "version": str(auth_version), + "method": "login", + "account": SYNOLOGY_USERNAME, + "passwd": SYNOLOGY_PASSWORD, + "session": "SynologyPowerControlBot", + "format": "cookie" + } + + # Для версии 6+ используем немного другой формат + if auth_version >= 6: + params["enable_syno_token"] = "yes" + + logger.debug(f"Sending auth request to {url} with API version {auth_version}") + start_time = time.time() + response = self.session.get( + url, + params=params, + timeout=self.default_timeout, + verify=False + ) + elapsed_time = time.time() - start_time + logger.debug(f"Auth request completed in {elapsed_time:.2f}s with status code {response.status_code}") + + # Подробная информация для отладки + logger.debug(f"Auth response: {response.text[:500]}") + + # Проверка статуса HTTP + if response.status_code != 200: + logger.error(f"HTTP error: {response.status_code}") + logger.debug(f"Response content: {response.text[:200]}") + return False + + try: + data = response.json() + except json.JSONDecodeError: + logger.error("Failed to decode JSON response") + logger.debug(f"Response content: {response.text[:200]}") + return False + + if data.get("success"): + self.sid = data.get("data", {}).get("sid") + self._last_auth_time = current_time + logger.info("Successfully logged in to Synology NAS") + logger.debug(f"Session ID obtained: {self.sid[:5] if self.sid else 'None'}...") # Показываем только начало SID для безопасности + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to log in to Synology NAS: Error code {error_code}") + + # Дополнительная диагностика + if error_code == 400: + logger.error("Authentication error: Invalid credentials") + elif error_code == 401: + logger.error("Authentication error: Account disabled") + elif error_code == 402: + logger.error("Authentication error: Permission denied") + elif error_code == 403: + logger.error("Authentication error: 2-factor authentication required") + elif error_code == 404: + logger.error("Authentication error: Failed to authenticate with 2-factor authentication") + + return False + + except requests.exceptions.Timeout: + logger.error(f"Connection timeout after {SYNOLOGY_TIMEOUT} seconds") + return False + except requests.exceptions.ConnectionError as e: + logger.error(f"Connection error: {str(e)}") + logger.debug(f"Connection details: {self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}") + return False + except requests.RequestException as e: + logger.error(f"Request error: {str(e)}") + return False + except Exception as e: + logger.error(f"Unexpected error during login: {str(e)}", exc_info=True) + return False + + def logout(self) -> bool: + """Выход из API Synology NAS""" + if not self.sid: + return True + + try: + url = f"{self.base_url}/auth.cgi" + params = { + "api": "SYNO.API.Auth", + "version": "1", + "method": "logout", + "session": "SynologyPowerControlBot", + "_sid": self.sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + self.sid = None + logger.info("Successfully logged out from Synology NAS") + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to log out from Synology NAS: Error code {error_code}") + return False + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + return False + + def _make_api_request(self, api_name: str, method: str, version: int = 1, + params: Optional[Dict[str, Any]] = None, retry_auth: bool = True) -> Optional[Dict[str, Any]]: + """Обобщенный метод для выполнения API запросов с обработкой ошибок""" + # Проверка наличия авторизации + if not self.sid and not self.login(): + logger.error(f"Not authenticated for API request: {api_name}.{method}") + return None + + # Проверка информации API для определения пути и поддерживаемой версии + api_info_url = f"{self.base_url}/entry.cgi" + api_info_params = { + "api": "SYNO.API.Info", + "version": "1", + "method": "query", + "query": api_name + } + + api_path = "entry.cgi" + try: + logger.debug(f"Querying API info for {api_name}") + api_info_response = self.session.get( + api_info_url, + params=api_info_params, + timeout=self.default_timeout, + verify=False + ) + + if api_info_response.status_code == 200: + api_info_data = api_info_response.json() + if api_info_data.get("success"): + api_info = api_info_data.get("data", {}).get(api_name, {}) + if api_info: + max_version = api_info.get("maxVersion", version) + min_version = api_info.get("minVersion", version) + api_path = api_info.get("path", "entry.cgi") + + # Проверка, поддерживается ли запрошенная версия + if version < min_version: + logger.warning(f"API version {version} for {api_name} is below minimum {min_version}, using {min_version}") + version = min_version + elif version > max_version: + logger.warning(f"API version {version} for {api_name} exceeds maximum {max_version}, using {max_version}") + version = max_version + + logger.debug(f"Using API path: {api_path}, version: {version}") + else: + logger.warning(f"API {api_name} not found in API info, using defaults") + except Exception as e: + logger.warning(f"Error querying API info for {api_name}: {str(e)}, using defaults") + + # Подготовка базовых параметров запроса + base_params = { + "api": api_name, + "version": str(version), + "method": method, + "sid": self.sid + } + + # Добавление дополнительных параметров, если они заданы + if params: + base_params.update(params) + + url = f"{self.base_url}/{api_path}" + logger.debug(f"API request: {api_name}.{method} v{version} to {url}") + logger.debug(f"Full request params: {base_params}") + + try: + start_time = time.time() + response = self.session.get( + url, + params=base_params, + timeout=self.default_timeout, + verify=False + ) + elapsed_time = time.time() - start_time + logger.debug(f"API request completed in {elapsed_time:.2f}s with status {response.status_code}") + + # Подробное логирование для отладки + logger.debug(f"Response: {response.text[:500]}") + + # Проверка статуса HTTP + if response.status_code != 200: + logger.error(f"HTTP error: {response.status_code} for {api_name}.{method}") + return None + + try: + data = response.json() + except json.JSONDecodeError: + logger.error(f"Failed to decode JSON response for {api_name}.{method}") + logger.debug(f"Response content: {response.text[:200]}") + return None + + if data.get("success"): + logger.info(f"API request successful for {api_name}.{method}") + return data.get("data", {}) + else: + error_code = data.get("error", {}).get("code", -1) + error_desc = self._get_error_description(error_code) + logger.error(f"API error for {api_name}.{method}: {error_code} - {error_desc}") + + # Ошибки доступа или прав часто встречаются, но они не критичные + # Например, ошибка 102 означает, что нет прав, но NAS доступен + if error_code in [102, 103, 104, 105]: + logger.warning(f"Permission-related error for {api_name}.{method}: {error_code}") + # Возвращаем пустой словарь вместо None, + # чтобы вызывающий код мог понять, что запрос выполнен + return {} + + # Если ошибка связана с авторизацией и нам разрешено повторить попытку + if error_code in [106, 107, 119] and retry_auth: + logger.info("Session may have expired, re-authenticating...") + self.sid = None # Сбрасываем SID + if self.login(): + logger.info("Re-authenticated, retrying API request...") + # Рекурсивный вызов, но без повторной авторизации во избежание зацикливания + return self._make_api_request(api_name, method, version, params, False) + + return None + + except requests.exceptions.Timeout: + logger.error(f"Request timeout for {api_name}.{method}") + return None + except requests.exceptions.ConnectionError as e: + logger.error(f"Connection error for {api_name}.{method}: {str(e)}") + return None + except Exception as e: + logger.error(f"Unexpected error during {api_name}.{method}: {str(e)}") + return None + + def get_system_status(self) -> Dict[str, Any]: + """Получение статуса системы""" + # Проверяем доступность системы + if not self.is_online(): + logger.info("Device is offline, skipping API request") + return {"status": "offline"} + + # Проверяем, есть ли кэшированный результат + cache_key = "system_status" + current_time = time.time() + if cache_key in self._cache and current_time - self._cache_ttl.get(cache_key, 0) < 60: + logger.debug("Using cached system status") + return self._cache[cache_key] + + # Пробуем разные API для получения информации о системе + apis_to_try = [ + {"name": "SYNO.DSM.Info", "method": "getinfo", "version": 2}, + {"name": "SYNO.Core.System", "method": "info", "version": 1}, + {"name": "SYNO.Core.System.Status", "method": "get", "version": 1}, + {"name": "SYNO.Core.System.Info", "method": "get", "version": 1}, + ] + + for api in apis_to_try: + logger.info(f"Trying to get system info using {api['name']}.{api['method']} v{api['version']}") + result = self._make_api_request(api["name"], api["method"], version=api["version"]) + + if result: + logger.info(f"Successfully retrieved system info using {api['name']}") + + # Формируем расширенный ответ с дополнительной информацией + system_info = { + "status": "online", + "hostname": result.get("hostname", "unknown"), + "model": result.get("model", "unknown"), + "version": result.get("version", "unknown"), + "uptime": result.get("uptime", 0), + "time": current_time, + "is_online": True, + "api_used": api["name"] + } + + # Сохраняем в кэше + self._cache[cache_key] = system_info + self._cache_ttl[cache_key] = current_time + + return system_info + + # Если все запросы не удались, но система онлайн, возвращаем базовую информацию + logger.warning("Failed to retrieve system info with all API methods") + return { + "status": "error", + "error": "Failed to fetch system information", + "is_online": True, + "time": current_time + } + + def shutdown_system(self) -> bool: + """Выключение системы""" + # Проверяем, включено ли устройство перед попыткой его выключить + if not self.is_online(force_check=True): + logger.info("Synology NAS is already offline, no need to shut down") + return True + + logger.info("Attempting to shutdown Synology NAS...") + + # Проверка всех доступных методов API для выключения + # Проверяем наличие API перед использованием + apis_to_try = [ + {"name": "SYNO.Core.System.Power", "method": "shutdown", "version": 1}, + {"name": "SYNO.DSM.Power", "method": "shutdown", "version": 1}, + {"name": "SYNO.Core.System", "method": "shutdown", "version": 3}, + {"name": "SYNO.System.Power", "method": "shutdown", "version": 1}, + {"name": "SYNO.DSM.System", "method": "shutdown", "version": 1} + ] + + # Получаем список доступных API + try: + api_info_url = f"{self.base_url}/entry.cgi" + api_info_params = { + "api": "SYNO.API.Info", + "version": "1", + "method": "query", + "query": "SYNO.Core.System.Power,SYNO.DSM.Power,SYNO.Core.System,SYNO.System.Power,SYNO.DSM.System" + } + + logger.debug("Checking available shutdown APIs") + api_info_response = self.session.get( + api_info_url, + params=api_info_params, + timeout=self.default_timeout, + verify=False + ) + + if api_info_response.status_code == 200: + api_info_data = api_info_response.json() + if api_info_data.get("success"): + available_apis = api_info_data.get("data", {}) + logger.debug(f"Available APIs: {list(available_apis.keys())}") + + # Фильтруем только доступные API + filtered_apis = [] + for api in apis_to_try: + if api["name"] in available_apis: + api_info = available_apis[api["name"]] + # Проверка версии API + if api_info.get("minVersion", 1) <= api["version"] <= api_info.get("maxVersion", 1): + filtered_apis.append(api) + logger.debug(f"Adding {api['name']} to available shutdown APIs") + else: + logger.debug(f"API {api['name']} version {api['version']} not supported. Supported: {api_info.get('minVersion')}-{api_info.get('maxVersion')}") + + if filtered_apis: + apis_to_try = filtered_apis + else: + logger.warning("No compatible APIs found, trying all methods as fallback") + else: + logger.warning("Failed to query API info, using default methods") + else: + logger.warning(f"API info request failed with status {api_info_response.status_code}, using default methods") + except Exception as e: + logger.warning(f"Error querying API info: {str(e)}, using default methods") + + # Пробуем все доступные методы по порядку + for api in apis_to_try: + logger.info(f"Trying shutdown with {api['name']}.{api['method']} v{api['version']}") + result = self._make_api_request(api["name"], api["method"], version=api["version"]) + + if result is not None: + logger.info(f"Successfully initiated system shutdown using {api['name']}") + + # Даем системе время начать процесс выключения + logger.info("Waiting for shutdown to initialize...") + sleep(5) + + # Проверяем статус + if not self.is_online(force_check=True): + logger.info("System is now offline. Shutdown confirmed successful.") + return True + else: + logger.info("System still appears to be online, but shutdown may be in progress.") + return True + else: + logger.warning(f"Failed to shutdown using {api['name']}.{api['method']} v{api['version']}") + + # Если ни один метод не сработал, но система стала недоступна + if not self.is_online(force_check=True): + logger.info("System appears to be shutting down despite API errors") + return True + + logger.error("Failed to shutdown system after trying multiple APIs") + return False + + def reboot_system(self) -> bool: + """Перезагрузка системы""" + # Проверяем, включена ли система + if not self.is_online(force_check=True): + logger.error("Cannot reboot: System is offline") + return False + + logger.info("Attempting to reboot Synology NAS...") + + # Проверка всех доступных методов API для перезагрузки + apis_to_try = [ + {"name": "SYNO.Core.System.Power", "method": "restart", "version": 1}, + {"name": "SYNO.DSM.Power", "method": "reboot", "version": 1}, + {"name": "SYNO.Core.System", "method": "reboot", "version": 3}, + {"name": "SYNO.System.Power", "method": "reboot", "version": 1}, + {"name": "SYNO.DSM.System", "method": "reboot", "version": 1} + ] + + # Получаем список доступных API + try: + api_info_url = f"{self.base_url}/entry.cgi" + api_info_params = { + "api": "SYNO.API.Info", + "version": "1", + "method": "query", + "query": "SYNO.Core.System.Power,SYNO.DSM.Power,SYNO.Core.System,SYNO.System.Power,SYNO.DSM.System" + } + + logger.debug("Checking available reboot APIs") + api_info_response = self.session.get( + api_info_url, + params=api_info_params, + timeout=self.default_timeout, + verify=False + ) + + if api_info_response.status_code == 200: + api_info_data = api_info_response.json() + if api_info_data.get("success"): + available_apis = api_info_data.get("data", {}) + logger.debug(f"Available APIs: {list(available_apis.keys())}") + + # Фильтруем только доступные API + filtered_apis = [] + for api in apis_to_try: + if api["name"] in available_apis: + api_info = available_apis[api["name"]] + # Проверка версии API + if api_info.get("minVersion", 1) <= api["version"] <= api_info.get("maxVersion", 1): + filtered_apis.append(api) + logger.debug(f"Adding {api['name']} to available reboot APIs") + else: + logger.debug(f"API {api['name']} version {api['version']} not supported. Supported: {api_info.get('minVersion')}-{api_info.get('maxVersion')}") + + if filtered_apis: + apis_to_try = filtered_apis + else: + logger.warning("No compatible APIs found, trying all methods as fallback") + else: + logger.warning("Failed to query API info, using default methods") + else: + logger.warning(f"API info request failed with status {api_info_response.status_code}, using default methods") + except Exception as e: + logger.warning(f"Error querying API info: {str(e)}, using default methods") + + # Пробуем все доступные методы по порядку + for api in apis_to_try: + logger.info(f"Trying reboot with {api['name']}.{api['method']} v{api['version']}") + result = self._make_api_request(api["name"], api["method"], version=api["version"]) + + if result is not None: + logger.info(f"Successfully initiated system reboot using {api['name']}") + + # Даем системе время начать процесс перезагрузки + logger.info("Waiting for reboot to initialize...") + sleep(5) + + # Ждем, пока система станет недоступна (признак перезагрузки) + reboot_started = False + for i in range(12): # Проверяем в течение 60 секунд (12 * 5сек) + if not self.is_online(force_check=True): + logger.info(f"System went offline after {i*5} seconds, reboot in progress") + reboot_started = True + break + logger.debug(f"System still online, waiting... ({i+1}/12)") + sleep(5) + + if reboot_started: + # Ждем, пока система снова станет доступна + logger.info("Waiting for system to come back online...") + return self.wait_for_boot(max_attempts=30, delay=10) + else: + logger.warning("System did not go offline after reboot command, but command was accepted") + # Даже если система не ушла оффлайн, команда могла быть принята + return True + else: + logger.warning(f"Failed to reboot using {api['name']}.{api['method']} v{api['version']}") + + logger.error("Failed to reboot system after trying multiple APIs") + return False + + def _get_error_description(self, error_code: int) -> str: + """Получение описания ошибки по коду""" + error_descriptions = { + 100: "Unknown error", + 101: "Invalid parameter", + 102: "API does not exist", + 103: "Method does not exist", + 104: "Version does not support", + 105: "Permission denied", + 106: "Session timeout", + 107: "Session interrupted by duplicate login", + 400: "Invalid credentials", + 401: "Account disabled", + 402: "Permission denied", + 403: "2FA required", + 404: "Failed to authenticate with 2FA" + } + return error_descriptions.get(error_code, "Unknown error code") + + def _check_tcp_connection(self) -> bool: + """Проверка базового TCP-соединения с Synology NAS""" + try: + socket_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + socket_obj.settimeout(SYNOLOGY_TIMEOUT) + result = socket_obj.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT)) + socket_obj.close() + + return result == 0 + except socket.error as e: + logger.error(f"Socket error during connection check: {str(e)}") + return False + except Exception as e: + logger.error(f"Unexpected error during connection check: {str(e)}") + return False + + def is_online(self, force_check=False) -> bool: + """Проверка онлайн-статуса Synology NAS""" + # Используем кэшированное значение, если доступно и не устарело + current_time = time.time() + if not force_check and (current_time - self._last_online_check) < self._online_check_interval: + logger.debug(f"Using cached online status: {self._last_online_status}") + return self._last_online_status + + logger.info("Checking if NAS is online...") + + # Проверяем TCP-соединение + online_status = self._check_tcp_connection() + logger.info(f"Detected Synology NAS online status: {online_status}") + + # Если TCP-соединение успешно и у нас есть действующий SID, + # попробуем более детальную проверку через API + if online_status and self.sid: + logger.info("Trying to fetch more detailed online status through API...") + + # Пробуем разные API для проверки онлайн-статуса + api_checks = [ + {"api": "SYNO.DSM.Info", "version": "2", "method": "getinfo"}, + {"api": "SYNO.Core.System", "version": "1", "method": "info"}, + {"api": "SYNO.Core.System.Status", "version": "1", "method": "get"} + ] + + api_success = False + for api_check in api_checks: + try: + url = f"{self.base_url}/entry.cgi" + params = { + "api": api_check["api"], + "version": api_check["version"], + "method": api_check["method"], + "sid": self.sid + } + + logger.debug(f"Trying online status check with {api_check['api']}") + response = self.session.get( + url, + params=params, + timeout=self.default_timeout, + verify=False + ) + + if response.status_code == 200: + data = response.json() + if data.get("success"): + logger.info(f"API request successful for {api_check['api']}") + logger.info("Synology NAS is online with API access") + api_success = True + break + else: + error_code = data.get("error", {}).get("code", -1) + logger.warning(f"API response indicates an error: {error_code}, but NAS is reachable") + else: + logger.warning(f"API returned status code {response.status_code}") + except Exception as e: + logger.warning(f"API check failed for {api_check['api']}, but TCP connection succeeded: {str(e)}") + + if not api_success: + logger.warning("All API checks failed, but TCP connection is successful") + + # Обновляем кэшированное значение + self._last_online_check = current_time + self._last_online_status = online_status + + return online_status + + def wake_on_lan(self) -> bool: + """Отправка Wake-on-LAN пакета для включения Synology NAS""" + if not SYNOLOGY_MAC: + logger.error("MAC address not configured") + return False + + try: + # Преобразование MAC-адреса в байты + mac_address = SYNOLOGY_MAC.replace(':', '').replace('-', '') + if len(mac_address) != 12: + logger.error(f"Invalid MAC address format: {SYNOLOGY_MAC}") + return False + + try: + mac_bytes = bytes.fromhex(mac_address) + except ValueError as e: + logger.error(f"Failed to parse MAC address: {str(e)}") + logger.error(f"MAC address must contain only hex characters: {SYNOLOGY_MAC}") + return False + + # Создание Magic Packet + magic_packet = b'\xff' * 6 + mac_bytes * 16 + + # Отправка пакета на конкретный адрес + try: + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) + sock.sendto(magic_packet, (SYNOLOGY_HOST, WOL_PORT)) + sock.close() + logger.info(f"Wake-on-LAN packet sent to {SYNOLOGY_MAC} at {SYNOLOGY_HOST}:{WOL_PORT}") + except Exception as e: + logger.error(f"Error sending directed WoL packet: {str(e)}") + return False + + # Для надежности отправляем также широковещательный пакет + try: + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) + + # Используем стандартный широковещательный адрес + broadcast_addr = "255.255.255.255" + sock.sendto(magic_packet, (broadcast_addr, WOL_PORT)) + sock.close() + logger.info(f"Wake-on-LAN broadcast packet sent to {broadcast_addr}:{WOL_PORT}") + except Exception as e: + logger.warning(f"Error sending broadcast WoL packet (non-critical): {str(e)}") + # Не считаем ошибкой, т.к. основной пакет уже отправлен + + return True + + except Exception as e: + logger.error(f"Unexpected error in wake_on_lan: {str(e)}") + return False + + def wait_for_boot(self, max_attempts: int = 30, delay: int = 10) -> bool: + """Ожидание загрузки Synology NAS""" + logger.info(f"Waiting for Synology NAS to boot (timeout: {max_attempts * delay}s)...") + + for attempt in range(max_attempts): + # Принудительно проверяем статус без использования кэша + if self.is_online(force_check=True): + logger.info(f"Synology NAS is online after {attempt + 1} attempts ({(attempt + 1) * delay}s)") + + # Проверяем, что не только сеть доступна, но и API загрузился + api_ready = False + logger.info("Waiting for API services to initialize...") + + for api_check in range(5): # Даем еще до 50 секунд для загрузки API + if self.sid or self.login(): + api_ready = True + logger.info(f"API services are ready after {api_check + 1} attempts") + break + logger.debug(f"API not ready yet, waiting... ({api_check + 1}/5)") + sleep(10) + + if not api_ready: + logger.warning("System is online but API services may not be fully initialized") + + # Дадим дополнительное время для полной загрузки всех сервисов + sleep(delay) + return True + + sleep(delay) + logger.info(f"Waiting for boot... Attempt {attempt + 1}/{max_attempts}") + + logger.error(f"Synology NAS didn't come online after {max_attempts * delay} seconds") + return False + + def power_on(self) -> bool: + """Включение Synology NAS""" + # Принудительная проверка статуса + if self.is_online(force_check=True): + logger.info("Synology NAS is already online") + return True + + logger.info("Powering on Synology NAS via Wake-on-LAN...") + + # Проверяем, настроен ли MAC-адрес + if not SYNOLOGY_MAC: + logger.error("Cannot power on: MAC address not configured in settings") + return False + + # Пробуем отправить несколько WoL пакетов для надежности + success = False + for attempt in range(3): + logger.debug(f"Sending WoL packet, attempt {attempt + 1}/3") + if self.wake_on_lan(): + success = True + break + sleep(1) + + if not success: + logger.error("Failed to send Wake-on-LAN packets") + return False + + # Ожидание загрузки + logger.info("WoL packets sent successfully, waiting for system to boot...") + boot_result = self.wait_for_boot(max_attempts=30, delay=10) + + if boot_result: + # Проверяем доступность API после загрузки + system_status = self.get_system_status() + if system_status.get("status") == "online": + logger.info("System booted successfully with API access") + return True + else: + logger.warning("System appears to be online but API may not be fully ready") + return True + else: + logger.error("System did not come online after WoL") + return False + + def power_off(self) -> bool: + """Выключение Synology NAS""" + if not self.is_online(force_check=True): + logger.info("Synology NAS is already offline") + return True + + logger.info("Powering off Synology NAS...") + result = self.shutdown_system() + + if result: + # Дополнительная проверка, что система действительно выключилась + logger.info("Verifying system is offline...") + for attempt in range(12): # Проверяем в течение 2 минут (12 * 10 сек) + if not self.is_online(force_check=True): + logger.info(f"System confirmed offline after {attempt * 10} seconds") + return True + logger.debug(f"System still shutting down, checking again... ({attempt + 1}/12)") + sleep(10) + + logger.warning("System still appears to be online after shutdown command") + return False + else: + logger.error("Failed to initiate shutdown") + return False + + # Заглушки для расширенных методов + def get_shared_folders(self) -> List[Dict[str, Any]]: + """Получение списка общих папок (заглушка)""" + logger.warning("Function get_shared_folders() is not implemented yet") + return [] + + def get_system_load(self) -> Dict[str, Any]: + """Получение информации о загрузке системы (заглушка)""" + logger.warning("Function get_system_load() is not implemented yet") + return {} + + def get_storage_status(self) -> Dict[str, Any]: + """Получение подробной информации о хранилище (заглушка)""" + logger.warning("Function get_storage_status() is not implemented yet") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for storage status request") + return {"error": "authentication_failed"} + + try: + # Здесь будет реализация после добавления соответствующего API + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.Storage.CGI.Storage", + "version": "1", + "method": "load_info", + "sid": self.sid + } + + # В текущей версии просто возвращаем заглушку + return { + "volumes": [], + "disks": [], + "total_size": 0, + "total_used": 0, + "error": "not_implemented" + } + except Exception as e: + logger.error(f"Error in get_storage_status: {str(e)}") + return {"error": str(e)} + + def get_security_status(self) -> Dict[str, Any]: + """Получение информации о состоянии безопасности (заглушка)""" + logger.warning("Function get_security_status() is not implemented yet") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for security status request") + return {"success": False, "error": "authentication_failed"} + + try: + # Здесь будет реализация после добавления соответствующего API + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.Core.SecurityScan.Status", + "version": "1", + "method": "get", + "sid": self.sid + } + + # В текущей версии просто возвращаем заглушку + return { + "success": False, + "status": "not_implemented", + "last_check": None, + "is_secure": False, + "error": "not_implemented" + } + except Exception as e: + logger.error(f"Error in get_security_status: {str(e)}") + return {"success": False, "error": str(e)} diff --git a/.history/src/api/synology_20250830082235.py b/.history/src/api/synology_20250830082235.py new file mode 100644 index 0000000..d75371d --- /dev/null +++ b/.history/src/api/synology_20250830082235.py @@ -0,0 +1,980 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Модуль для взаимодействия с API Synology NAS +""" + +import requests +from requests.adapters import HTTPAdapter +import json +import logging +import time +import urllib3 +from urllib3.util import Retry +from typing import Dict, Any, Optional, List +import socket +import struct +from time import sleep + +from src.config.config import ( + SYNOLOGY_HOST, + SYNOLOGY_PORT, + SYNOLOGY_USERNAME, + SYNOLOGY_PASSWORD, + SYNOLOGY_SECURE, + SYNOLOGY_TIMEOUT, + SYNOLOGY_MAC, + WOL_PORT, + SYNOLOGY_API_VERSION, + SYNOLOGY_POWER_API, + SYNOLOGY_INFO_API +) +from src.api.api_discovery import discover_available_apis, find_compatible_api + +# Отключение предупреждений о небезопасных SSL-соединениях +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + +logger = logging.getLogger(__name__) + +class SynologyAPI: + """Класс для взаимодействия с API Synology NAS""" + + def __init__(self): + """Инициализация класса SynologyAPI""" + logger.info("Creating API with auto-retry and connection pool") + logger.debug(f"Connection details: Host={SYNOLOGY_HOST}, Port={SYNOLOGY_PORT}, Secure={SYNOLOGY_SECURE}") + + self.protocol = "https" if SYNOLOGY_SECURE else "http" + self.base_url = f"{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi" + self.sid = None + self.session = requests.Session() + + # Настройка SSL + if self.protocol == "https": + logger.debug("SSL enabled, disabling certificate verification for internal network") + self.session.verify = False # Отключаем проверку SSL для внутренней сети + + # Добавляем повторные попытки для HTTP-запросов + retry_strategy = Retry( + total=5, # Увеличиваем количество попыток + status_forcelist=[429, 500, 502, 503, 504, 404], + allowed_methods=["GET", "POST"], + backoff_factor=1.5, # Увеличиваем задержку между попытками + respect_retry_after_header=True + ) + adapter = HTTPAdapter( + max_retries=retry_strategy, + pool_connections=3, + pool_maxsize=10 + ) + self.session.mount("http://", adapter) + self.session.mount("https://", adapter) + + # Таймауты будут указаны в запросах + self.default_timeout = (SYNOLOGY_TIMEOUT, SYNOLOGY_TIMEOUT*2) + logger.debug(f"Setting default request timeout: {self.default_timeout}") + + # Кэш для хранения результатов запросов + self._cache = {} + self._cache_ttl = {} + self._last_online_check = 0 + self._last_online_status = False + self._online_check_interval = 30 # Интервал для кэширования проверок онлайн-статуса + + # Время последней успешной аутентификации и срок действия сессии + self._last_auth_time = 0 + self._auth_expiry = 3600 # По умолчанию 1 час + + # Информация о доступных API + self._available_apis = {} + self._api_info_ttl = 0 + + def login(self) -> bool: + """Авторизация в API Synology NAS""" + # Проверяем, действительна ли текущая сессия + current_time = time.time() + if self.sid and (current_time - self._last_auth_time) < self._auth_expiry: + logger.debug("Using existing session, still valid") + return True + + logger.info("Attempting to authenticate with Synology NAS...") + logger.debug(f"Base URL: {self.base_url}") + + # Проверяем доступность NAS перед запросом с помощью прямой TCP-проверки + # Избегаем вызова is_online(), чтобы не создавать рекурсию + online_status = self._check_tcp_connection() + if not online_status: + logger.error("Cannot login: Synology NAS is not reachable") + return False + + try: + # Проверка информации API для определения доступных версий API + api_info_url = f"{self.base_url}/entry.cgi" + api_info_params = { + "api": "SYNO.API.Info", + "version": "1", + "method": "query", + "query": "SYNO.API.Auth" + } + + logger.debug("Querying API info to determine optimal auth version") + try: + api_info_response = self.session.get( + api_info_url, + params=api_info_params, + timeout=self.default_timeout, + verify=False + ) + + if api_info_response.status_code == 200: + api_info_data = api_info_response.json() + if api_info_data.get("success"): + auth_info = api_info_data.get("data", {}).get("SYNO.API.Auth", {}) + max_version = auth_info.get("maxVersion", 3) + min_version = auth_info.get("minVersion", 1) + auth_path = auth_info.get("path", "auth.cgi") + logger.debug(f"Auth API versions: min={min_version}, max={max_version}, path={auth_path}") + + # Используем максимально поддерживаемую версию, но не выше 6 + auth_version = min(max_version, 6) + else: + logger.warning("Failed to query API info, using default auth version 3") + auth_version = 3 + auth_path = "auth.cgi" + else: + logger.warning(f"API info request failed with status {api_info_response.status_code}, using default auth version 3") + auth_version = 3 + auth_path = "auth.cgi" + except Exception as e: + logger.warning(f"Error querying API info: {str(e)}, using default auth version 3") + auth_version = 3 + auth_path = "auth.cgi" + + # Основной запрос авторизации + url = f"{self.base_url}/{auth_path}" + params = { + "api": "SYNO.API.Auth", + "version": str(auth_version), + "method": "login", + "account": SYNOLOGY_USERNAME, + "passwd": SYNOLOGY_PASSWORD, + "session": "SynologyPowerControlBot", + "format": "cookie" + } + + # Для версии 6+ используем немного другой формат + if auth_version >= 6: + params["enable_syno_token"] = "yes" + + logger.debug(f"Sending auth request to {url} with API version {auth_version}") + start_time = time.time() + response = self.session.get( + url, + params=params, + timeout=self.default_timeout, + verify=False + ) + elapsed_time = time.time() - start_time + logger.debug(f"Auth request completed in {elapsed_time:.2f}s with status code {response.status_code}") + + # Подробная информация для отладки + logger.debug(f"Auth response: {response.text[:500]}") + + # Проверка статуса HTTP + if response.status_code != 200: + logger.error(f"HTTP error: {response.status_code}") + logger.debug(f"Response content: {response.text[:200]}") + return False + + try: + data = response.json() + except json.JSONDecodeError: + logger.error("Failed to decode JSON response") + logger.debug(f"Response content: {response.text[:200]}") + return False + + if data.get("success"): + self.sid = data.get("data", {}).get("sid") + self._last_auth_time = current_time + logger.info("Successfully logged in to Synology NAS") + logger.debug(f"Session ID obtained: {self.sid[:5] if self.sid else 'None'}...") # Показываем только начало SID для безопасности + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to log in to Synology NAS: Error code {error_code}") + + # Дополнительная диагностика + if error_code == 400: + logger.error("Authentication error: Invalid credentials") + elif error_code == 401: + logger.error("Authentication error: Account disabled") + elif error_code == 402: + logger.error("Authentication error: Permission denied") + elif error_code == 403: + logger.error("Authentication error: 2-factor authentication required") + elif error_code == 404: + logger.error("Authentication error: Failed to authenticate with 2-factor authentication") + + return False + + except requests.exceptions.Timeout: + logger.error(f"Connection timeout after {SYNOLOGY_TIMEOUT} seconds") + return False + except requests.exceptions.ConnectionError as e: + logger.error(f"Connection error: {str(e)}") + logger.debug(f"Connection details: {self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}") + return False + except requests.RequestException as e: + logger.error(f"Request error: {str(e)}") + return False + except Exception as e: + logger.error(f"Unexpected error during login: {str(e)}", exc_info=True) + return False + + def logout(self) -> bool: + """Выход из API Synology NAS""" + if not self.sid: + return True + + try: + url = f"{self.base_url}/auth.cgi" + params = { + "api": "SYNO.API.Auth", + "version": "1", + "method": "logout", + "session": "SynologyPowerControlBot", + "_sid": self.sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + self.sid = None + logger.info("Successfully logged out from Synology NAS") + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to log out from Synology NAS: Error code {error_code}") + return False + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + return False + + def _make_api_request(self, api_name: str, method: str, version: int = 1, + params: Optional[Dict[str, Any]] = None, retry_auth: bool = True) -> Optional[Dict[str, Any]]: + """Обобщенный метод для выполнения API запросов с обработкой ошибок""" + # Проверка наличия авторизации + if not self.sid and not self.login(): + logger.error(f"Not authenticated for API request: {api_name}.{method}") + return None + + # Проверка информации API для определения пути и поддерживаемой версии + api_info_url = f"{self.base_url}/entry.cgi" + api_info_params = { + "api": "SYNO.API.Info", + "version": "1", + "method": "query", + "query": api_name + } + + api_path = "entry.cgi" + try: + logger.debug(f"Querying API info for {api_name}") + api_info_response = self.session.get( + api_info_url, + params=api_info_params, + timeout=self.default_timeout, + verify=False + ) + + if api_info_response.status_code == 200: + api_info_data = api_info_response.json() + if api_info_data.get("success"): + api_info = api_info_data.get("data", {}).get(api_name, {}) + if api_info: + max_version = api_info.get("maxVersion", version) + min_version = api_info.get("minVersion", version) + api_path = api_info.get("path", "entry.cgi") + + # Проверка, поддерживается ли запрошенная версия + if version < min_version: + logger.warning(f"API version {version} for {api_name} is below minimum {min_version}, using {min_version}") + version = min_version + elif version > max_version: + logger.warning(f"API version {version} for {api_name} exceeds maximum {max_version}, using {max_version}") + version = max_version + + logger.debug(f"Using API path: {api_path}, version: {version}") + else: + logger.warning(f"API {api_name} not found in API info, using defaults") + except Exception as e: + logger.warning(f"Error querying API info for {api_name}: {str(e)}, using defaults") + + # Подготовка базовых параметров запроса + base_params = { + "api": api_name, + "version": str(version), + "method": method, + "sid": self.sid + } + + # Добавление дополнительных параметров, если они заданы + if params: + base_params.update(params) + + url = f"{self.base_url}/{api_path}" + logger.debug(f"API request: {api_name}.{method} v{version} to {url}") + logger.debug(f"Full request params: {base_params}") + + try: + start_time = time.time() + response = self.session.get( + url, + params=base_params, + timeout=self.default_timeout, + verify=False + ) + elapsed_time = time.time() - start_time + logger.debug(f"API request completed in {elapsed_time:.2f}s with status {response.status_code}") + + # Подробное логирование для отладки + logger.debug(f"Response: {response.text[:500]}") + + # Проверка статуса HTTP + if response.status_code != 200: + logger.error(f"HTTP error: {response.status_code} for {api_name}.{method}") + return None + + try: + data = response.json() + except json.JSONDecodeError: + logger.error(f"Failed to decode JSON response for {api_name}.{method}") + logger.debug(f"Response content: {response.text[:200]}") + return None + + if data.get("success"): + logger.info(f"API request successful for {api_name}.{method}") + return data.get("data", {}) + else: + error_code = data.get("error", {}).get("code", -1) + error_desc = self._get_error_description(error_code) + logger.error(f"API error for {api_name}.{method}: {error_code} - {error_desc}") + + # Ошибки доступа или прав часто встречаются, но они не критичные + # Например, ошибка 102 означает, что нет прав, но NAS доступен + if error_code in [102, 103, 104, 105]: + logger.warning(f"Permission-related error for {api_name}.{method}: {error_code}") + # Возвращаем пустой словарь вместо None, + # чтобы вызывающий код мог понять, что запрос выполнен + return {} + + # Если ошибка связана с авторизацией и нам разрешено повторить попытку + if error_code in [106, 107, 119] and retry_auth: + logger.info("Session may have expired, re-authenticating...") + self.sid = None # Сбрасываем SID + if self.login(): + logger.info("Re-authenticated, retrying API request...") + # Рекурсивный вызов, но без повторной авторизации во избежание зацикливания + return self._make_api_request(api_name, method, version, params, False) + + return None + + except requests.exceptions.Timeout: + logger.error(f"Request timeout for {api_name}.{method}") + return None + except requests.exceptions.ConnectionError as e: + logger.error(f"Connection error for {api_name}.{method}: {str(e)}") + return None + except Exception as e: + logger.error(f"Unexpected error during {api_name}.{method}: {str(e)}") + return None + + def get_system_status(self) -> Dict[str, Any]: + """Получение статуса системы""" + # Проверяем доступность системы + if not self.is_online(): + logger.info("Device is offline, skipping API request") + return {"status": "offline"} + + # Проверяем, есть ли кэшированный результат + cache_key = "system_status" + current_time = time.time() + if cache_key in self._cache and current_time - self._cache_ttl.get(cache_key, 0) < 60: + logger.debug("Using cached system status") + return self._cache[cache_key] + + # Пробуем разные API для получения информации о системе + apis_to_try = [ + {"name": "SYNO.DSM.Info", "method": "getinfo", "version": 2}, + {"name": "SYNO.Core.System", "method": "info", "version": 1}, + {"name": "SYNO.Core.System.Status", "method": "get", "version": 1}, + {"name": "SYNO.Core.System.Info", "method": "get", "version": 1}, + ] + + for api in apis_to_try: + logger.info(f"Trying to get system info using {api['name']}.{api['method']} v{api['version']}") + result = self._make_api_request(api["name"], api["method"], version=api["version"]) + + if result: + logger.info(f"Successfully retrieved system info using {api['name']}") + + # Формируем расширенный ответ с дополнительной информацией + system_info = { + "status": "online", + "hostname": result.get("hostname", "unknown"), + "model": result.get("model", "unknown"), + "version": result.get("version", "unknown"), + "uptime": result.get("uptime", 0), + "time": current_time, + "is_online": True, + "api_used": api["name"] + } + + # Сохраняем в кэше + self._cache[cache_key] = system_info + self._cache_ttl[cache_key] = current_time + + return system_info + + # Если все запросы не удались, но система онлайн, возвращаем базовую информацию + logger.warning("Failed to retrieve system info with all API methods") + return { + "status": "error", + "error": "Failed to fetch system information", + "is_online": True, + "time": current_time + } + + def shutdown_system(self) -> bool: + """Выключение системы""" + # Проверяем, включено ли устройство перед попыткой его выключить + if not self.is_online(force_check=True): + logger.info("Synology NAS is already offline, no need to shut down") + return True + + logger.info("Attempting to shutdown Synology NAS...") + + # Проверка всех доступных методов API для выключения + # Проверяем наличие API перед использованием + apis_to_try = [ + {"name": "SYNO.Core.System.Power", "method": "shutdown", "version": 1}, + {"name": "SYNO.DSM.Power", "method": "shutdown", "version": 1}, + {"name": "SYNO.Core.System", "method": "shutdown", "version": 3}, + {"name": "SYNO.System.Power", "method": "shutdown", "version": 1}, + {"name": "SYNO.DSM.System", "method": "shutdown", "version": 1} + ] + + # Получаем список доступных API + try: + api_info_url = f"{self.base_url}/entry.cgi" + api_info_params = { + "api": "SYNO.API.Info", + "version": "1", + "method": "query", + "query": "SYNO.Core.System.Power,SYNO.DSM.Power,SYNO.Core.System,SYNO.System.Power,SYNO.DSM.System" + } + + logger.debug("Checking available shutdown APIs") + api_info_response = self.session.get( + api_info_url, + params=api_info_params, + timeout=self.default_timeout, + verify=False + ) + + if api_info_response.status_code == 200: + api_info_data = api_info_response.json() + if api_info_data.get("success"): + available_apis = api_info_data.get("data", {}) + logger.debug(f"Available APIs: {list(available_apis.keys())}") + + # Фильтруем только доступные API + filtered_apis = [] + for api in apis_to_try: + if api["name"] in available_apis: + api_info = available_apis[api["name"]] + # Проверка версии API + if api_info.get("minVersion", 1) <= api["version"] <= api_info.get("maxVersion", 1): + filtered_apis.append(api) + logger.debug(f"Adding {api['name']} to available shutdown APIs") + else: + logger.debug(f"API {api['name']} version {api['version']} not supported. Supported: {api_info.get('minVersion')}-{api_info.get('maxVersion')}") + + if filtered_apis: + apis_to_try = filtered_apis + else: + logger.warning("No compatible APIs found, trying all methods as fallback") + else: + logger.warning("Failed to query API info, using default methods") + else: + logger.warning(f"API info request failed with status {api_info_response.status_code}, using default methods") + except Exception as e: + logger.warning(f"Error querying API info: {str(e)}, using default methods") + + # Пробуем все доступные методы по порядку + for api in apis_to_try: + logger.info(f"Trying shutdown with {api['name']}.{api['method']} v{api['version']}") + result = self._make_api_request(api["name"], api["method"], version=api["version"]) + + if result is not None: + logger.info(f"Successfully initiated system shutdown using {api['name']}") + + # Даем системе время начать процесс выключения + logger.info("Waiting for shutdown to initialize...") + sleep(5) + + # Проверяем статус + if not self.is_online(force_check=True): + logger.info("System is now offline. Shutdown confirmed successful.") + return True + else: + logger.info("System still appears to be online, but shutdown may be in progress.") + return True + else: + logger.warning(f"Failed to shutdown using {api['name']}.{api['method']} v{api['version']}") + + # Если ни один метод не сработал, но система стала недоступна + if not self.is_online(force_check=True): + logger.info("System appears to be shutting down despite API errors") + return True + + logger.error("Failed to shutdown system after trying multiple APIs") + return False + + def reboot_system(self) -> bool: + """Перезагрузка системы""" + # Проверяем, включена ли система + if not self.is_online(force_check=True): + logger.error("Cannot reboot: System is offline") + return False + + logger.info("Attempting to reboot Synology NAS...") + + # Проверка всех доступных методов API для перезагрузки + apis_to_try = [ + {"name": "SYNO.Core.System.Power", "method": "restart", "version": 1}, + {"name": "SYNO.DSM.Power", "method": "reboot", "version": 1}, + {"name": "SYNO.Core.System", "method": "reboot", "version": 3}, + {"name": "SYNO.System.Power", "method": "reboot", "version": 1}, + {"name": "SYNO.DSM.System", "method": "reboot", "version": 1} + ] + + # Получаем список доступных API + try: + api_info_url = f"{self.base_url}/entry.cgi" + api_info_params = { + "api": "SYNO.API.Info", + "version": "1", + "method": "query", + "query": "SYNO.Core.System.Power,SYNO.DSM.Power,SYNO.Core.System,SYNO.System.Power,SYNO.DSM.System" + } + + logger.debug("Checking available reboot APIs") + api_info_response = self.session.get( + api_info_url, + params=api_info_params, + timeout=self.default_timeout, + verify=False + ) + + if api_info_response.status_code == 200: + api_info_data = api_info_response.json() + if api_info_data.get("success"): + available_apis = api_info_data.get("data", {}) + logger.debug(f"Available APIs: {list(available_apis.keys())}") + + # Фильтруем только доступные API + filtered_apis = [] + for api in apis_to_try: + if api["name"] in available_apis: + api_info = available_apis[api["name"]] + # Проверка версии API + if api_info.get("minVersion", 1) <= api["version"] <= api_info.get("maxVersion", 1): + filtered_apis.append(api) + logger.debug(f"Adding {api['name']} to available reboot APIs") + else: + logger.debug(f"API {api['name']} version {api['version']} not supported. Supported: {api_info.get('minVersion')}-{api_info.get('maxVersion')}") + + if filtered_apis: + apis_to_try = filtered_apis + else: + logger.warning("No compatible APIs found, trying all methods as fallback") + else: + logger.warning("Failed to query API info, using default methods") + else: + logger.warning(f"API info request failed with status {api_info_response.status_code}, using default methods") + except Exception as e: + logger.warning(f"Error querying API info: {str(e)}, using default methods") + + # Пробуем все доступные методы по порядку + for api in apis_to_try: + logger.info(f"Trying reboot with {api['name']}.{api['method']} v{api['version']}") + result = self._make_api_request(api["name"], api["method"], version=api["version"]) + + if result is not None: + logger.info(f"Successfully initiated system reboot using {api['name']}") + + # Даем системе время начать процесс перезагрузки + logger.info("Waiting for reboot to initialize...") + sleep(5) + + # Ждем, пока система станет недоступна (признак перезагрузки) + reboot_started = False + for i in range(12): # Проверяем в течение 60 секунд (12 * 5сек) + if not self.is_online(force_check=True): + logger.info(f"System went offline after {i*5} seconds, reboot in progress") + reboot_started = True + break + logger.debug(f"System still online, waiting... ({i+1}/12)") + sleep(5) + + if reboot_started: + # Ждем, пока система снова станет доступна + logger.info("Waiting for system to come back online...") + return self.wait_for_boot(max_attempts=30, delay=10) + else: + logger.warning("System did not go offline after reboot command, but command was accepted") + # Даже если система не ушла оффлайн, команда могла быть принята + return True + else: + logger.warning(f"Failed to reboot using {api['name']}.{api['method']} v{api['version']}") + + logger.error("Failed to reboot system after trying multiple APIs") + return False + + def _get_error_description(self, error_code: int) -> str: + """Получение описания ошибки по коду""" + error_descriptions = { + 100: "Unknown error", + 101: "Invalid parameter", + 102: "API does not exist", + 103: "Method does not exist", + 104: "Version does not support", + 105: "Permission denied", + 106: "Session timeout", + 107: "Session interrupted by duplicate login", + 400: "Invalid credentials", + 401: "Account disabled", + 402: "Permission denied", + 403: "2FA required", + 404: "Failed to authenticate with 2FA" + } + return error_descriptions.get(error_code, "Unknown error code") + + def _check_tcp_connection(self) -> bool: + """Проверка базового TCP-соединения с Synology NAS""" + try: + socket_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + socket_obj.settimeout(SYNOLOGY_TIMEOUT) + result = socket_obj.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT)) + socket_obj.close() + + return result == 0 + except socket.error as e: + logger.error(f"Socket error during connection check: {str(e)}") + return False + except Exception as e: + logger.error(f"Unexpected error during connection check: {str(e)}") + return False + + def is_online(self, force_check=False) -> bool: + """Проверка онлайн-статуса Synology NAS""" + # Используем кэшированное значение, если доступно и не устарело + current_time = time.time() + if not force_check and (current_time - self._last_online_check) < self._online_check_interval: + logger.debug(f"Using cached online status: {self._last_online_status}") + return self._last_online_status + + logger.info("Checking if NAS is online...") + + # Проверяем TCP-соединение + online_status = self._check_tcp_connection() + logger.info(f"Detected Synology NAS online status: {online_status}") + + # Если TCP-соединение успешно и у нас есть действующий SID, + # попробуем более детальную проверку через API + if online_status and self.sid: + logger.info("Trying to fetch more detailed online status through API...") + + # Пробуем разные API для проверки онлайн-статуса + api_checks = [ + {"api": "SYNO.DSM.Info", "version": "2", "method": "getinfo"}, + {"api": "SYNO.Core.System", "version": "1", "method": "info"}, + {"api": "SYNO.Core.System.Status", "version": "1", "method": "get"} + ] + + api_success = False + for api_check in api_checks: + try: + url = f"{self.base_url}/entry.cgi" + params = { + "api": api_check["api"], + "version": api_check["version"], + "method": api_check["method"], + "sid": self.sid + } + + logger.debug(f"Trying online status check with {api_check['api']}") + response = self.session.get( + url, + params=params, + timeout=self.default_timeout, + verify=False + ) + + if response.status_code == 200: + data = response.json() + if data.get("success"): + logger.info(f"API request successful for {api_check['api']}") + logger.info("Synology NAS is online with API access") + api_success = True + break + else: + error_code = data.get("error", {}).get("code", -1) + logger.warning(f"API response indicates an error: {error_code}, but NAS is reachable") + else: + logger.warning(f"API returned status code {response.status_code}") + except Exception as e: + logger.warning(f"API check failed for {api_check['api']}, but TCP connection succeeded: {str(e)}") + + if not api_success: + logger.warning("All API checks failed, but TCP connection is successful") + + # Обновляем кэшированное значение + self._last_online_check = current_time + self._last_online_status = online_status + + return online_status + + def wake_on_lan(self) -> bool: + """Отправка Wake-on-LAN пакета для включения Synology NAS""" + if not SYNOLOGY_MAC: + logger.error("MAC address not configured") + return False + + try: + # Преобразование MAC-адреса в байты + mac_address = SYNOLOGY_MAC.replace(':', '').replace('-', '') + if len(mac_address) != 12: + logger.error(f"Invalid MAC address format: {SYNOLOGY_MAC}") + return False + + try: + mac_bytes = bytes.fromhex(mac_address) + except ValueError as e: + logger.error(f"Failed to parse MAC address: {str(e)}") + logger.error(f"MAC address must contain only hex characters: {SYNOLOGY_MAC}") + return False + + # Создание Magic Packet + magic_packet = b'\xff' * 6 + mac_bytes * 16 + + # Отправка пакета на конкретный адрес + try: + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) + sock.sendto(magic_packet, (SYNOLOGY_HOST, WOL_PORT)) + sock.close() + logger.info(f"Wake-on-LAN packet sent to {SYNOLOGY_MAC} at {SYNOLOGY_HOST}:{WOL_PORT}") + except Exception as e: + logger.error(f"Error sending directed WoL packet: {str(e)}") + return False + + # Для надежности отправляем также широковещательный пакет + try: + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) + + # Используем стандартный широковещательный адрес + broadcast_addr = "255.255.255.255" + sock.sendto(magic_packet, (broadcast_addr, WOL_PORT)) + sock.close() + logger.info(f"Wake-on-LAN broadcast packet sent to {broadcast_addr}:{WOL_PORT}") + except Exception as e: + logger.warning(f"Error sending broadcast WoL packet (non-critical): {str(e)}") + # Не считаем ошибкой, т.к. основной пакет уже отправлен + + return True + + except Exception as e: + logger.error(f"Unexpected error in wake_on_lan: {str(e)}") + return False + + def wait_for_boot(self, max_attempts: int = 30, delay: int = 10) -> bool: + """Ожидание загрузки Synology NAS""" + logger.info(f"Waiting for Synology NAS to boot (timeout: {max_attempts * delay}s)...") + + for attempt in range(max_attempts): + # Принудительно проверяем статус без использования кэша + if self.is_online(force_check=True): + logger.info(f"Synology NAS is online after {attempt + 1} attempts ({(attempt + 1) * delay}s)") + + # Проверяем, что не только сеть доступна, но и API загрузился + api_ready = False + logger.info("Waiting for API services to initialize...") + + for api_check in range(5): # Даем еще до 50 секунд для загрузки API + if self.sid or self.login(): + api_ready = True + logger.info(f"API services are ready after {api_check + 1} attempts") + break + logger.debug(f"API not ready yet, waiting... ({api_check + 1}/5)") + sleep(10) + + if not api_ready: + logger.warning("System is online but API services may not be fully initialized") + + # Дадим дополнительное время для полной загрузки всех сервисов + sleep(delay) + return True + + sleep(delay) + logger.info(f"Waiting for boot... Attempt {attempt + 1}/{max_attempts}") + + logger.error(f"Synology NAS didn't come online after {max_attempts * delay} seconds") + return False + + def power_on(self) -> bool: + """Включение Synology NAS""" + # Принудительная проверка статуса + if self.is_online(force_check=True): + logger.info("Synology NAS is already online") + return True + + logger.info("Powering on Synology NAS via Wake-on-LAN...") + + # Проверяем, настроен ли MAC-адрес + if not SYNOLOGY_MAC: + logger.error("Cannot power on: MAC address not configured in settings") + return False + + # Пробуем отправить несколько WoL пакетов для надежности + success = False + for attempt in range(3): + logger.debug(f"Sending WoL packet, attempt {attempt + 1}/3") + if self.wake_on_lan(): + success = True + break + sleep(1) + + if not success: + logger.error("Failed to send Wake-on-LAN packets") + return False + + # Ожидание загрузки + logger.info("WoL packets sent successfully, waiting for system to boot...") + boot_result = self.wait_for_boot(max_attempts=30, delay=10) + + if boot_result: + # Проверяем доступность API после загрузки + system_status = self.get_system_status() + if system_status.get("status") == "online": + logger.info("System booted successfully with API access") + return True + else: + logger.warning("System appears to be online but API may not be fully ready") + return True + else: + logger.error("System did not come online after WoL") + return False + + def power_off(self) -> bool: + """Выключение Synology NAS""" + if not self.is_online(force_check=True): + logger.info("Synology NAS is already offline") + return True + + logger.info("Powering off Synology NAS...") + result = self.shutdown_system() + + if result: + # Дополнительная проверка, что система действительно выключилась + logger.info("Verifying system is offline...") + for attempt in range(12): # Проверяем в течение 2 минут (12 * 10 сек) + if not self.is_online(force_check=True): + logger.info(f"System confirmed offline after {attempt * 10} seconds") + return True + logger.debug(f"System still shutting down, checking again... ({attempt + 1}/12)") + sleep(10) + + logger.warning("System still appears to be online after shutdown command") + return False + else: + logger.error("Failed to initiate shutdown") + return False + + # Заглушки для расширенных методов + def get_shared_folders(self) -> List[Dict[str, Any]]: + """Получение списка общих папок (заглушка)""" + logger.warning("Function get_shared_folders() is not implemented yet") + return [] + + def get_system_load(self) -> Dict[str, Any]: + """Получение информации о загрузке системы (заглушка)""" + logger.warning("Function get_system_load() is not implemented yet") + return {} + + def get_storage_status(self) -> Dict[str, Any]: + """Получение подробной информации о хранилище (заглушка)""" + logger.warning("Function get_storage_status() is not implemented yet") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for storage status request") + return {"error": "authentication_failed"} + + try: + # Здесь будет реализация после добавления соответствующего API + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.Storage.CGI.Storage", + "version": "1", + "method": "load_info", + "sid": self.sid + } + + # В текущей версии просто возвращаем заглушку + return { + "volumes": [], + "disks": [], + "total_size": 0, + "total_used": 0, + "error": "not_implemented" + } + except Exception as e: + logger.error(f"Error in get_storage_status: {str(e)}") + return {"error": str(e)} + + def get_security_status(self) -> Dict[str, Any]: + """Получение информации о состоянии безопасности (заглушка)""" + logger.warning("Function get_security_status() is not implemented yet") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for security status request") + return {"success": False, "error": "authentication_failed"} + + try: + # Здесь будет реализация после добавления соответствующего API + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.Core.SecurityScan.Status", + "version": "1", + "method": "get", + "sid": self.sid + } + + # В текущей версии просто возвращаем заглушку + return { + "success": False, + "status": "not_implemented", + "last_check": None, + "is_secure": False, + "error": "not_implemented" + } + except Exception as e: + logger.error(f"Error in get_security_status: {str(e)}") + return {"success": False, "error": str(e)} diff --git a/.history/src/api/synology_20250830082307.py b/.history/src/api/synology_20250830082307.py new file mode 100644 index 0000000..0d5a4b2 --- /dev/null +++ b/.history/src/api/synology_20250830082307.py @@ -0,0 +1,1018 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Модуль для взаимодействия с API Synology NAS +""" + +import requests +from requests.adapters import HTTPAdapter +import json +import logging +import time +import urllib3 +from urllib3.util import Retry +from typing import Dict, Any, Optional, List +import socket +import struct +from time import sleep + +from src.config.config import ( + SYNOLOGY_HOST, + SYNOLOGY_PORT, + SYNOLOGY_USERNAME, + SYNOLOGY_PASSWORD, + SYNOLOGY_SECURE, + SYNOLOGY_TIMEOUT, + SYNOLOGY_MAC, + WOL_PORT, + SYNOLOGY_API_VERSION, + SYNOLOGY_POWER_API, + SYNOLOGY_INFO_API +) +from src.api.api_discovery import discover_available_apis, find_compatible_api + +# Отключение предупреждений о небезопасных SSL-соединениях +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + +logger = logging.getLogger(__name__) + +class SynologyAPI: + """Класс для взаимодействия с API Synology NAS""" + + def __init__(self): + """Инициализация класса SynologyAPI""" + logger.info("Creating API with auto-retry and connection pool") + logger.debug(f"Connection details: Host={SYNOLOGY_HOST}, Port={SYNOLOGY_PORT}, Secure={SYNOLOGY_SECURE}") + + self.protocol = "https" if SYNOLOGY_SECURE else "http" + self.base_url = f"{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi" + self.sid = None + self.session = requests.Session() + + # Настройка SSL + if self.protocol == "https": + logger.debug("SSL enabled, disabling certificate verification for internal network") + self.session.verify = False # Отключаем проверку SSL для внутренней сети + + # Добавляем повторные попытки для HTTP-запросов + retry_strategy = Retry( + total=5, # Увеличиваем количество попыток + status_forcelist=[429, 500, 502, 503, 504, 404], + allowed_methods=["GET", "POST"], + backoff_factor=1.5, # Увеличиваем задержку между попытками + respect_retry_after_header=True + ) + adapter = HTTPAdapter( + max_retries=retry_strategy, + pool_connections=3, + pool_maxsize=10 + ) + self.session.mount("http://", adapter) + self.session.mount("https://", adapter) + + # Таймауты будут указаны в запросах + self.default_timeout = (SYNOLOGY_TIMEOUT, SYNOLOGY_TIMEOUT*2) + logger.debug(f"Setting default request timeout: {self.default_timeout}") + + # Кэш для хранения результатов запросов + self._cache = {} + self._cache_ttl = {} + self._last_online_check = 0 + self._last_online_status = False + self._online_check_interval = 30 # Интервал для кэширования проверок онлайн-статуса + + # Время последней успешной аутентификации и срок действия сессии + self._last_auth_time = 0 + self._auth_expiry = 3600 # По умолчанию 1 час + + # Информация о доступных API + self._available_apis = {} + self._api_info_ttl = 0 + + def login(self) -> bool: + """Авторизация в API Synology NAS""" + # Проверяем, действительна ли текущая сессия + current_time = time.time() + if self.sid and (current_time - self._last_auth_time) < self._auth_expiry: + logger.debug("Using existing session, still valid") + return True + + logger.info("Attempting to authenticate with Synology NAS...") + logger.debug(f"Base URL: {self.base_url}") + + # Проверяем доступность NAS перед запросом с помощью прямой TCP-проверки + # Избегаем вызова is_online(), чтобы не создавать рекурсию + online_status = self._check_tcp_connection() + if not online_status: + logger.error("Cannot login: Synology NAS is not reachable") + return False + + try: + # Проверка информации API для определения доступных версий API + api_info_url = f"{self.base_url}/entry.cgi" + api_info_params = { + "api": "SYNO.API.Info", + "version": "1", + "method": "query", + "query": "SYNO.API.Auth" + } + + logger.debug("Querying API info to determine optimal auth version") + try: + api_info_response = self.session.get( + api_info_url, + params=api_info_params, + timeout=self.default_timeout, + verify=False + ) + + if api_info_response.status_code == 200: + api_info_data = api_info_response.json() + if api_info_data.get("success"): + auth_info = api_info_data.get("data", {}).get("SYNO.API.Auth", {}) + max_version = auth_info.get("maxVersion", 3) + min_version = auth_info.get("minVersion", 1) + auth_path = auth_info.get("path", "auth.cgi") + logger.debug(f"Auth API versions: min={min_version}, max={max_version}, path={auth_path}") + + # Используем максимально поддерживаемую версию, но не выше 6 + auth_version = min(max_version, 6) + else: + logger.warning("Failed to query API info, using default auth version 3") + auth_version = 3 + auth_path = "auth.cgi" + else: + logger.warning(f"API info request failed with status {api_info_response.status_code}, using default auth version 3") + auth_version = 3 + auth_path = "auth.cgi" + except Exception as e: + logger.warning(f"Error querying API info: {str(e)}, using default auth version 3") + auth_version = 3 + auth_path = "auth.cgi" + + # Основной запрос авторизации + url = f"{self.base_url}/{auth_path}" + params = { + "api": "SYNO.API.Auth", + "version": str(auth_version), + "method": "login", + "account": SYNOLOGY_USERNAME, + "passwd": SYNOLOGY_PASSWORD, + "session": "SynologyPowerControlBot", + "format": "cookie" + } + + # Для версии 6+ используем немного другой формат + if auth_version >= 6: + params["enable_syno_token"] = "yes" + + logger.debug(f"Sending auth request to {url} with API version {auth_version}") + start_time = time.time() + response = self.session.get( + url, + params=params, + timeout=self.default_timeout, + verify=False + ) + elapsed_time = time.time() - start_time + logger.debug(f"Auth request completed in {elapsed_time:.2f}s with status code {response.status_code}") + + # Подробная информация для отладки + logger.debug(f"Auth response: {response.text[:500]}") + + # Проверка статуса HTTP + if response.status_code != 200: + logger.error(f"HTTP error: {response.status_code}") + logger.debug(f"Response content: {response.text[:200]}") + return False + + try: + data = response.json() + except json.JSONDecodeError: + logger.error("Failed to decode JSON response") + logger.debug(f"Response content: {response.text[:200]}") + return False + + if data.get("success"): + self.sid = data.get("data", {}).get("sid") + self._last_auth_time = current_time + logger.info("Successfully logged in to Synology NAS") + logger.debug(f"Session ID obtained: {self.sid[:5] if self.sid else 'None'}...") # Показываем только начало SID для безопасности + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to log in to Synology NAS: Error code {error_code}") + + # Дополнительная диагностика + if error_code == 400: + logger.error("Authentication error: Invalid credentials") + elif error_code == 401: + logger.error("Authentication error: Account disabled") + elif error_code == 402: + logger.error("Authentication error: Permission denied") + elif error_code == 403: + logger.error("Authentication error: 2-factor authentication required") + elif error_code == 404: + logger.error("Authentication error: Failed to authenticate with 2-factor authentication") + + return False + + except requests.exceptions.Timeout: + logger.error(f"Connection timeout after {SYNOLOGY_TIMEOUT} seconds") + return False + except requests.exceptions.ConnectionError as e: + logger.error(f"Connection error: {str(e)}") + logger.debug(f"Connection details: {self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}") + return False + except requests.RequestException as e: + logger.error(f"Request error: {str(e)}") + return False + except Exception as e: + logger.error(f"Unexpected error during login: {str(e)}", exc_info=True) + return False + + def logout(self) -> bool: + """Выход из API Synology NAS""" + if not self.sid: + return True + + try: + url = f"{self.base_url}/auth.cgi" + params = { + "api": "SYNO.API.Auth", + "version": "1", + "method": "logout", + "session": "SynologyPowerControlBot", + "_sid": self.sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + self.sid = None + logger.info("Successfully logged out from Synology NAS") + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to log out from Synology NAS: Error code {error_code}") + return False + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + return False + + def _make_api_request(self, api_name: str, method: str, version: int = 1, + params: Optional[Dict[str, Any]] = None, retry_auth: bool = True) -> Optional[Dict[str, Any]]: + """Обобщенный метод для выполнения API запросов с обработкой ошибок""" + # Проверка наличия авторизации + if not self.sid and not self.login(): + logger.error(f"Not authenticated for API request: {api_name}.{method}") + return None + + # Проверка информации API для определения пути и поддерживаемой версии + api_info_url = f"{self.base_url}/entry.cgi" + api_info_params = { + "api": "SYNO.API.Info", + "version": "1", + "method": "query", + "query": api_name + } + + api_path = "entry.cgi" + try: + logger.debug(f"Querying API info for {api_name}") + api_info_response = self.session.get( + api_info_url, + params=api_info_params, + timeout=self.default_timeout, + verify=False + ) + + if api_info_response.status_code == 200: + api_info_data = api_info_response.json() + if api_info_data.get("success"): + api_info = api_info_data.get("data", {}).get(api_name, {}) + if api_info: + max_version = api_info.get("maxVersion", version) + min_version = api_info.get("minVersion", version) + api_path = api_info.get("path", "entry.cgi") + + # Проверка, поддерживается ли запрошенная версия + if version < min_version: + logger.warning(f"API version {version} for {api_name} is below minimum {min_version}, using {min_version}") + version = min_version + elif version > max_version: + logger.warning(f"API version {version} for {api_name} exceeds maximum {max_version}, using {max_version}") + version = max_version + + logger.debug(f"Using API path: {api_path}, version: {version}") + else: + logger.warning(f"API {api_name} not found in API info, using defaults") + except Exception as e: + logger.warning(f"Error querying API info for {api_name}: {str(e)}, using defaults") + + # Подготовка базовых параметров запроса + base_params = { + "api": api_name, + "version": str(version), + "method": method, + "sid": self.sid + } + + # Добавление дополнительных параметров, если они заданы + if params: + base_params.update(params) + + url = f"{self.base_url}/{api_path}" + logger.debug(f"API request: {api_name}.{method} v{version} to {url}") + logger.debug(f"Full request params: {base_params}") + + try: + start_time = time.time() + response = self.session.get( + url, + params=base_params, + timeout=self.default_timeout, + verify=False + ) + elapsed_time = time.time() - start_time + logger.debug(f"API request completed in {elapsed_time:.2f}s with status {response.status_code}") + + # Подробное логирование для отладки + logger.debug(f"Response: {response.text[:500]}") + + # Проверка статуса HTTP + if response.status_code != 200: + logger.error(f"HTTP error: {response.status_code} for {api_name}.{method}") + return None + + try: + data = response.json() + except json.JSONDecodeError: + logger.error(f"Failed to decode JSON response for {api_name}.{method}") + logger.debug(f"Response content: {response.text[:200]}") + return None + + if data.get("success"): + logger.info(f"API request successful for {api_name}.{method}") + return data.get("data", {}) + else: + error_code = data.get("error", {}).get("code", -1) + error_desc = self._get_error_description(error_code) + logger.error(f"API error for {api_name}.{method}: {error_code} - {error_desc}") + + # Ошибки доступа или прав часто встречаются, но они не критичные + # Например, ошибка 102 означает, что нет прав, но NAS доступен + if error_code in [102, 103, 104, 105]: + logger.warning(f"Permission-related error for {api_name}.{method}: {error_code}") + # Возвращаем пустой словарь вместо None, + # чтобы вызывающий код мог понять, что запрос выполнен + return {} + + # Если ошибка связана с авторизацией и нам разрешено повторить попытку + if error_code in [106, 107, 119] and retry_auth: + logger.info("Session may have expired, re-authenticating...") + self.sid = None # Сбрасываем SID + if self.login(): + logger.info("Re-authenticated, retrying API request...") + # Рекурсивный вызов, но без повторной авторизации во избежание зацикливания + return self._make_api_request(api_name, method, version, params, False) + + return None + + except requests.exceptions.Timeout: + logger.error(f"Request timeout for {api_name}.{method}") + return None + except requests.exceptions.ConnectionError as e: + logger.error(f"Connection error for {api_name}.{method}: {str(e)}") + return None + except Exception as e: + logger.error(f"Unexpected error during {api_name}.{method}: {str(e)}") + return None + + def get_system_status(self) -> Dict[str, Any]: + """Получение статуса системы""" + # Проверяем доступность системы + if not self.is_online(): + logger.info("Device is offline, skipping API request") + return {"status": "offline"} + + # Проверяем, есть ли кэшированный результат + cache_key = "system_status" + current_time = time.time() + if cache_key in self._cache and current_time - self._cache_ttl.get(cache_key, 0) < 60: + logger.debug("Using cached system status") + return self._cache[cache_key] + + # Используем рекомендованный API для получения информации о системе + logger.info(f"Getting system info using {SYNOLOGY_INFO_API} v{SYNOLOGY_API_VERSION}") + + # Для SYNO.DSM.Info метод getinfo, для других обычно используется get или info + if SYNOLOGY_INFO_API == "SYNO.DSM.Info": + method = "getinfo" + else: + method = "get" + + result = self._make_api_request(SYNOLOGY_INFO_API, method, version=SYNOLOGY_API_VERSION) + + if result: + logger.info(f"Successfully retrieved system info using {SYNOLOGY_INFO_API}") + + # Формируем расширенный ответ с дополнительной информацией + system_info = { + "status": "online", + "hostname": result.get("hostname", "unknown"), + "model": result.get("model", "unknown"), + "version": result.get("version", "unknown"), + "uptime": result.get("uptime", 0), + "time": current_time, + "is_online": True, + "api_used": SYNOLOGY_INFO_API + } + + # Сохраняем в кэше + self._cache[cache_key] = system_info + self._cache_ttl[cache_key] = current_time + + return system_info + + # Если основной API не сработал, пробуем резервные варианты + logger.warning(f"Failed to retrieve system info with {SYNOLOGY_INFO_API}, trying fallback APIs") + + # Пробуем резервные API + apis_to_try = [ + {"name": "SYNO.DSM.Info", "method": "getinfo", "version": 2}, + {"name": "SYNO.Core.System", "method": "info", "version": 1}, + {"name": "SYNO.Core.System.Status", "method": "get", "version": 1}, + {"name": "SYNO.Core.System.Info", "method": "get", "version": 1}, + ] + + for api in apis_to_try: + if api["name"] == SYNOLOGY_INFO_API: + continue # Пропускаем уже проверенный API + + logger.info(f"Trying to get system info using {api['name']}.{api['method']} v{api['version']}") + result = self._make_api_request(api["name"], api["method"], version=api["version"]) + + if result: + logger.info(f"Successfully retrieved system info using {api['name']}") + + # Формируем расширенный ответ с дополнительной информацией + system_info = { + "status": "online", + "hostname": result.get("hostname", "unknown"), + "model": result.get("model", "unknown"), + "version": result.get("version", "unknown"), + "uptime": result.get("uptime", 0), + "time": current_time, + "is_online": True, + "api_used": api["name"] + } + + # Сохраняем в кэше + self._cache[cache_key] = system_info + self._cache_ttl[cache_key] = current_time + + return system_info + + # Если все запросы не удались, но система онлайн, возвращаем базовую информацию + logger.warning("Failed to retrieve system info with all API methods") + return { + "status": "error", + "error": "Failed to fetch system information", + "is_online": True, + "time": current_time + } + + def shutdown_system(self) -> bool: + """Выключение системы""" + # Проверяем, включено ли устройство перед попыткой его выключить + if not self.is_online(force_check=True): + logger.info("Synology NAS is already offline, no need to shut down") + return True + + logger.info("Attempting to shutdown Synology NAS...") + + # Проверка всех доступных методов API для выключения + # Проверяем наличие API перед использованием + apis_to_try = [ + {"name": "SYNO.Core.System.Power", "method": "shutdown", "version": 1}, + {"name": "SYNO.DSM.Power", "method": "shutdown", "version": 1}, + {"name": "SYNO.Core.System", "method": "shutdown", "version": 3}, + {"name": "SYNO.System.Power", "method": "shutdown", "version": 1}, + {"name": "SYNO.DSM.System", "method": "shutdown", "version": 1} + ] + + # Получаем список доступных API + try: + api_info_url = f"{self.base_url}/entry.cgi" + api_info_params = { + "api": "SYNO.API.Info", + "version": "1", + "method": "query", + "query": "SYNO.Core.System.Power,SYNO.DSM.Power,SYNO.Core.System,SYNO.System.Power,SYNO.DSM.System" + } + + logger.debug("Checking available shutdown APIs") + api_info_response = self.session.get( + api_info_url, + params=api_info_params, + timeout=self.default_timeout, + verify=False + ) + + if api_info_response.status_code == 200: + api_info_data = api_info_response.json() + if api_info_data.get("success"): + available_apis = api_info_data.get("data", {}) + logger.debug(f"Available APIs: {list(available_apis.keys())}") + + # Фильтруем только доступные API + filtered_apis = [] + for api in apis_to_try: + if api["name"] in available_apis: + api_info = available_apis[api["name"]] + # Проверка версии API + if api_info.get("minVersion", 1) <= api["version"] <= api_info.get("maxVersion", 1): + filtered_apis.append(api) + logger.debug(f"Adding {api['name']} to available shutdown APIs") + else: + logger.debug(f"API {api['name']} version {api['version']} not supported. Supported: {api_info.get('minVersion')}-{api_info.get('maxVersion')}") + + if filtered_apis: + apis_to_try = filtered_apis + else: + logger.warning("No compatible APIs found, trying all methods as fallback") + else: + logger.warning("Failed to query API info, using default methods") + else: + logger.warning(f"API info request failed with status {api_info_response.status_code}, using default methods") + except Exception as e: + logger.warning(f"Error querying API info: {str(e)}, using default methods") + + # Пробуем все доступные методы по порядку + for api in apis_to_try: + logger.info(f"Trying shutdown with {api['name']}.{api['method']} v{api['version']}") + result = self._make_api_request(api["name"], api["method"], version=api["version"]) + + if result is not None: + logger.info(f"Successfully initiated system shutdown using {api['name']}") + + # Даем системе время начать процесс выключения + logger.info("Waiting for shutdown to initialize...") + sleep(5) + + # Проверяем статус + if not self.is_online(force_check=True): + logger.info("System is now offline. Shutdown confirmed successful.") + return True + else: + logger.info("System still appears to be online, but shutdown may be in progress.") + return True + else: + logger.warning(f"Failed to shutdown using {api['name']}.{api['method']} v{api['version']}") + + # Если ни один метод не сработал, но система стала недоступна + if not self.is_online(force_check=True): + logger.info("System appears to be shutting down despite API errors") + return True + + logger.error("Failed to shutdown system after trying multiple APIs") + return False + + def reboot_system(self) -> bool: + """Перезагрузка системы""" + # Проверяем, включена ли система + if not self.is_online(force_check=True): + logger.error("Cannot reboot: System is offline") + return False + + logger.info("Attempting to reboot Synology NAS...") + + # Проверка всех доступных методов API для перезагрузки + apis_to_try = [ + {"name": "SYNO.Core.System.Power", "method": "restart", "version": 1}, + {"name": "SYNO.DSM.Power", "method": "reboot", "version": 1}, + {"name": "SYNO.Core.System", "method": "reboot", "version": 3}, + {"name": "SYNO.System.Power", "method": "reboot", "version": 1}, + {"name": "SYNO.DSM.System", "method": "reboot", "version": 1} + ] + + # Получаем список доступных API + try: + api_info_url = f"{self.base_url}/entry.cgi" + api_info_params = { + "api": "SYNO.API.Info", + "version": "1", + "method": "query", + "query": "SYNO.Core.System.Power,SYNO.DSM.Power,SYNO.Core.System,SYNO.System.Power,SYNO.DSM.System" + } + + logger.debug("Checking available reboot APIs") + api_info_response = self.session.get( + api_info_url, + params=api_info_params, + timeout=self.default_timeout, + verify=False + ) + + if api_info_response.status_code == 200: + api_info_data = api_info_response.json() + if api_info_data.get("success"): + available_apis = api_info_data.get("data", {}) + logger.debug(f"Available APIs: {list(available_apis.keys())}") + + # Фильтруем только доступные API + filtered_apis = [] + for api in apis_to_try: + if api["name"] in available_apis: + api_info = available_apis[api["name"]] + # Проверка версии API + if api_info.get("minVersion", 1) <= api["version"] <= api_info.get("maxVersion", 1): + filtered_apis.append(api) + logger.debug(f"Adding {api['name']} to available reboot APIs") + else: + logger.debug(f"API {api['name']} version {api['version']} not supported. Supported: {api_info.get('minVersion')}-{api_info.get('maxVersion')}") + + if filtered_apis: + apis_to_try = filtered_apis + else: + logger.warning("No compatible APIs found, trying all methods as fallback") + else: + logger.warning("Failed to query API info, using default methods") + else: + logger.warning(f"API info request failed with status {api_info_response.status_code}, using default methods") + except Exception as e: + logger.warning(f"Error querying API info: {str(e)}, using default methods") + + # Пробуем все доступные методы по порядку + for api in apis_to_try: + logger.info(f"Trying reboot with {api['name']}.{api['method']} v{api['version']}") + result = self._make_api_request(api["name"], api["method"], version=api["version"]) + + if result is not None: + logger.info(f"Successfully initiated system reboot using {api['name']}") + + # Даем системе время начать процесс перезагрузки + logger.info("Waiting for reboot to initialize...") + sleep(5) + + # Ждем, пока система станет недоступна (признак перезагрузки) + reboot_started = False + for i in range(12): # Проверяем в течение 60 секунд (12 * 5сек) + if not self.is_online(force_check=True): + logger.info(f"System went offline after {i*5} seconds, reboot in progress") + reboot_started = True + break + logger.debug(f"System still online, waiting... ({i+1}/12)") + sleep(5) + + if reboot_started: + # Ждем, пока система снова станет доступна + logger.info("Waiting for system to come back online...") + return self.wait_for_boot(max_attempts=30, delay=10) + else: + logger.warning("System did not go offline after reboot command, but command was accepted") + # Даже если система не ушла оффлайн, команда могла быть принята + return True + else: + logger.warning(f"Failed to reboot using {api['name']}.{api['method']} v{api['version']}") + + logger.error("Failed to reboot system after trying multiple APIs") + return False + + def _get_error_description(self, error_code: int) -> str: + """Получение описания ошибки по коду""" + error_descriptions = { + 100: "Unknown error", + 101: "Invalid parameter", + 102: "API does not exist", + 103: "Method does not exist", + 104: "Version does not support", + 105: "Permission denied", + 106: "Session timeout", + 107: "Session interrupted by duplicate login", + 400: "Invalid credentials", + 401: "Account disabled", + 402: "Permission denied", + 403: "2FA required", + 404: "Failed to authenticate with 2FA" + } + return error_descriptions.get(error_code, "Unknown error code") + + def _check_tcp_connection(self) -> bool: + """Проверка базового TCP-соединения с Synology NAS""" + try: + socket_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + socket_obj.settimeout(SYNOLOGY_TIMEOUT) + result = socket_obj.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT)) + socket_obj.close() + + return result == 0 + except socket.error as e: + logger.error(f"Socket error during connection check: {str(e)}") + return False + except Exception as e: + logger.error(f"Unexpected error during connection check: {str(e)}") + return False + + def is_online(self, force_check=False) -> bool: + """Проверка онлайн-статуса Synology NAS""" + # Используем кэшированное значение, если доступно и не устарело + current_time = time.time() + if not force_check and (current_time - self._last_online_check) < self._online_check_interval: + logger.debug(f"Using cached online status: {self._last_online_status}") + return self._last_online_status + + logger.info("Checking if NAS is online...") + + # Проверяем TCP-соединение + online_status = self._check_tcp_connection() + logger.info(f"Detected Synology NAS online status: {online_status}") + + # Если TCP-соединение успешно и у нас есть действующий SID, + # попробуем более детальную проверку через API + if online_status and self.sid: + logger.info("Trying to fetch more detailed online status through API...") + + # Пробуем разные API для проверки онлайн-статуса + api_checks = [ + {"api": "SYNO.DSM.Info", "version": "2", "method": "getinfo"}, + {"api": "SYNO.Core.System", "version": "1", "method": "info"}, + {"api": "SYNO.Core.System.Status", "version": "1", "method": "get"} + ] + + api_success = False + for api_check in api_checks: + try: + url = f"{self.base_url}/entry.cgi" + params = { + "api": api_check["api"], + "version": api_check["version"], + "method": api_check["method"], + "sid": self.sid + } + + logger.debug(f"Trying online status check with {api_check['api']}") + response = self.session.get( + url, + params=params, + timeout=self.default_timeout, + verify=False + ) + + if response.status_code == 200: + data = response.json() + if data.get("success"): + logger.info(f"API request successful for {api_check['api']}") + logger.info("Synology NAS is online with API access") + api_success = True + break + else: + error_code = data.get("error", {}).get("code", -1) + logger.warning(f"API response indicates an error: {error_code}, but NAS is reachable") + else: + logger.warning(f"API returned status code {response.status_code}") + except Exception as e: + logger.warning(f"API check failed for {api_check['api']}, but TCP connection succeeded: {str(e)}") + + if not api_success: + logger.warning("All API checks failed, but TCP connection is successful") + + # Обновляем кэшированное значение + self._last_online_check = current_time + self._last_online_status = online_status + + return online_status + + def wake_on_lan(self) -> bool: + """Отправка Wake-on-LAN пакета для включения Synology NAS""" + if not SYNOLOGY_MAC: + logger.error("MAC address not configured") + return False + + try: + # Преобразование MAC-адреса в байты + mac_address = SYNOLOGY_MAC.replace(':', '').replace('-', '') + if len(mac_address) != 12: + logger.error(f"Invalid MAC address format: {SYNOLOGY_MAC}") + return False + + try: + mac_bytes = bytes.fromhex(mac_address) + except ValueError as e: + logger.error(f"Failed to parse MAC address: {str(e)}") + logger.error(f"MAC address must contain only hex characters: {SYNOLOGY_MAC}") + return False + + # Создание Magic Packet + magic_packet = b'\xff' * 6 + mac_bytes * 16 + + # Отправка пакета на конкретный адрес + try: + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) + sock.sendto(magic_packet, (SYNOLOGY_HOST, WOL_PORT)) + sock.close() + logger.info(f"Wake-on-LAN packet sent to {SYNOLOGY_MAC} at {SYNOLOGY_HOST}:{WOL_PORT}") + except Exception as e: + logger.error(f"Error sending directed WoL packet: {str(e)}") + return False + + # Для надежности отправляем также широковещательный пакет + try: + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) + + # Используем стандартный широковещательный адрес + broadcast_addr = "255.255.255.255" + sock.sendto(magic_packet, (broadcast_addr, WOL_PORT)) + sock.close() + logger.info(f"Wake-on-LAN broadcast packet sent to {broadcast_addr}:{WOL_PORT}") + except Exception as e: + logger.warning(f"Error sending broadcast WoL packet (non-critical): {str(e)}") + # Не считаем ошибкой, т.к. основной пакет уже отправлен + + return True + + except Exception as e: + logger.error(f"Unexpected error in wake_on_lan: {str(e)}") + return False + + def wait_for_boot(self, max_attempts: int = 30, delay: int = 10) -> bool: + """Ожидание загрузки Synology NAS""" + logger.info(f"Waiting for Synology NAS to boot (timeout: {max_attempts * delay}s)...") + + for attempt in range(max_attempts): + # Принудительно проверяем статус без использования кэша + if self.is_online(force_check=True): + logger.info(f"Synology NAS is online after {attempt + 1} attempts ({(attempt + 1) * delay}s)") + + # Проверяем, что не только сеть доступна, но и API загрузился + api_ready = False + logger.info("Waiting for API services to initialize...") + + for api_check in range(5): # Даем еще до 50 секунд для загрузки API + if self.sid or self.login(): + api_ready = True + logger.info(f"API services are ready after {api_check + 1} attempts") + break + logger.debug(f"API not ready yet, waiting... ({api_check + 1}/5)") + sleep(10) + + if not api_ready: + logger.warning("System is online but API services may not be fully initialized") + + # Дадим дополнительное время для полной загрузки всех сервисов + sleep(delay) + return True + + sleep(delay) + logger.info(f"Waiting for boot... Attempt {attempt + 1}/{max_attempts}") + + logger.error(f"Synology NAS didn't come online after {max_attempts * delay} seconds") + return False + + def power_on(self) -> bool: + """Включение Synology NAS""" + # Принудительная проверка статуса + if self.is_online(force_check=True): + logger.info("Synology NAS is already online") + return True + + logger.info("Powering on Synology NAS via Wake-on-LAN...") + + # Проверяем, настроен ли MAC-адрес + if not SYNOLOGY_MAC: + logger.error("Cannot power on: MAC address not configured in settings") + return False + + # Пробуем отправить несколько WoL пакетов для надежности + success = False + for attempt in range(3): + logger.debug(f"Sending WoL packet, attempt {attempt + 1}/3") + if self.wake_on_lan(): + success = True + break + sleep(1) + + if not success: + logger.error("Failed to send Wake-on-LAN packets") + return False + + # Ожидание загрузки + logger.info("WoL packets sent successfully, waiting for system to boot...") + boot_result = self.wait_for_boot(max_attempts=30, delay=10) + + if boot_result: + # Проверяем доступность API после загрузки + system_status = self.get_system_status() + if system_status.get("status") == "online": + logger.info("System booted successfully with API access") + return True + else: + logger.warning("System appears to be online but API may not be fully ready") + return True + else: + logger.error("System did not come online after WoL") + return False + + def power_off(self) -> bool: + """Выключение Synology NAS""" + if not self.is_online(force_check=True): + logger.info("Synology NAS is already offline") + return True + + logger.info("Powering off Synology NAS...") + result = self.shutdown_system() + + if result: + # Дополнительная проверка, что система действительно выключилась + logger.info("Verifying system is offline...") + for attempt in range(12): # Проверяем в течение 2 минут (12 * 10 сек) + if not self.is_online(force_check=True): + logger.info(f"System confirmed offline after {attempt * 10} seconds") + return True + logger.debug(f"System still shutting down, checking again... ({attempt + 1}/12)") + sleep(10) + + logger.warning("System still appears to be online after shutdown command") + return False + else: + logger.error("Failed to initiate shutdown") + return False + + # Заглушки для расширенных методов + def get_shared_folders(self) -> List[Dict[str, Any]]: + """Получение списка общих папок (заглушка)""" + logger.warning("Function get_shared_folders() is not implemented yet") + return [] + + def get_system_load(self) -> Dict[str, Any]: + """Получение информации о загрузке системы (заглушка)""" + logger.warning("Function get_system_load() is not implemented yet") + return {} + + def get_storage_status(self) -> Dict[str, Any]: + """Получение подробной информации о хранилище (заглушка)""" + logger.warning("Function get_storage_status() is not implemented yet") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for storage status request") + return {"error": "authentication_failed"} + + try: + # Здесь будет реализация после добавления соответствующего API + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.Storage.CGI.Storage", + "version": "1", + "method": "load_info", + "sid": self.sid + } + + # В текущей версии просто возвращаем заглушку + return { + "volumes": [], + "disks": [], + "total_size": 0, + "total_used": 0, + "error": "not_implemented" + } + except Exception as e: + logger.error(f"Error in get_storage_status: {str(e)}") + return {"error": str(e)} + + def get_security_status(self) -> Dict[str, Any]: + """Получение информации о состоянии безопасности (заглушка)""" + logger.warning("Function get_security_status() is not implemented yet") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for security status request") + return {"success": False, "error": "authentication_failed"} + + try: + # Здесь будет реализация после добавления соответствующего API + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.Core.SecurityScan.Status", + "version": "1", + "method": "get", + "sid": self.sid + } + + # В текущей версии просто возвращаем заглушку + return { + "success": False, + "status": "not_implemented", + "last_check": None, + "is_secure": False, + "error": "not_implemented" + } + except Exception as e: + logger.error(f"Error in get_security_status: {str(e)}") + return {"success": False, "error": str(e)} diff --git a/.history/src/api/synology_20250830082353.py b/.history/src/api/synology_20250830082353.py new file mode 100644 index 0000000..c78c04d --- /dev/null +++ b/.history/src/api/synology_20250830082353.py @@ -0,0 +1,1048 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Модуль для взаимодействия с API Synology NAS +""" + +import requests +from requests.adapters import HTTPAdapter +import json +import logging +import time +import urllib3 +from urllib3.util import Retry +from typing import Dict, Any, Optional, List +import socket +import struct +from time import sleep + +from src.config.config import ( + SYNOLOGY_HOST, + SYNOLOGY_PORT, + SYNOLOGY_USERNAME, + SYNOLOGY_PASSWORD, + SYNOLOGY_SECURE, + SYNOLOGY_TIMEOUT, + SYNOLOGY_MAC, + WOL_PORT, + SYNOLOGY_API_VERSION, + SYNOLOGY_POWER_API, + SYNOLOGY_INFO_API +) +from src.api.api_discovery import discover_available_apis, find_compatible_api + +# Отключение предупреждений о небезопасных SSL-соединениях +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + +logger = logging.getLogger(__name__) + +class SynologyAPI: + """Класс для взаимодействия с API Synology NAS""" + + def __init__(self): + """Инициализация класса SynologyAPI""" + logger.info("Creating API with auto-retry and connection pool") + logger.debug(f"Connection details: Host={SYNOLOGY_HOST}, Port={SYNOLOGY_PORT}, Secure={SYNOLOGY_SECURE}") + + self.protocol = "https" if SYNOLOGY_SECURE else "http" + self.base_url = f"{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi" + self.sid = None + self.session = requests.Session() + + # Настройка SSL + if self.protocol == "https": + logger.debug("SSL enabled, disabling certificate verification for internal network") + self.session.verify = False # Отключаем проверку SSL для внутренней сети + + # Добавляем повторные попытки для HTTP-запросов + retry_strategy = Retry( + total=5, # Увеличиваем количество попыток + status_forcelist=[429, 500, 502, 503, 504, 404], + allowed_methods=["GET", "POST"], + backoff_factor=1.5, # Увеличиваем задержку между попытками + respect_retry_after_header=True + ) + adapter = HTTPAdapter( + max_retries=retry_strategy, + pool_connections=3, + pool_maxsize=10 + ) + self.session.mount("http://", adapter) + self.session.mount("https://", adapter) + + # Таймауты будут указаны в запросах + self.default_timeout = (SYNOLOGY_TIMEOUT, SYNOLOGY_TIMEOUT*2) + logger.debug(f"Setting default request timeout: {self.default_timeout}") + + # Кэш для хранения результатов запросов + self._cache = {} + self._cache_ttl = {} + self._last_online_check = 0 + self._last_online_status = False + self._online_check_interval = 30 # Интервал для кэширования проверок онлайн-статуса + + # Время последней успешной аутентификации и срок действия сессии + self._last_auth_time = 0 + self._auth_expiry = 3600 # По умолчанию 1 час + + # Информация о доступных API + self._available_apis = {} + self._api_info_ttl = 0 + + def login(self) -> bool: + """Авторизация в API Synology NAS""" + # Проверяем, действительна ли текущая сессия + current_time = time.time() + if self.sid and (current_time - self._last_auth_time) < self._auth_expiry: + logger.debug("Using existing session, still valid") + return True + + logger.info("Attempting to authenticate with Synology NAS...") + logger.debug(f"Base URL: {self.base_url}") + + # Проверяем доступность NAS перед запросом с помощью прямой TCP-проверки + # Избегаем вызова is_online(), чтобы не создавать рекурсию + online_status = self._check_tcp_connection() + if not online_status: + logger.error("Cannot login: Synology NAS is not reachable") + return False + + try: + # Проверка информации API для определения доступных версий API + api_info_url = f"{self.base_url}/entry.cgi" + api_info_params = { + "api": "SYNO.API.Info", + "version": "1", + "method": "query", + "query": "SYNO.API.Auth" + } + + logger.debug("Querying API info to determine optimal auth version") + try: + api_info_response = self.session.get( + api_info_url, + params=api_info_params, + timeout=self.default_timeout, + verify=False + ) + + if api_info_response.status_code == 200: + api_info_data = api_info_response.json() + if api_info_data.get("success"): + auth_info = api_info_data.get("data", {}).get("SYNO.API.Auth", {}) + max_version = auth_info.get("maxVersion", 3) + min_version = auth_info.get("minVersion", 1) + auth_path = auth_info.get("path", "auth.cgi") + logger.debug(f"Auth API versions: min={min_version}, max={max_version}, path={auth_path}") + + # Используем максимально поддерживаемую версию, но не выше 6 + auth_version = min(max_version, 6) + else: + logger.warning("Failed to query API info, using default auth version 3") + auth_version = 3 + auth_path = "auth.cgi" + else: + logger.warning(f"API info request failed with status {api_info_response.status_code}, using default auth version 3") + auth_version = 3 + auth_path = "auth.cgi" + except Exception as e: + logger.warning(f"Error querying API info: {str(e)}, using default auth version 3") + auth_version = 3 + auth_path = "auth.cgi" + + # Основной запрос авторизации + url = f"{self.base_url}/{auth_path}" + params = { + "api": "SYNO.API.Auth", + "version": str(auth_version), + "method": "login", + "account": SYNOLOGY_USERNAME, + "passwd": SYNOLOGY_PASSWORD, + "session": "SynologyPowerControlBot", + "format": "cookie" + } + + # Для версии 6+ используем немного другой формат + if auth_version >= 6: + params["enable_syno_token"] = "yes" + + logger.debug(f"Sending auth request to {url} with API version {auth_version}") + start_time = time.time() + response = self.session.get( + url, + params=params, + timeout=self.default_timeout, + verify=False + ) + elapsed_time = time.time() - start_time + logger.debug(f"Auth request completed in {elapsed_time:.2f}s with status code {response.status_code}") + + # Подробная информация для отладки + logger.debug(f"Auth response: {response.text[:500]}") + + # Проверка статуса HTTP + if response.status_code != 200: + logger.error(f"HTTP error: {response.status_code}") + logger.debug(f"Response content: {response.text[:200]}") + return False + + try: + data = response.json() + except json.JSONDecodeError: + logger.error("Failed to decode JSON response") + logger.debug(f"Response content: {response.text[:200]}") + return False + + if data.get("success"): + self.sid = data.get("data", {}).get("sid") + self._last_auth_time = current_time + logger.info("Successfully logged in to Synology NAS") + logger.debug(f"Session ID obtained: {self.sid[:5] if self.sid else 'None'}...") # Показываем только начало SID для безопасности + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to log in to Synology NAS: Error code {error_code}") + + # Дополнительная диагностика + if error_code == 400: + logger.error("Authentication error: Invalid credentials") + elif error_code == 401: + logger.error("Authentication error: Account disabled") + elif error_code == 402: + logger.error("Authentication error: Permission denied") + elif error_code == 403: + logger.error("Authentication error: 2-factor authentication required") + elif error_code == 404: + logger.error("Authentication error: Failed to authenticate with 2-factor authentication") + + return False + + except requests.exceptions.Timeout: + logger.error(f"Connection timeout after {SYNOLOGY_TIMEOUT} seconds") + return False + except requests.exceptions.ConnectionError as e: + logger.error(f"Connection error: {str(e)}") + logger.debug(f"Connection details: {self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}") + return False + except requests.RequestException as e: + logger.error(f"Request error: {str(e)}") + return False + except Exception as e: + logger.error(f"Unexpected error during login: {str(e)}", exc_info=True) + return False + + def logout(self) -> bool: + """Выход из API Synology NAS""" + if not self.sid: + return True + + try: + url = f"{self.base_url}/auth.cgi" + params = { + "api": "SYNO.API.Auth", + "version": "1", + "method": "logout", + "session": "SynologyPowerControlBot", + "_sid": self.sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + self.sid = None + logger.info("Successfully logged out from Synology NAS") + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to log out from Synology NAS: Error code {error_code}") + return False + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + return False + + def _make_api_request(self, api_name: str, method: str, version: int = 1, + params: Optional[Dict[str, Any]] = None, retry_auth: bool = True) -> Optional[Dict[str, Any]]: + """Обобщенный метод для выполнения API запросов с обработкой ошибок""" + # Проверка наличия авторизации + if not self.sid and not self.login(): + logger.error(f"Not authenticated for API request: {api_name}.{method}") + return None + + # Проверка информации API для определения пути и поддерживаемой версии + api_info_url = f"{self.base_url}/entry.cgi" + api_info_params = { + "api": "SYNO.API.Info", + "version": "1", + "method": "query", + "query": api_name + } + + api_path = "entry.cgi" + try: + logger.debug(f"Querying API info for {api_name}") + api_info_response = self.session.get( + api_info_url, + params=api_info_params, + timeout=self.default_timeout, + verify=False + ) + + if api_info_response.status_code == 200: + api_info_data = api_info_response.json() + if api_info_data.get("success"): + api_info = api_info_data.get("data", {}).get(api_name, {}) + if api_info: + max_version = api_info.get("maxVersion", version) + min_version = api_info.get("minVersion", version) + api_path = api_info.get("path", "entry.cgi") + + # Проверка, поддерживается ли запрошенная версия + if version < min_version: + logger.warning(f"API version {version} for {api_name} is below minimum {min_version}, using {min_version}") + version = min_version + elif version > max_version: + logger.warning(f"API version {version} for {api_name} exceeds maximum {max_version}, using {max_version}") + version = max_version + + logger.debug(f"Using API path: {api_path}, version: {version}") + else: + logger.warning(f"API {api_name} not found in API info, using defaults") + except Exception as e: + logger.warning(f"Error querying API info for {api_name}: {str(e)}, using defaults") + + # Подготовка базовых параметров запроса + base_params = { + "api": api_name, + "version": str(version), + "method": method, + "sid": self.sid + } + + # Добавление дополнительных параметров, если они заданы + if params: + base_params.update(params) + + url = f"{self.base_url}/{api_path}" + logger.debug(f"API request: {api_name}.{method} v{version} to {url}") + logger.debug(f"Full request params: {base_params}") + + try: + start_time = time.time() + response = self.session.get( + url, + params=base_params, + timeout=self.default_timeout, + verify=False + ) + elapsed_time = time.time() - start_time + logger.debug(f"API request completed in {elapsed_time:.2f}s with status {response.status_code}") + + # Подробное логирование для отладки + logger.debug(f"Response: {response.text[:500]}") + + # Проверка статуса HTTP + if response.status_code != 200: + logger.error(f"HTTP error: {response.status_code} for {api_name}.{method}") + return None + + try: + data = response.json() + except json.JSONDecodeError: + logger.error(f"Failed to decode JSON response for {api_name}.{method}") + logger.debug(f"Response content: {response.text[:200]}") + return None + + if data.get("success"): + logger.info(f"API request successful for {api_name}.{method}") + return data.get("data", {}) + else: + error_code = data.get("error", {}).get("code", -1) + error_desc = self._get_error_description(error_code) + logger.error(f"API error for {api_name}.{method}: {error_code} - {error_desc}") + + # Ошибки доступа или прав часто встречаются, но они не критичные + # Например, ошибка 102 означает, что нет прав, но NAS доступен + if error_code in [102, 103, 104, 105]: + logger.warning(f"Permission-related error for {api_name}.{method}: {error_code}") + # Возвращаем пустой словарь вместо None, + # чтобы вызывающий код мог понять, что запрос выполнен + return {} + + # Если ошибка связана с авторизацией и нам разрешено повторить попытку + if error_code in [106, 107, 119] and retry_auth: + logger.info("Session may have expired, re-authenticating...") + self.sid = None # Сбрасываем SID + if self.login(): + logger.info("Re-authenticated, retrying API request...") + # Рекурсивный вызов, но без повторной авторизации во избежание зацикливания + return self._make_api_request(api_name, method, version, params, False) + + return None + + except requests.exceptions.Timeout: + logger.error(f"Request timeout for {api_name}.{method}") + return None + except requests.exceptions.ConnectionError as e: + logger.error(f"Connection error for {api_name}.{method}: {str(e)}") + return None + except Exception as e: + logger.error(f"Unexpected error during {api_name}.{method}: {str(e)}") + return None + + def get_system_status(self) -> Dict[str, Any]: + """Получение статуса системы""" + # Проверяем доступность системы + if not self.is_online(): + logger.info("Device is offline, skipping API request") + return {"status": "offline"} + + # Проверяем, есть ли кэшированный результат + cache_key = "system_status" + current_time = time.time() + if cache_key in self._cache and current_time - self._cache_ttl.get(cache_key, 0) < 60: + logger.debug("Using cached system status") + return self._cache[cache_key] + + # Используем рекомендованный API для получения информации о системе + logger.info(f"Getting system info using {SYNOLOGY_INFO_API} v{SYNOLOGY_API_VERSION}") + + # Для SYNO.DSM.Info метод getinfo, для других обычно используется get или info + if SYNOLOGY_INFO_API == "SYNO.DSM.Info": + method = "getinfo" + else: + method = "get" + + result = self._make_api_request(SYNOLOGY_INFO_API, method, version=SYNOLOGY_API_VERSION) + + if result: + logger.info(f"Successfully retrieved system info using {SYNOLOGY_INFO_API}") + + # Формируем расширенный ответ с дополнительной информацией + system_info = { + "status": "online", + "hostname": result.get("hostname", "unknown"), + "model": result.get("model", "unknown"), + "version": result.get("version", "unknown"), + "uptime": result.get("uptime", 0), + "time": current_time, + "is_online": True, + "api_used": SYNOLOGY_INFO_API + } + + # Сохраняем в кэше + self._cache[cache_key] = system_info + self._cache_ttl[cache_key] = current_time + + return system_info + + # Если основной API не сработал, пробуем резервные варианты + logger.warning(f"Failed to retrieve system info with {SYNOLOGY_INFO_API}, trying fallback APIs") + + # Пробуем резервные API + apis_to_try = [ + {"name": "SYNO.DSM.Info", "method": "getinfo", "version": 2}, + {"name": "SYNO.Core.System", "method": "info", "version": 1}, + {"name": "SYNO.Core.System.Status", "method": "get", "version": 1}, + {"name": "SYNO.Core.System.Info", "method": "get", "version": 1}, + ] + + for api in apis_to_try: + if api["name"] == SYNOLOGY_INFO_API: + continue # Пропускаем уже проверенный API + + logger.info(f"Trying to get system info using {api['name']}.{api['method']} v{api['version']}") + result = self._make_api_request(api["name"], api["method"], version=api["version"]) + + if result: + logger.info(f"Successfully retrieved system info using {api['name']}") + + # Формируем расширенный ответ с дополнительной информацией + system_info = { + "status": "online", + "hostname": result.get("hostname", "unknown"), + "model": result.get("model", "unknown"), + "version": result.get("version", "unknown"), + "uptime": result.get("uptime", 0), + "time": current_time, + "is_online": True, + "api_used": api["name"] + } + + # Сохраняем в кэше + self._cache[cache_key] = system_info + self._cache_ttl[cache_key] = current_time + + return system_info + + # Если все запросы не удались, но система онлайн, возвращаем базовую информацию + logger.warning("Failed to retrieve system info with all API methods") + return { + "status": "error", + "error": "Failed to fetch system information", + "is_online": True, + "time": current_time + } + + def shutdown_system(self) -> bool: + """Выключение системы""" + # Проверяем, включено ли устройство перед попыткой его выключить + if not self.is_online(force_check=True): + logger.info("Synology NAS is already offline, no need to shut down") + return True + + logger.info("Attempting to shutdown Synology NAS...") + + # Попробуем сначала использовать предпочтительный API для управления питанием + logger.info(f"Trying shutdown with {SYNOLOGY_POWER_API} v{SYNOLOGY_API_VERSION}") + + # Для SYNO.Core.Hardware.PowerRecovery используется метод setPowerOnState + # Для других API обычно используется метод shutdown или reboot + if SYNOLOGY_POWER_API == "SYNO.Core.Hardware.PowerRecovery": + # Для этого API нужны специальные параметры + params = {"state": "powerbtn"} # powerbtn имитирует нажатие кнопки питания + result = self._make_api_request(SYNOLOGY_POWER_API, "setPowerOnState", version=SYNOLOGY_API_VERSION, params=params) + else: + # Пробуем стандартный метод + result = self._make_api_request(SYNOLOGY_POWER_API, "shutdown", version=SYNOLOGY_API_VERSION) + + if result is not None: + logger.info(f"Successfully initiated system shutdown using {SYNOLOGY_POWER_API}") + + # Даем системе время начать процесс выключения + logger.info("Waiting for shutdown to initialize...") + sleep(5) + + # Проверяем статус + if not self.is_online(force_check=True): + logger.info("System is now offline. Shutdown confirmed successful.") + return True + else: + logger.info("System still appears to be online, but shutdown may be in progress.") + return True + else: + logger.warning(f"Failed to shutdown using {SYNOLOGY_POWER_API}, trying alternative methods") + + # Если не сработал основной метод, пробуем резервные варианты + # Проверка всех доступных методов API для выключения + apis_to_try = [ + {"name": "SYNO.Core.System", "method": "shutdown", "version": 3}, + {"name": "SYNO.Core.System.Power", "method": "shutdown", "version": 1}, + {"name": "SYNO.DSM.Power", "method": "shutdown", "version": 1}, + {"name": "SYNO.System.Power", "method": "shutdown", "version": 1}, + {"name": "SYNO.DSM.System", "method": "shutdown", "version": 1} + ] + + # Проверяем доступные API + try: + api_info_url = f"{self.base_url}/entry.cgi" + api_info_params = { + "api": "SYNO.API.Info", + "version": "1", + "method": "query", + "query": "SYNO.Core.System.Power,SYNO.DSM.Power,SYNO.Core.System,SYNO.System.Power,SYNO.DSM.System" + } + + logger.debug("Checking available shutdown APIs") + api_info_response = self.session.get( + api_info_url, + params=api_info_params, + timeout=self.default_timeout, + verify=False + ) + + if api_info_response.status_code == 200: + api_info_data = api_info_response.json() + if api_info_data.get("success"): + available_apis = api_info_data.get("data", {}) + logger.debug(f"Available APIs: {list(available_apis.keys())}") + + # Фильтруем только доступные API + filtered_apis = [] + for api in apis_to_try: + if api["name"] in available_apis: + api_info = available_apis[api["name"]] + # Проверка версии API + if api_info.get("minVersion", 1) <= api["version"] <= api_info.get("maxVersion", 1): + filtered_apis.append(api) + logger.debug(f"Adding {api['name']} to available shutdown APIs") + else: + logger.debug(f"API {api['name']} version {api['version']} not supported. Supported: {api_info.get('minVersion')}-{api_info.get('maxVersion')}") + + if filtered_apis: + apis_to_try = filtered_apis + else: + logger.warning("No compatible APIs found, trying all methods as fallback") + else: + logger.warning("Failed to query API info, using default methods") + else: + logger.warning(f"API info request failed with status {api_info_response.status_code}, using default methods") + except Exception as e: + logger.warning(f"Error querying API info: {str(e)}, using default methods") + + # Пробуем все доступные методы по порядку + for api in apis_to_try: + logger.info(f"Trying shutdown with {api['name']}.{api['method']} v{api['version']}") + result = self._make_api_request(api["name"], api["method"], version=api["version"]) + + if result is not None: + logger.info(f"Successfully initiated system shutdown using {api['name']}") + + # Даем системе время начать процесс выключения + logger.info("Waiting for shutdown to initialize...") + sleep(5) + + # Проверяем статус + if not self.is_online(force_check=True): + logger.info("System is now offline. Shutdown confirmed successful.") + return True + else: + logger.info("System still appears to be online, but shutdown may be in progress.") + return True + else: + logger.warning(f"Failed to shutdown using {api['name']}.{api['method']} v{api['version']}") + + # Если ни один метод не сработал, но система стала недоступна + if not self.is_online(force_check=True): + logger.info("System appears to be shutting down despite API errors") + return True + + logger.error("Failed to shutdown system after trying multiple APIs") + return False + + def reboot_system(self) -> bool: + """Перезагрузка системы""" + # Проверяем, включена ли система + if not self.is_online(force_check=True): + logger.error("Cannot reboot: System is offline") + return False + + logger.info("Attempting to reboot Synology NAS...") + + # Проверка всех доступных методов API для перезагрузки + apis_to_try = [ + {"name": "SYNO.Core.System.Power", "method": "restart", "version": 1}, + {"name": "SYNO.DSM.Power", "method": "reboot", "version": 1}, + {"name": "SYNO.Core.System", "method": "reboot", "version": 3}, + {"name": "SYNO.System.Power", "method": "reboot", "version": 1}, + {"name": "SYNO.DSM.System", "method": "reboot", "version": 1} + ] + + # Получаем список доступных API + try: + api_info_url = f"{self.base_url}/entry.cgi" + api_info_params = { + "api": "SYNO.API.Info", + "version": "1", + "method": "query", + "query": "SYNO.Core.System.Power,SYNO.DSM.Power,SYNO.Core.System,SYNO.System.Power,SYNO.DSM.System" + } + + logger.debug("Checking available reboot APIs") + api_info_response = self.session.get( + api_info_url, + params=api_info_params, + timeout=self.default_timeout, + verify=False + ) + + if api_info_response.status_code == 200: + api_info_data = api_info_response.json() + if api_info_data.get("success"): + available_apis = api_info_data.get("data", {}) + logger.debug(f"Available APIs: {list(available_apis.keys())}") + + # Фильтруем только доступные API + filtered_apis = [] + for api in apis_to_try: + if api["name"] in available_apis: + api_info = available_apis[api["name"]] + # Проверка версии API + if api_info.get("minVersion", 1) <= api["version"] <= api_info.get("maxVersion", 1): + filtered_apis.append(api) + logger.debug(f"Adding {api['name']} to available reboot APIs") + else: + logger.debug(f"API {api['name']} version {api['version']} not supported. Supported: {api_info.get('minVersion')}-{api_info.get('maxVersion')}") + + if filtered_apis: + apis_to_try = filtered_apis + else: + logger.warning("No compatible APIs found, trying all methods as fallback") + else: + logger.warning("Failed to query API info, using default methods") + else: + logger.warning(f"API info request failed with status {api_info_response.status_code}, using default methods") + except Exception as e: + logger.warning(f"Error querying API info: {str(e)}, using default methods") + + # Пробуем все доступные методы по порядку + for api in apis_to_try: + logger.info(f"Trying reboot with {api['name']}.{api['method']} v{api['version']}") + result = self._make_api_request(api["name"], api["method"], version=api["version"]) + + if result is not None: + logger.info(f"Successfully initiated system reboot using {api['name']}") + + # Даем системе время начать процесс перезагрузки + logger.info("Waiting for reboot to initialize...") + sleep(5) + + # Ждем, пока система станет недоступна (признак перезагрузки) + reboot_started = False + for i in range(12): # Проверяем в течение 60 секунд (12 * 5сек) + if not self.is_online(force_check=True): + logger.info(f"System went offline after {i*5} seconds, reboot in progress") + reboot_started = True + break + logger.debug(f"System still online, waiting... ({i+1}/12)") + sleep(5) + + if reboot_started: + # Ждем, пока система снова станет доступна + logger.info("Waiting for system to come back online...") + return self.wait_for_boot(max_attempts=30, delay=10) + else: + logger.warning("System did not go offline after reboot command, but command was accepted") + # Даже если система не ушла оффлайн, команда могла быть принята + return True + else: + logger.warning(f"Failed to reboot using {api['name']}.{api['method']} v{api['version']}") + + logger.error("Failed to reboot system after trying multiple APIs") + return False + + def _get_error_description(self, error_code: int) -> str: + """Получение описания ошибки по коду""" + error_descriptions = { + 100: "Unknown error", + 101: "Invalid parameter", + 102: "API does not exist", + 103: "Method does not exist", + 104: "Version does not support", + 105: "Permission denied", + 106: "Session timeout", + 107: "Session interrupted by duplicate login", + 400: "Invalid credentials", + 401: "Account disabled", + 402: "Permission denied", + 403: "2FA required", + 404: "Failed to authenticate with 2FA" + } + return error_descriptions.get(error_code, "Unknown error code") + + def _check_tcp_connection(self) -> bool: + """Проверка базового TCP-соединения с Synology NAS""" + try: + socket_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + socket_obj.settimeout(SYNOLOGY_TIMEOUT) + result = socket_obj.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT)) + socket_obj.close() + + return result == 0 + except socket.error as e: + logger.error(f"Socket error during connection check: {str(e)}") + return False + except Exception as e: + logger.error(f"Unexpected error during connection check: {str(e)}") + return False + + def is_online(self, force_check=False) -> bool: + """Проверка онлайн-статуса Synology NAS""" + # Используем кэшированное значение, если доступно и не устарело + current_time = time.time() + if not force_check and (current_time - self._last_online_check) < self._online_check_interval: + logger.debug(f"Using cached online status: {self._last_online_status}") + return self._last_online_status + + logger.info("Checking if NAS is online...") + + # Проверяем TCP-соединение + online_status = self._check_tcp_connection() + logger.info(f"Detected Synology NAS online status: {online_status}") + + # Если TCP-соединение успешно и у нас есть действующий SID, + # попробуем более детальную проверку через API + if online_status and self.sid: + logger.info("Trying to fetch more detailed online status through API...") + + # Пробуем разные API для проверки онлайн-статуса + api_checks = [ + {"api": "SYNO.DSM.Info", "version": "2", "method": "getinfo"}, + {"api": "SYNO.Core.System", "version": "1", "method": "info"}, + {"api": "SYNO.Core.System.Status", "version": "1", "method": "get"} + ] + + api_success = False + for api_check in api_checks: + try: + url = f"{self.base_url}/entry.cgi" + params = { + "api": api_check["api"], + "version": api_check["version"], + "method": api_check["method"], + "sid": self.sid + } + + logger.debug(f"Trying online status check with {api_check['api']}") + response = self.session.get( + url, + params=params, + timeout=self.default_timeout, + verify=False + ) + + if response.status_code == 200: + data = response.json() + if data.get("success"): + logger.info(f"API request successful for {api_check['api']}") + logger.info("Synology NAS is online with API access") + api_success = True + break + else: + error_code = data.get("error", {}).get("code", -1) + logger.warning(f"API response indicates an error: {error_code}, but NAS is reachable") + else: + logger.warning(f"API returned status code {response.status_code}") + except Exception as e: + logger.warning(f"API check failed for {api_check['api']}, but TCP connection succeeded: {str(e)}") + + if not api_success: + logger.warning("All API checks failed, but TCP connection is successful") + + # Обновляем кэшированное значение + self._last_online_check = current_time + self._last_online_status = online_status + + return online_status + + def wake_on_lan(self) -> bool: + """Отправка Wake-on-LAN пакета для включения Synology NAS""" + if not SYNOLOGY_MAC: + logger.error("MAC address not configured") + return False + + try: + # Преобразование MAC-адреса в байты + mac_address = SYNOLOGY_MAC.replace(':', '').replace('-', '') + if len(mac_address) != 12: + logger.error(f"Invalid MAC address format: {SYNOLOGY_MAC}") + return False + + try: + mac_bytes = bytes.fromhex(mac_address) + except ValueError as e: + logger.error(f"Failed to parse MAC address: {str(e)}") + logger.error(f"MAC address must contain only hex characters: {SYNOLOGY_MAC}") + return False + + # Создание Magic Packet + magic_packet = b'\xff' * 6 + mac_bytes * 16 + + # Отправка пакета на конкретный адрес + try: + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) + sock.sendto(magic_packet, (SYNOLOGY_HOST, WOL_PORT)) + sock.close() + logger.info(f"Wake-on-LAN packet sent to {SYNOLOGY_MAC} at {SYNOLOGY_HOST}:{WOL_PORT}") + except Exception as e: + logger.error(f"Error sending directed WoL packet: {str(e)}") + return False + + # Для надежности отправляем также широковещательный пакет + try: + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) + + # Используем стандартный широковещательный адрес + broadcast_addr = "255.255.255.255" + sock.sendto(magic_packet, (broadcast_addr, WOL_PORT)) + sock.close() + logger.info(f"Wake-on-LAN broadcast packet sent to {broadcast_addr}:{WOL_PORT}") + except Exception as e: + logger.warning(f"Error sending broadcast WoL packet (non-critical): {str(e)}") + # Не считаем ошибкой, т.к. основной пакет уже отправлен + + return True + + except Exception as e: + logger.error(f"Unexpected error in wake_on_lan: {str(e)}") + return False + + def wait_for_boot(self, max_attempts: int = 30, delay: int = 10) -> bool: + """Ожидание загрузки Synology NAS""" + logger.info(f"Waiting for Synology NAS to boot (timeout: {max_attempts * delay}s)...") + + for attempt in range(max_attempts): + # Принудительно проверяем статус без использования кэша + if self.is_online(force_check=True): + logger.info(f"Synology NAS is online after {attempt + 1} attempts ({(attempt + 1) * delay}s)") + + # Проверяем, что не только сеть доступна, но и API загрузился + api_ready = False + logger.info("Waiting for API services to initialize...") + + for api_check in range(5): # Даем еще до 50 секунд для загрузки API + if self.sid or self.login(): + api_ready = True + logger.info(f"API services are ready after {api_check + 1} attempts") + break + logger.debug(f"API not ready yet, waiting... ({api_check + 1}/5)") + sleep(10) + + if not api_ready: + logger.warning("System is online but API services may not be fully initialized") + + # Дадим дополнительное время для полной загрузки всех сервисов + sleep(delay) + return True + + sleep(delay) + logger.info(f"Waiting for boot... Attempt {attempt + 1}/{max_attempts}") + + logger.error(f"Synology NAS didn't come online after {max_attempts * delay} seconds") + return False + + def power_on(self) -> bool: + """Включение Synology NAS""" + # Принудительная проверка статуса + if self.is_online(force_check=True): + logger.info("Synology NAS is already online") + return True + + logger.info("Powering on Synology NAS via Wake-on-LAN...") + + # Проверяем, настроен ли MAC-адрес + if not SYNOLOGY_MAC: + logger.error("Cannot power on: MAC address not configured in settings") + return False + + # Пробуем отправить несколько WoL пакетов для надежности + success = False + for attempt in range(3): + logger.debug(f"Sending WoL packet, attempt {attempt + 1}/3") + if self.wake_on_lan(): + success = True + break + sleep(1) + + if not success: + logger.error("Failed to send Wake-on-LAN packets") + return False + + # Ожидание загрузки + logger.info("WoL packets sent successfully, waiting for system to boot...") + boot_result = self.wait_for_boot(max_attempts=30, delay=10) + + if boot_result: + # Проверяем доступность API после загрузки + system_status = self.get_system_status() + if system_status.get("status") == "online": + logger.info("System booted successfully with API access") + return True + else: + logger.warning("System appears to be online but API may not be fully ready") + return True + else: + logger.error("System did not come online after WoL") + return False + + def power_off(self) -> bool: + """Выключение Synology NAS""" + if not self.is_online(force_check=True): + logger.info("Synology NAS is already offline") + return True + + logger.info("Powering off Synology NAS...") + result = self.shutdown_system() + + if result: + # Дополнительная проверка, что система действительно выключилась + logger.info("Verifying system is offline...") + for attempt in range(12): # Проверяем в течение 2 минут (12 * 10 сек) + if not self.is_online(force_check=True): + logger.info(f"System confirmed offline after {attempt * 10} seconds") + return True + logger.debug(f"System still shutting down, checking again... ({attempt + 1}/12)") + sleep(10) + + logger.warning("System still appears to be online after shutdown command") + return False + else: + logger.error("Failed to initiate shutdown") + return False + + # Заглушки для расширенных методов + def get_shared_folders(self) -> List[Dict[str, Any]]: + """Получение списка общих папок (заглушка)""" + logger.warning("Function get_shared_folders() is not implemented yet") + return [] + + def get_system_load(self) -> Dict[str, Any]: + """Получение информации о загрузке системы (заглушка)""" + logger.warning("Function get_system_load() is not implemented yet") + return {} + + def get_storage_status(self) -> Dict[str, Any]: + """Получение подробной информации о хранилище (заглушка)""" + logger.warning("Function get_storage_status() is not implemented yet") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for storage status request") + return {"error": "authentication_failed"} + + try: + # Здесь будет реализация после добавления соответствующего API + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.Storage.CGI.Storage", + "version": "1", + "method": "load_info", + "sid": self.sid + } + + # В текущей версии просто возвращаем заглушку + return { + "volumes": [], + "disks": [], + "total_size": 0, + "total_used": 0, + "error": "not_implemented" + } + except Exception as e: + logger.error(f"Error in get_storage_status: {str(e)}") + return {"error": str(e)} + + def get_security_status(self) -> Dict[str, Any]: + """Получение информации о состоянии безопасности (заглушка)""" + logger.warning("Function get_security_status() is not implemented yet") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for security status request") + return {"success": False, "error": "authentication_failed"} + + try: + # Здесь будет реализация после добавления соответствующего API + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.Core.SecurityScan.Status", + "version": "1", + "method": "get", + "sid": self.sid + } + + # В текущей версии просто возвращаем заглушку + return { + "success": False, + "status": "not_implemented", + "last_check": None, + "is_secure": False, + "error": "not_implemented" + } + except Exception as e: + logger.error(f"Error in get_security_status: {str(e)}") + return {"success": False, "error": str(e)} diff --git a/.history/src/api/synology_20250830082444.py b/.history/src/api/synology_20250830082444.py new file mode 100644 index 0000000..beeca16 --- /dev/null +++ b/.history/src/api/synology_20250830082444.py @@ -0,0 +1,1097 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Модуль для взаимодействия с API Synology NAS +""" + +import requests +from requests.adapters import HTTPAdapter +import json +import logging +import time +import urllib3 +from urllib3.util import Retry +from typing import Dict, Any, Optional, List +import socket +import struct +from time import sleep + +from src.config.config import ( + SYNOLOGY_HOST, + SYNOLOGY_PORT, + SYNOLOGY_USERNAME, + SYNOLOGY_PASSWORD, + SYNOLOGY_SECURE, + SYNOLOGY_TIMEOUT, + SYNOLOGY_MAC, + WOL_PORT, + SYNOLOGY_API_VERSION, + SYNOLOGY_POWER_API, + SYNOLOGY_INFO_API +) +from src.api.api_discovery import discover_available_apis, find_compatible_api + +# Отключение предупреждений о небезопасных SSL-соединениях +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + +logger = logging.getLogger(__name__) + +class SynologyAPI: + """Класс для взаимодействия с API Synology NAS""" + + def __init__(self): + """Инициализация класса SynologyAPI""" + logger.info("Creating API with auto-retry and connection pool") + logger.debug(f"Connection details: Host={SYNOLOGY_HOST}, Port={SYNOLOGY_PORT}, Secure={SYNOLOGY_SECURE}") + + self.protocol = "https" if SYNOLOGY_SECURE else "http" + self.base_url = f"{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi" + self.sid = None + self.session = requests.Session() + + # Настройка SSL + if self.protocol == "https": + logger.debug("SSL enabled, disabling certificate verification for internal network") + self.session.verify = False # Отключаем проверку SSL для внутренней сети + + # Добавляем повторные попытки для HTTP-запросов + retry_strategy = Retry( + total=5, # Увеличиваем количество попыток + status_forcelist=[429, 500, 502, 503, 504, 404], + allowed_methods=["GET", "POST"], + backoff_factor=1.5, # Увеличиваем задержку между попытками + respect_retry_after_header=True + ) + adapter = HTTPAdapter( + max_retries=retry_strategy, + pool_connections=3, + pool_maxsize=10 + ) + self.session.mount("http://", adapter) + self.session.mount("https://", adapter) + + # Таймауты будут указаны в запросах + self.default_timeout = (SYNOLOGY_TIMEOUT, SYNOLOGY_TIMEOUT*2) + logger.debug(f"Setting default request timeout: {self.default_timeout}") + + # Кэш для хранения результатов запросов + self._cache = {} + self._cache_ttl = {} + self._last_online_check = 0 + self._last_online_status = False + self._online_check_interval = 30 # Интервал для кэширования проверок онлайн-статуса + + # Время последней успешной аутентификации и срок действия сессии + self._last_auth_time = 0 + self._auth_expiry = 3600 # По умолчанию 1 час + + # Информация о доступных API + self._available_apis = {} + self._api_info_ttl = 0 + + def login(self) -> bool: + """Авторизация в API Synology NAS""" + # Проверяем, действительна ли текущая сессия + current_time = time.time() + if self.sid and (current_time - self._last_auth_time) < self._auth_expiry: + logger.debug("Using existing session, still valid") + return True + + logger.info("Attempting to authenticate with Synology NAS...") + logger.debug(f"Base URL: {self.base_url}") + + # Проверяем доступность NAS перед запросом с помощью прямой TCP-проверки + # Избегаем вызова is_online(), чтобы не создавать рекурсию + online_status = self._check_tcp_connection() + if not online_status: + logger.error("Cannot login: Synology NAS is not reachable") + return False + + try: + # Проверка информации API для определения доступных версий API + api_info_url = f"{self.base_url}/entry.cgi" + api_info_params = { + "api": "SYNO.API.Info", + "version": "1", + "method": "query", + "query": "SYNO.API.Auth" + } + + logger.debug("Querying API info to determine optimal auth version") + try: + api_info_response = self.session.get( + api_info_url, + params=api_info_params, + timeout=self.default_timeout, + verify=False + ) + + if api_info_response.status_code == 200: + api_info_data = api_info_response.json() + if api_info_data.get("success"): + auth_info = api_info_data.get("data", {}).get("SYNO.API.Auth", {}) + max_version = auth_info.get("maxVersion", 3) + min_version = auth_info.get("minVersion", 1) + auth_path = auth_info.get("path", "auth.cgi") + logger.debug(f"Auth API versions: min={min_version}, max={max_version}, path={auth_path}") + + # Используем максимально поддерживаемую версию, но не выше 6 + auth_version = min(max_version, 6) + else: + logger.warning("Failed to query API info, using default auth version 3") + auth_version = 3 + auth_path = "auth.cgi" + else: + logger.warning(f"API info request failed with status {api_info_response.status_code}, using default auth version 3") + auth_version = 3 + auth_path = "auth.cgi" + except Exception as e: + logger.warning(f"Error querying API info: {str(e)}, using default auth version 3") + auth_version = 3 + auth_path = "auth.cgi" + + # Основной запрос авторизации + url = f"{self.base_url}/{auth_path}" + params = { + "api": "SYNO.API.Auth", + "version": str(auth_version), + "method": "login", + "account": SYNOLOGY_USERNAME, + "passwd": SYNOLOGY_PASSWORD, + "session": "SynologyPowerControlBot", + "format": "cookie" + } + + # Для версии 6+ используем немного другой формат + if auth_version >= 6: + params["enable_syno_token"] = "yes" + + logger.debug(f"Sending auth request to {url} with API version {auth_version}") + start_time = time.time() + response = self.session.get( + url, + params=params, + timeout=self.default_timeout, + verify=False + ) + elapsed_time = time.time() - start_time + logger.debug(f"Auth request completed in {elapsed_time:.2f}s with status code {response.status_code}") + + # Подробная информация для отладки + logger.debug(f"Auth response: {response.text[:500]}") + + # Проверка статуса HTTP + if response.status_code != 200: + logger.error(f"HTTP error: {response.status_code}") + logger.debug(f"Response content: {response.text[:200]}") + return False + + try: + data = response.json() + except json.JSONDecodeError: + logger.error("Failed to decode JSON response") + logger.debug(f"Response content: {response.text[:200]}") + return False + + if data.get("success"): + self.sid = data.get("data", {}).get("sid") + self._last_auth_time = current_time + logger.info("Successfully logged in to Synology NAS") + logger.debug(f"Session ID obtained: {self.sid[:5] if self.sid else 'None'}...") # Показываем только начало SID для безопасности + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to log in to Synology NAS: Error code {error_code}") + + # Дополнительная диагностика + if error_code == 400: + logger.error("Authentication error: Invalid credentials") + elif error_code == 401: + logger.error("Authentication error: Account disabled") + elif error_code == 402: + logger.error("Authentication error: Permission denied") + elif error_code == 403: + logger.error("Authentication error: 2-factor authentication required") + elif error_code == 404: + logger.error("Authentication error: Failed to authenticate with 2-factor authentication") + + return False + + except requests.exceptions.Timeout: + logger.error(f"Connection timeout after {SYNOLOGY_TIMEOUT} seconds") + return False + except requests.exceptions.ConnectionError as e: + logger.error(f"Connection error: {str(e)}") + logger.debug(f"Connection details: {self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}") + return False + except requests.RequestException as e: + logger.error(f"Request error: {str(e)}") + return False + except Exception as e: + logger.error(f"Unexpected error during login: {str(e)}", exc_info=True) + return False + + def logout(self) -> bool: + """Выход из API Synology NAS""" + if not self.sid: + return True + + try: + url = f"{self.base_url}/auth.cgi" + params = { + "api": "SYNO.API.Auth", + "version": "1", + "method": "logout", + "session": "SynologyPowerControlBot", + "_sid": self.sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + self.sid = None + logger.info("Successfully logged out from Synology NAS") + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to log out from Synology NAS: Error code {error_code}") + return False + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + return False + + def _make_api_request(self, api_name: str, method: str, version: int = 1, + params: Optional[Dict[str, Any]] = None, retry_auth: bool = True) -> Optional[Dict[str, Any]]: + """Обобщенный метод для выполнения API запросов с обработкой ошибок""" + # Проверка наличия авторизации + if not self.sid and not self.login(): + logger.error(f"Not authenticated for API request: {api_name}.{method}") + return None + + # Проверка информации API для определения пути и поддерживаемой версии + api_info_url = f"{self.base_url}/entry.cgi" + api_info_params = { + "api": "SYNO.API.Info", + "version": "1", + "method": "query", + "query": api_name + } + + api_path = "entry.cgi" + try: + logger.debug(f"Querying API info for {api_name}") + api_info_response = self.session.get( + api_info_url, + params=api_info_params, + timeout=self.default_timeout, + verify=False + ) + + if api_info_response.status_code == 200: + api_info_data = api_info_response.json() + if api_info_data.get("success"): + api_info = api_info_data.get("data", {}).get(api_name, {}) + if api_info: + max_version = api_info.get("maxVersion", version) + min_version = api_info.get("minVersion", version) + api_path = api_info.get("path", "entry.cgi") + + # Проверка, поддерживается ли запрошенная версия + if version < min_version: + logger.warning(f"API version {version} for {api_name} is below minimum {min_version}, using {min_version}") + version = min_version + elif version > max_version: + logger.warning(f"API version {version} for {api_name} exceeds maximum {max_version}, using {max_version}") + version = max_version + + logger.debug(f"Using API path: {api_path}, version: {version}") + else: + logger.warning(f"API {api_name} not found in API info, using defaults") + except Exception as e: + logger.warning(f"Error querying API info for {api_name}: {str(e)}, using defaults") + + # Подготовка базовых параметров запроса + base_params = { + "api": api_name, + "version": str(version), + "method": method, + "sid": self.sid + } + + # Добавление дополнительных параметров, если они заданы + if params: + base_params.update(params) + + url = f"{self.base_url}/{api_path}" + logger.debug(f"API request: {api_name}.{method} v{version} to {url}") + logger.debug(f"Full request params: {base_params}") + + try: + start_time = time.time() + response = self.session.get( + url, + params=base_params, + timeout=self.default_timeout, + verify=False + ) + elapsed_time = time.time() - start_time + logger.debug(f"API request completed in {elapsed_time:.2f}s with status {response.status_code}") + + # Подробное логирование для отладки + logger.debug(f"Response: {response.text[:500]}") + + # Проверка статуса HTTP + if response.status_code != 200: + logger.error(f"HTTP error: {response.status_code} for {api_name}.{method}") + return None + + try: + data = response.json() + except json.JSONDecodeError: + logger.error(f"Failed to decode JSON response for {api_name}.{method}") + logger.debug(f"Response content: {response.text[:200]}") + return None + + if data.get("success"): + logger.info(f"API request successful for {api_name}.{method}") + return data.get("data", {}) + else: + error_code = data.get("error", {}).get("code", -1) + error_desc = self._get_error_description(error_code) + logger.error(f"API error for {api_name}.{method}: {error_code} - {error_desc}") + + # Ошибки доступа или прав часто встречаются, но они не критичные + # Например, ошибка 102 означает, что нет прав, но NAS доступен + if error_code in [102, 103, 104, 105]: + logger.warning(f"Permission-related error for {api_name}.{method}: {error_code}") + # Возвращаем пустой словарь вместо None, + # чтобы вызывающий код мог понять, что запрос выполнен + return {} + + # Если ошибка связана с авторизацией и нам разрешено повторить попытку + if error_code in [106, 107, 119] and retry_auth: + logger.info("Session may have expired, re-authenticating...") + self.sid = None # Сбрасываем SID + if self.login(): + logger.info("Re-authenticated, retrying API request...") + # Рекурсивный вызов, но без повторной авторизации во избежание зацикливания + return self._make_api_request(api_name, method, version, params, False) + + return None + + except requests.exceptions.Timeout: + logger.error(f"Request timeout for {api_name}.{method}") + return None + except requests.exceptions.ConnectionError as e: + logger.error(f"Connection error for {api_name}.{method}: {str(e)}") + return None + except Exception as e: + logger.error(f"Unexpected error during {api_name}.{method}: {str(e)}") + return None + + def get_system_status(self) -> Dict[str, Any]: + """Получение статуса системы""" + # Проверяем доступность системы + if not self.is_online(): + logger.info("Device is offline, skipping API request") + return {"status": "offline"} + + # Проверяем, есть ли кэшированный результат + cache_key = "system_status" + current_time = time.time() + if cache_key in self._cache and current_time - self._cache_ttl.get(cache_key, 0) < 60: + logger.debug("Using cached system status") + return self._cache[cache_key] + + # Используем рекомендованный API для получения информации о системе + logger.info(f"Getting system info using {SYNOLOGY_INFO_API} v{SYNOLOGY_API_VERSION}") + + # Для SYNO.DSM.Info метод getinfo, для других обычно используется get или info + if SYNOLOGY_INFO_API == "SYNO.DSM.Info": + method = "getinfo" + else: + method = "get" + + result = self._make_api_request(SYNOLOGY_INFO_API, method, version=SYNOLOGY_API_VERSION) + + if result: + logger.info(f"Successfully retrieved system info using {SYNOLOGY_INFO_API}") + + # Формируем расширенный ответ с дополнительной информацией + system_info = { + "status": "online", + "hostname": result.get("hostname", "unknown"), + "model": result.get("model", "unknown"), + "version": result.get("version", "unknown"), + "uptime": result.get("uptime", 0), + "time": current_time, + "is_online": True, + "api_used": SYNOLOGY_INFO_API + } + + # Сохраняем в кэше + self._cache[cache_key] = system_info + self._cache_ttl[cache_key] = current_time + + return system_info + + # Если основной API не сработал, пробуем резервные варианты + logger.warning(f"Failed to retrieve system info with {SYNOLOGY_INFO_API}, trying fallback APIs") + + # Пробуем резервные API + apis_to_try = [ + {"name": "SYNO.DSM.Info", "method": "getinfo", "version": 2}, + {"name": "SYNO.Core.System", "method": "info", "version": 1}, + {"name": "SYNO.Core.System.Status", "method": "get", "version": 1}, + {"name": "SYNO.Core.System.Info", "method": "get", "version": 1}, + ] + + for api in apis_to_try: + if api["name"] == SYNOLOGY_INFO_API: + continue # Пропускаем уже проверенный API + + logger.info(f"Trying to get system info using {api['name']}.{api['method']} v{api['version']}") + result = self._make_api_request(api["name"], api["method"], version=api["version"]) + + if result: + logger.info(f"Successfully retrieved system info using {api['name']}") + + # Формируем расширенный ответ с дополнительной информацией + system_info = { + "status": "online", + "hostname": result.get("hostname", "unknown"), + "model": result.get("model", "unknown"), + "version": result.get("version", "unknown"), + "uptime": result.get("uptime", 0), + "time": current_time, + "is_online": True, + "api_used": api["name"] + } + + # Сохраняем в кэше + self._cache[cache_key] = system_info + self._cache_ttl[cache_key] = current_time + + return system_info + + # Если все запросы не удались, но система онлайн, возвращаем базовую информацию + logger.warning("Failed to retrieve system info with all API methods") + return { + "status": "error", + "error": "Failed to fetch system information", + "is_online": True, + "time": current_time + } + + def shutdown_system(self) -> bool: + """Выключение системы""" + # Проверяем, включено ли устройство перед попыткой его выключить + if not self.is_online(force_check=True): + logger.info("Synology NAS is already offline, no need to shut down") + return True + + logger.info("Attempting to shutdown Synology NAS...") + + # Попробуем сначала использовать предпочтительный API для управления питанием + logger.info(f"Trying shutdown with {SYNOLOGY_POWER_API} v{SYNOLOGY_API_VERSION}") + + # Для SYNO.Core.Hardware.PowerRecovery используется метод setPowerOnState + # Для других API обычно используется метод shutdown или reboot + if SYNOLOGY_POWER_API == "SYNO.Core.Hardware.PowerRecovery": + # Для этого API нужны специальные параметры + params = {"state": "powerbtn"} # powerbtn имитирует нажатие кнопки питания + result = self._make_api_request(SYNOLOGY_POWER_API, "setPowerOnState", version=SYNOLOGY_API_VERSION, params=params) + else: + # Пробуем стандартный метод + result = self._make_api_request(SYNOLOGY_POWER_API, "shutdown", version=SYNOLOGY_API_VERSION) + + if result is not None: + logger.info(f"Successfully initiated system shutdown using {SYNOLOGY_POWER_API}") + + # Даем системе время начать процесс выключения + logger.info("Waiting for shutdown to initialize...") + sleep(5) + + # Проверяем статус + if not self.is_online(force_check=True): + logger.info("System is now offline. Shutdown confirmed successful.") + return True + else: + logger.info("System still appears to be online, but shutdown may be in progress.") + return True + else: + logger.warning(f"Failed to shutdown using {SYNOLOGY_POWER_API}, trying alternative methods") + + # Если не сработал основной метод, пробуем резервные варианты + # Проверка всех доступных методов API для выключения + apis_to_try = [ + {"name": "SYNO.Core.System", "method": "shutdown", "version": 3}, + {"name": "SYNO.Core.System.Power", "method": "shutdown", "version": 1}, + {"name": "SYNO.DSM.Power", "method": "shutdown", "version": 1}, + {"name": "SYNO.System.Power", "method": "shutdown", "version": 1}, + {"name": "SYNO.DSM.System", "method": "shutdown", "version": 1} + ] + + # Проверяем доступные API + try: + api_info_url = f"{self.base_url}/entry.cgi" + api_info_params = { + "api": "SYNO.API.Info", + "version": "1", + "method": "query", + "query": "SYNO.Core.System.Power,SYNO.DSM.Power,SYNO.Core.System,SYNO.System.Power,SYNO.DSM.System" + } + + logger.debug("Checking available shutdown APIs") + api_info_response = self.session.get( + api_info_url, + params=api_info_params, + timeout=self.default_timeout, + verify=False + ) + + if api_info_response.status_code == 200: + api_info_data = api_info_response.json() + if api_info_data.get("success"): + available_apis = api_info_data.get("data", {}) + logger.debug(f"Available APIs: {list(available_apis.keys())}") + + # Фильтруем только доступные API + filtered_apis = [] + for api in apis_to_try: + if api["name"] in available_apis: + api_info = available_apis[api["name"]] + # Проверка версии API + if api_info.get("minVersion", 1) <= api["version"] <= api_info.get("maxVersion", 1): + filtered_apis.append(api) + logger.debug(f"Adding {api['name']} to available shutdown APIs") + else: + logger.debug(f"API {api['name']} version {api['version']} not supported. Supported: {api_info.get('minVersion')}-{api_info.get('maxVersion')}") + + if filtered_apis: + apis_to_try = filtered_apis + else: + logger.warning("No compatible APIs found, trying all methods as fallback") + else: + logger.warning("Failed to query API info, using default methods") + else: + logger.warning(f"API info request failed with status {api_info_response.status_code}, using default methods") + except Exception as e: + logger.warning(f"Error querying API info: {str(e)}, using default methods") + + # Пробуем все доступные методы по порядку + for api in apis_to_try: + logger.info(f"Trying shutdown with {api['name']}.{api['method']} v{api['version']}") + result = self._make_api_request(api["name"], api["method"], version=api["version"]) + + if result is not None: + logger.info(f"Successfully initiated system shutdown using {api['name']}") + + # Даем системе время начать процесс выключения + logger.info("Waiting for shutdown to initialize...") + sleep(5) + + # Проверяем статус + if not self.is_online(force_check=True): + logger.info("System is now offline. Shutdown confirmed successful.") + return True + else: + logger.info("System still appears to be online, but shutdown may be in progress.") + return True + else: + logger.warning(f"Failed to shutdown using {api['name']}.{api['method']} v{api['version']}") + + # Если ни один метод не сработал, но система стала недоступна + if not self.is_online(force_check=True): + logger.info("System appears to be shutting down despite API errors") + return True + + logger.error("Failed to shutdown system after trying multiple APIs") + return False + + def reboot_system(self) -> bool: + """Перезагрузка системы""" + # Проверяем, включена ли система + if not self.is_online(force_check=True): + logger.error("Cannot reboot: System is offline") + return False + + logger.info("Attempting to reboot Synology NAS...") + + # Попробуем сначала использовать предпочтительный API для управления питанием + logger.info(f"Trying reboot with {SYNOLOGY_POWER_API} v{SYNOLOGY_API_VERSION}") + + # Для SYNO.Core.Hardware.PowerRecovery используется метод setPowerOnState или powerButton + # Для других API обычно используется метод restart или reboot + if SYNOLOGY_POWER_API == "SYNO.Core.Hardware.PowerRecovery": + # Для этого API нужны специальные параметры + params = {"reboot": "true"} # Передаем флаг для перезагрузки + result = self._make_api_request(SYNOLOGY_POWER_API, "setPowerOnState", version=SYNOLOGY_API_VERSION, params=params) + else: + # Пробуем стандартный метод + methods_to_try = ["restart", "reboot"] + result = None + for method in methods_to_try: + result = self._make_api_request(SYNOLOGY_POWER_API, method, version=SYNOLOGY_API_VERSION) + if result is not None: + logger.info(f"Successfully used method {method} for {SYNOLOGY_POWER_API}") + break + + if result is not None: + logger.info(f"Successfully initiated system reboot using {SYNOLOGY_POWER_API}") + + # Даем системе время начать процесс перезагрузки + logger.info("Waiting for reboot to initialize...") + sleep(5) + + # Ждем, пока система станет недоступна (признак перезагрузки) + reboot_started = False + for i in range(12): # Проверяем в течение 60 секунд (12 * 5сек) + if not self.is_online(force_check=True): + logger.info(f"System went offline after {i*5} seconds, reboot in progress") + reboot_started = True + break + logger.debug(f"System still online, waiting... ({i+1}/12)") + sleep(5) + + if reboot_started: + # Ждем, пока система снова станет доступна + logger.info("Waiting for system to come back online...") + return self.wait_for_boot(max_attempts=30, delay=10) + else: + logger.warning("System did not go offline after reboot command, but command was accepted") + # Даже если система не ушла оффлайн, команда могла быть принята + return True + else: + logger.warning(f"Failed to reboot using {SYNOLOGY_POWER_API}, trying alternative methods") + + # Если не сработал основной метод, пробуем резервные варианты + # Проверка всех доступных методов API для перезагрузки + apis_to_try = [ + {"name": "SYNO.Core.System.Power", "method": "restart", "version": 1}, + {"name": "SYNO.DSM.Power", "method": "reboot", "version": 1}, + {"name": "SYNO.Core.System", "method": "reboot", "version": 3}, + {"name": "SYNO.System.Power", "method": "reboot", "version": 1}, + {"name": "SYNO.DSM.System", "method": "reboot", "version": 1}, + {"name": "SYNO.Core.Hardware.NeedReboot", "method": "reboot", "version": 1} + ] + + # Получаем список доступных API + try: + api_info_url = f"{self.base_url}/entry.cgi" + api_info_params = { + "api": "SYNO.API.Info", + "version": "1", + "method": "query", + "query": "SYNO.Core.System.Power,SYNO.DSM.Power,SYNO.Core.System,SYNO.System.Power,SYNO.DSM.System,SYNO.Core.Hardware.NeedReboot" + } + + logger.debug("Checking available reboot APIs") + api_info_response = self.session.get( + api_info_url, + params=api_info_params, + timeout=self.default_timeout, + verify=False + ) + + if api_info_response.status_code == 200: + api_info_data = api_info_response.json() + if api_info_data.get("success"): + available_apis = api_info_data.get("data", {}) + logger.debug(f"Available APIs: {list(available_apis.keys())}") + + # Фильтруем только доступные API + filtered_apis = [] + for api in apis_to_try: + if api["name"] in available_apis: + api_info = available_apis[api["name"]] + # Проверка версии API + if api_info.get("minVersion", 1) <= api["version"] <= api_info.get("maxVersion", 1): + filtered_apis.append(api) + logger.debug(f"Adding {api['name']} to available reboot APIs") + else: + logger.debug(f"API {api['name']} version {api['version']} not supported. Supported: {api_info.get('minVersion')}-{api_info.get('maxVersion')}") + + if filtered_apis: + apis_to_try = filtered_apis + else: + logger.warning("No compatible APIs found, trying all methods as fallback") + else: + logger.warning("Failed to query API info, using default methods") + else: + logger.warning(f"API info request failed with status {api_info_response.status_code}, using default methods") + except Exception as e: + logger.warning(f"Error querying API info: {str(e)}, using default methods") + + # Пробуем все доступные методы по порядку + for api in apis_to_try: + logger.info(f"Trying reboot with {api['name']}.{api['method']} v{api['version']}") + result = self._make_api_request(api["name"], api["method"], version=api["version"]) + + if result is not None: + logger.info(f"Successfully initiated system reboot using {api['name']}") + + # Даем системе время начать процесс перезагрузки + logger.info("Waiting for reboot to initialize...") + sleep(5) + + # Ждем, пока система станет недоступна (признак перезагрузки) + reboot_started = False + for i in range(12): # Проверяем в течение 60 секунд (12 * 5сек) + if not self.is_online(force_check=True): + logger.info(f"System went offline after {i*5} seconds, reboot in progress") + reboot_started = True + break + logger.debug(f"System still online, waiting... ({i+1}/12)") + sleep(5) + + if reboot_started: + # Ждем, пока система снова станет доступна + logger.info("Waiting for system to come back online...") + return self.wait_for_boot(max_attempts=30, delay=10) + else: + logger.warning("System did not go offline after reboot command, but command was accepted") + # Даже если система не ушла оффлайн, команда могла быть принята + return True + else: + logger.warning(f"Failed to reboot using {api['name']}.{api['method']} v{api['version']}") + + logger.error("Failed to reboot system after trying multiple APIs") + return False + + def _get_error_description(self, error_code: int) -> str: + """Получение описания ошибки по коду""" + error_descriptions = { + 100: "Unknown error", + 101: "Invalid parameter", + 102: "API does not exist", + 103: "Method does not exist", + 104: "Version does not support", + 105: "Permission denied", + 106: "Session timeout", + 107: "Session interrupted by duplicate login", + 400: "Invalid credentials", + 401: "Account disabled", + 402: "Permission denied", + 403: "2FA required", + 404: "Failed to authenticate with 2FA" + } + return error_descriptions.get(error_code, "Unknown error code") + + def _check_tcp_connection(self) -> bool: + """Проверка базового TCP-соединения с Synology NAS""" + try: + socket_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + socket_obj.settimeout(SYNOLOGY_TIMEOUT) + result = socket_obj.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT)) + socket_obj.close() + + return result == 0 + except socket.error as e: + logger.error(f"Socket error during connection check: {str(e)}") + return False + except Exception as e: + logger.error(f"Unexpected error during connection check: {str(e)}") + return False + + def is_online(self, force_check=False) -> bool: + """Проверка онлайн-статуса Synology NAS""" + # Используем кэшированное значение, если доступно и не устарело + current_time = time.time() + if not force_check and (current_time - self._last_online_check) < self._online_check_interval: + logger.debug(f"Using cached online status: {self._last_online_status}") + return self._last_online_status + + logger.info("Checking if NAS is online...") + + # Проверяем TCP-соединение + online_status = self._check_tcp_connection() + logger.info(f"Detected Synology NAS online status: {online_status}") + + # Если TCP-соединение успешно и у нас есть действующий SID, + # попробуем более детальную проверку через API + if online_status and self.sid: + logger.info("Trying to fetch more detailed online status through API...") + + # Пробуем разные API для проверки онлайн-статуса + api_checks = [ + {"api": "SYNO.DSM.Info", "version": "2", "method": "getinfo"}, + {"api": "SYNO.Core.System", "version": "1", "method": "info"}, + {"api": "SYNO.Core.System.Status", "version": "1", "method": "get"} + ] + + api_success = False + for api_check in api_checks: + try: + url = f"{self.base_url}/entry.cgi" + params = { + "api": api_check["api"], + "version": api_check["version"], + "method": api_check["method"], + "sid": self.sid + } + + logger.debug(f"Trying online status check with {api_check['api']}") + response = self.session.get( + url, + params=params, + timeout=self.default_timeout, + verify=False + ) + + if response.status_code == 200: + data = response.json() + if data.get("success"): + logger.info(f"API request successful for {api_check['api']}") + logger.info("Synology NAS is online with API access") + api_success = True + break + else: + error_code = data.get("error", {}).get("code", -1) + logger.warning(f"API response indicates an error: {error_code}, but NAS is reachable") + else: + logger.warning(f"API returned status code {response.status_code}") + except Exception as e: + logger.warning(f"API check failed for {api_check['api']}, but TCP connection succeeded: {str(e)}") + + if not api_success: + logger.warning("All API checks failed, but TCP connection is successful") + + # Обновляем кэшированное значение + self._last_online_check = current_time + self._last_online_status = online_status + + return online_status + + def wake_on_lan(self) -> bool: + """Отправка Wake-on-LAN пакета для включения Synology NAS""" + if not SYNOLOGY_MAC: + logger.error("MAC address not configured") + return False + + try: + # Преобразование MAC-адреса в байты + mac_address = SYNOLOGY_MAC.replace(':', '').replace('-', '') + if len(mac_address) != 12: + logger.error(f"Invalid MAC address format: {SYNOLOGY_MAC}") + return False + + try: + mac_bytes = bytes.fromhex(mac_address) + except ValueError as e: + logger.error(f"Failed to parse MAC address: {str(e)}") + logger.error(f"MAC address must contain only hex characters: {SYNOLOGY_MAC}") + return False + + # Создание Magic Packet + magic_packet = b'\xff' * 6 + mac_bytes * 16 + + # Отправка пакета на конкретный адрес + try: + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) + sock.sendto(magic_packet, (SYNOLOGY_HOST, WOL_PORT)) + sock.close() + logger.info(f"Wake-on-LAN packet sent to {SYNOLOGY_MAC} at {SYNOLOGY_HOST}:{WOL_PORT}") + except Exception as e: + logger.error(f"Error sending directed WoL packet: {str(e)}") + return False + + # Для надежности отправляем также широковещательный пакет + try: + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) + + # Используем стандартный широковещательный адрес + broadcast_addr = "255.255.255.255" + sock.sendto(magic_packet, (broadcast_addr, WOL_PORT)) + sock.close() + logger.info(f"Wake-on-LAN broadcast packet sent to {broadcast_addr}:{WOL_PORT}") + except Exception as e: + logger.warning(f"Error sending broadcast WoL packet (non-critical): {str(e)}") + # Не считаем ошибкой, т.к. основной пакет уже отправлен + + return True + + except Exception as e: + logger.error(f"Unexpected error in wake_on_lan: {str(e)}") + return False + + def wait_for_boot(self, max_attempts: int = 30, delay: int = 10) -> bool: + """Ожидание загрузки Synology NAS""" + logger.info(f"Waiting for Synology NAS to boot (timeout: {max_attempts * delay}s)...") + + for attempt in range(max_attempts): + # Принудительно проверяем статус без использования кэша + if self.is_online(force_check=True): + logger.info(f"Synology NAS is online after {attempt + 1} attempts ({(attempt + 1) * delay}s)") + + # Проверяем, что не только сеть доступна, но и API загрузился + api_ready = False + logger.info("Waiting for API services to initialize...") + + for api_check in range(5): # Даем еще до 50 секунд для загрузки API + if self.sid or self.login(): + api_ready = True + logger.info(f"API services are ready after {api_check + 1} attempts") + break + logger.debug(f"API not ready yet, waiting... ({api_check + 1}/5)") + sleep(10) + + if not api_ready: + logger.warning("System is online but API services may not be fully initialized") + + # Дадим дополнительное время для полной загрузки всех сервисов + sleep(delay) + return True + + sleep(delay) + logger.info(f"Waiting for boot... Attempt {attempt + 1}/{max_attempts}") + + logger.error(f"Synology NAS didn't come online after {max_attempts * delay} seconds") + return False + + def power_on(self) -> bool: + """Включение Synology NAS""" + # Принудительная проверка статуса + if self.is_online(force_check=True): + logger.info("Synology NAS is already online") + return True + + logger.info("Powering on Synology NAS via Wake-on-LAN...") + + # Проверяем, настроен ли MAC-адрес + if not SYNOLOGY_MAC: + logger.error("Cannot power on: MAC address not configured in settings") + return False + + # Пробуем отправить несколько WoL пакетов для надежности + success = False + for attempt in range(3): + logger.debug(f"Sending WoL packet, attempt {attempt + 1}/3") + if self.wake_on_lan(): + success = True + break + sleep(1) + + if not success: + logger.error("Failed to send Wake-on-LAN packets") + return False + + # Ожидание загрузки + logger.info("WoL packets sent successfully, waiting for system to boot...") + boot_result = self.wait_for_boot(max_attempts=30, delay=10) + + if boot_result: + # Проверяем доступность API после загрузки + system_status = self.get_system_status() + if system_status.get("status") == "online": + logger.info("System booted successfully with API access") + return True + else: + logger.warning("System appears to be online but API may not be fully ready") + return True + else: + logger.error("System did not come online after WoL") + return False + + def power_off(self) -> bool: + """Выключение Synology NAS""" + if not self.is_online(force_check=True): + logger.info("Synology NAS is already offline") + return True + + logger.info("Powering off Synology NAS...") + result = self.shutdown_system() + + if result: + # Дополнительная проверка, что система действительно выключилась + logger.info("Verifying system is offline...") + for attempt in range(12): # Проверяем в течение 2 минут (12 * 10 сек) + if not self.is_online(force_check=True): + logger.info(f"System confirmed offline after {attempt * 10} seconds") + return True + logger.debug(f"System still shutting down, checking again... ({attempt + 1}/12)") + sleep(10) + + logger.warning("System still appears to be online after shutdown command") + return False + else: + logger.error("Failed to initiate shutdown") + return False + + # Заглушки для расширенных методов + def get_shared_folders(self) -> List[Dict[str, Any]]: + """Получение списка общих папок (заглушка)""" + logger.warning("Function get_shared_folders() is not implemented yet") + return [] + + def get_system_load(self) -> Dict[str, Any]: + """Получение информации о загрузке системы (заглушка)""" + logger.warning("Function get_system_load() is not implemented yet") + return {} + + def get_storage_status(self) -> Dict[str, Any]: + """Получение подробной информации о хранилище (заглушка)""" + logger.warning("Function get_storage_status() is not implemented yet") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for storage status request") + return {"error": "authentication_failed"} + + try: + # Здесь будет реализация после добавления соответствующего API + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.Storage.CGI.Storage", + "version": "1", + "method": "load_info", + "sid": self.sid + } + + # В текущей версии просто возвращаем заглушку + return { + "volumes": [], + "disks": [], + "total_size": 0, + "total_used": 0, + "error": "not_implemented" + } + except Exception as e: + logger.error(f"Error in get_storage_status: {str(e)}") + return {"error": str(e)} + + def get_security_status(self) -> Dict[str, Any]: + """Получение информации о состоянии безопасности (заглушка)""" + logger.warning("Function get_security_status() is not implemented yet") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for security status request") + return {"success": False, "error": "authentication_failed"} + + try: + # Здесь будет реализация после добавления соответствующего API + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.Core.SecurityScan.Status", + "version": "1", + "method": "get", + "sid": self.sid + } + + # В текущей версии просто возвращаем заглушку + return { + "success": False, + "status": "not_implemented", + "last_check": None, + "is_secure": False, + "error": "not_implemented" + } + except Exception as e: + logger.error(f"Error in get_security_status: {str(e)}") + return {"success": False, "error": str(e)} diff --git a/.history/src/api/synology_20250830082500.py b/.history/src/api/synology_20250830082500.py new file mode 100644 index 0000000..beeca16 --- /dev/null +++ b/.history/src/api/synology_20250830082500.py @@ -0,0 +1,1097 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Модуль для взаимодействия с API Synology NAS +""" + +import requests +from requests.adapters import HTTPAdapter +import json +import logging +import time +import urllib3 +from urllib3.util import Retry +from typing import Dict, Any, Optional, List +import socket +import struct +from time import sleep + +from src.config.config import ( + SYNOLOGY_HOST, + SYNOLOGY_PORT, + SYNOLOGY_USERNAME, + SYNOLOGY_PASSWORD, + SYNOLOGY_SECURE, + SYNOLOGY_TIMEOUT, + SYNOLOGY_MAC, + WOL_PORT, + SYNOLOGY_API_VERSION, + SYNOLOGY_POWER_API, + SYNOLOGY_INFO_API +) +from src.api.api_discovery import discover_available_apis, find_compatible_api + +# Отключение предупреждений о небезопасных SSL-соединениях +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + +logger = logging.getLogger(__name__) + +class SynologyAPI: + """Класс для взаимодействия с API Synology NAS""" + + def __init__(self): + """Инициализация класса SynologyAPI""" + logger.info("Creating API with auto-retry and connection pool") + logger.debug(f"Connection details: Host={SYNOLOGY_HOST}, Port={SYNOLOGY_PORT}, Secure={SYNOLOGY_SECURE}") + + self.protocol = "https" if SYNOLOGY_SECURE else "http" + self.base_url = f"{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi" + self.sid = None + self.session = requests.Session() + + # Настройка SSL + if self.protocol == "https": + logger.debug("SSL enabled, disabling certificate verification for internal network") + self.session.verify = False # Отключаем проверку SSL для внутренней сети + + # Добавляем повторные попытки для HTTP-запросов + retry_strategy = Retry( + total=5, # Увеличиваем количество попыток + status_forcelist=[429, 500, 502, 503, 504, 404], + allowed_methods=["GET", "POST"], + backoff_factor=1.5, # Увеличиваем задержку между попытками + respect_retry_after_header=True + ) + adapter = HTTPAdapter( + max_retries=retry_strategy, + pool_connections=3, + pool_maxsize=10 + ) + self.session.mount("http://", adapter) + self.session.mount("https://", adapter) + + # Таймауты будут указаны в запросах + self.default_timeout = (SYNOLOGY_TIMEOUT, SYNOLOGY_TIMEOUT*2) + logger.debug(f"Setting default request timeout: {self.default_timeout}") + + # Кэш для хранения результатов запросов + self._cache = {} + self._cache_ttl = {} + self._last_online_check = 0 + self._last_online_status = False + self._online_check_interval = 30 # Интервал для кэширования проверок онлайн-статуса + + # Время последней успешной аутентификации и срок действия сессии + self._last_auth_time = 0 + self._auth_expiry = 3600 # По умолчанию 1 час + + # Информация о доступных API + self._available_apis = {} + self._api_info_ttl = 0 + + def login(self) -> bool: + """Авторизация в API Synology NAS""" + # Проверяем, действительна ли текущая сессия + current_time = time.time() + if self.sid and (current_time - self._last_auth_time) < self._auth_expiry: + logger.debug("Using existing session, still valid") + return True + + logger.info("Attempting to authenticate with Synology NAS...") + logger.debug(f"Base URL: {self.base_url}") + + # Проверяем доступность NAS перед запросом с помощью прямой TCP-проверки + # Избегаем вызова is_online(), чтобы не создавать рекурсию + online_status = self._check_tcp_connection() + if not online_status: + logger.error("Cannot login: Synology NAS is not reachable") + return False + + try: + # Проверка информации API для определения доступных версий API + api_info_url = f"{self.base_url}/entry.cgi" + api_info_params = { + "api": "SYNO.API.Info", + "version": "1", + "method": "query", + "query": "SYNO.API.Auth" + } + + logger.debug("Querying API info to determine optimal auth version") + try: + api_info_response = self.session.get( + api_info_url, + params=api_info_params, + timeout=self.default_timeout, + verify=False + ) + + if api_info_response.status_code == 200: + api_info_data = api_info_response.json() + if api_info_data.get("success"): + auth_info = api_info_data.get("data", {}).get("SYNO.API.Auth", {}) + max_version = auth_info.get("maxVersion", 3) + min_version = auth_info.get("minVersion", 1) + auth_path = auth_info.get("path", "auth.cgi") + logger.debug(f"Auth API versions: min={min_version}, max={max_version}, path={auth_path}") + + # Используем максимально поддерживаемую версию, но не выше 6 + auth_version = min(max_version, 6) + else: + logger.warning("Failed to query API info, using default auth version 3") + auth_version = 3 + auth_path = "auth.cgi" + else: + logger.warning(f"API info request failed with status {api_info_response.status_code}, using default auth version 3") + auth_version = 3 + auth_path = "auth.cgi" + except Exception as e: + logger.warning(f"Error querying API info: {str(e)}, using default auth version 3") + auth_version = 3 + auth_path = "auth.cgi" + + # Основной запрос авторизации + url = f"{self.base_url}/{auth_path}" + params = { + "api": "SYNO.API.Auth", + "version": str(auth_version), + "method": "login", + "account": SYNOLOGY_USERNAME, + "passwd": SYNOLOGY_PASSWORD, + "session": "SynologyPowerControlBot", + "format": "cookie" + } + + # Для версии 6+ используем немного другой формат + if auth_version >= 6: + params["enable_syno_token"] = "yes" + + logger.debug(f"Sending auth request to {url} with API version {auth_version}") + start_time = time.time() + response = self.session.get( + url, + params=params, + timeout=self.default_timeout, + verify=False + ) + elapsed_time = time.time() - start_time + logger.debug(f"Auth request completed in {elapsed_time:.2f}s with status code {response.status_code}") + + # Подробная информация для отладки + logger.debug(f"Auth response: {response.text[:500]}") + + # Проверка статуса HTTP + if response.status_code != 200: + logger.error(f"HTTP error: {response.status_code}") + logger.debug(f"Response content: {response.text[:200]}") + return False + + try: + data = response.json() + except json.JSONDecodeError: + logger.error("Failed to decode JSON response") + logger.debug(f"Response content: {response.text[:200]}") + return False + + if data.get("success"): + self.sid = data.get("data", {}).get("sid") + self._last_auth_time = current_time + logger.info("Successfully logged in to Synology NAS") + logger.debug(f"Session ID obtained: {self.sid[:5] if self.sid else 'None'}...") # Показываем только начало SID для безопасности + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to log in to Synology NAS: Error code {error_code}") + + # Дополнительная диагностика + if error_code == 400: + logger.error("Authentication error: Invalid credentials") + elif error_code == 401: + logger.error("Authentication error: Account disabled") + elif error_code == 402: + logger.error("Authentication error: Permission denied") + elif error_code == 403: + logger.error("Authentication error: 2-factor authentication required") + elif error_code == 404: + logger.error("Authentication error: Failed to authenticate with 2-factor authentication") + + return False + + except requests.exceptions.Timeout: + logger.error(f"Connection timeout after {SYNOLOGY_TIMEOUT} seconds") + return False + except requests.exceptions.ConnectionError as e: + logger.error(f"Connection error: {str(e)}") + logger.debug(f"Connection details: {self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}") + return False + except requests.RequestException as e: + logger.error(f"Request error: {str(e)}") + return False + except Exception as e: + logger.error(f"Unexpected error during login: {str(e)}", exc_info=True) + return False + + def logout(self) -> bool: + """Выход из API Synology NAS""" + if not self.sid: + return True + + try: + url = f"{self.base_url}/auth.cgi" + params = { + "api": "SYNO.API.Auth", + "version": "1", + "method": "logout", + "session": "SynologyPowerControlBot", + "_sid": self.sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + self.sid = None + logger.info("Successfully logged out from Synology NAS") + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to log out from Synology NAS: Error code {error_code}") + return False + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + return False + + def _make_api_request(self, api_name: str, method: str, version: int = 1, + params: Optional[Dict[str, Any]] = None, retry_auth: bool = True) -> Optional[Dict[str, Any]]: + """Обобщенный метод для выполнения API запросов с обработкой ошибок""" + # Проверка наличия авторизации + if not self.sid and not self.login(): + logger.error(f"Not authenticated for API request: {api_name}.{method}") + return None + + # Проверка информации API для определения пути и поддерживаемой версии + api_info_url = f"{self.base_url}/entry.cgi" + api_info_params = { + "api": "SYNO.API.Info", + "version": "1", + "method": "query", + "query": api_name + } + + api_path = "entry.cgi" + try: + logger.debug(f"Querying API info for {api_name}") + api_info_response = self.session.get( + api_info_url, + params=api_info_params, + timeout=self.default_timeout, + verify=False + ) + + if api_info_response.status_code == 200: + api_info_data = api_info_response.json() + if api_info_data.get("success"): + api_info = api_info_data.get("data", {}).get(api_name, {}) + if api_info: + max_version = api_info.get("maxVersion", version) + min_version = api_info.get("minVersion", version) + api_path = api_info.get("path", "entry.cgi") + + # Проверка, поддерживается ли запрошенная версия + if version < min_version: + logger.warning(f"API version {version} for {api_name} is below minimum {min_version}, using {min_version}") + version = min_version + elif version > max_version: + logger.warning(f"API version {version} for {api_name} exceeds maximum {max_version}, using {max_version}") + version = max_version + + logger.debug(f"Using API path: {api_path}, version: {version}") + else: + logger.warning(f"API {api_name} not found in API info, using defaults") + except Exception as e: + logger.warning(f"Error querying API info for {api_name}: {str(e)}, using defaults") + + # Подготовка базовых параметров запроса + base_params = { + "api": api_name, + "version": str(version), + "method": method, + "sid": self.sid + } + + # Добавление дополнительных параметров, если они заданы + if params: + base_params.update(params) + + url = f"{self.base_url}/{api_path}" + logger.debug(f"API request: {api_name}.{method} v{version} to {url}") + logger.debug(f"Full request params: {base_params}") + + try: + start_time = time.time() + response = self.session.get( + url, + params=base_params, + timeout=self.default_timeout, + verify=False + ) + elapsed_time = time.time() - start_time + logger.debug(f"API request completed in {elapsed_time:.2f}s with status {response.status_code}") + + # Подробное логирование для отладки + logger.debug(f"Response: {response.text[:500]}") + + # Проверка статуса HTTP + if response.status_code != 200: + logger.error(f"HTTP error: {response.status_code} for {api_name}.{method}") + return None + + try: + data = response.json() + except json.JSONDecodeError: + logger.error(f"Failed to decode JSON response for {api_name}.{method}") + logger.debug(f"Response content: {response.text[:200]}") + return None + + if data.get("success"): + logger.info(f"API request successful for {api_name}.{method}") + return data.get("data", {}) + else: + error_code = data.get("error", {}).get("code", -1) + error_desc = self._get_error_description(error_code) + logger.error(f"API error for {api_name}.{method}: {error_code} - {error_desc}") + + # Ошибки доступа или прав часто встречаются, но они не критичные + # Например, ошибка 102 означает, что нет прав, но NAS доступен + if error_code in [102, 103, 104, 105]: + logger.warning(f"Permission-related error for {api_name}.{method}: {error_code}") + # Возвращаем пустой словарь вместо None, + # чтобы вызывающий код мог понять, что запрос выполнен + return {} + + # Если ошибка связана с авторизацией и нам разрешено повторить попытку + if error_code in [106, 107, 119] and retry_auth: + logger.info("Session may have expired, re-authenticating...") + self.sid = None # Сбрасываем SID + if self.login(): + logger.info("Re-authenticated, retrying API request...") + # Рекурсивный вызов, но без повторной авторизации во избежание зацикливания + return self._make_api_request(api_name, method, version, params, False) + + return None + + except requests.exceptions.Timeout: + logger.error(f"Request timeout for {api_name}.{method}") + return None + except requests.exceptions.ConnectionError as e: + logger.error(f"Connection error for {api_name}.{method}: {str(e)}") + return None + except Exception as e: + logger.error(f"Unexpected error during {api_name}.{method}: {str(e)}") + return None + + def get_system_status(self) -> Dict[str, Any]: + """Получение статуса системы""" + # Проверяем доступность системы + if not self.is_online(): + logger.info("Device is offline, skipping API request") + return {"status": "offline"} + + # Проверяем, есть ли кэшированный результат + cache_key = "system_status" + current_time = time.time() + if cache_key in self._cache and current_time - self._cache_ttl.get(cache_key, 0) < 60: + logger.debug("Using cached system status") + return self._cache[cache_key] + + # Используем рекомендованный API для получения информации о системе + logger.info(f"Getting system info using {SYNOLOGY_INFO_API} v{SYNOLOGY_API_VERSION}") + + # Для SYNO.DSM.Info метод getinfo, для других обычно используется get или info + if SYNOLOGY_INFO_API == "SYNO.DSM.Info": + method = "getinfo" + else: + method = "get" + + result = self._make_api_request(SYNOLOGY_INFO_API, method, version=SYNOLOGY_API_VERSION) + + if result: + logger.info(f"Successfully retrieved system info using {SYNOLOGY_INFO_API}") + + # Формируем расширенный ответ с дополнительной информацией + system_info = { + "status": "online", + "hostname": result.get("hostname", "unknown"), + "model": result.get("model", "unknown"), + "version": result.get("version", "unknown"), + "uptime": result.get("uptime", 0), + "time": current_time, + "is_online": True, + "api_used": SYNOLOGY_INFO_API + } + + # Сохраняем в кэше + self._cache[cache_key] = system_info + self._cache_ttl[cache_key] = current_time + + return system_info + + # Если основной API не сработал, пробуем резервные варианты + logger.warning(f"Failed to retrieve system info with {SYNOLOGY_INFO_API}, trying fallback APIs") + + # Пробуем резервные API + apis_to_try = [ + {"name": "SYNO.DSM.Info", "method": "getinfo", "version": 2}, + {"name": "SYNO.Core.System", "method": "info", "version": 1}, + {"name": "SYNO.Core.System.Status", "method": "get", "version": 1}, + {"name": "SYNO.Core.System.Info", "method": "get", "version": 1}, + ] + + for api in apis_to_try: + if api["name"] == SYNOLOGY_INFO_API: + continue # Пропускаем уже проверенный API + + logger.info(f"Trying to get system info using {api['name']}.{api['method']} v{api['version']}") + result = self._make_api_request(api["name"], api["method"], version=api["version"]) + + if result: + logger.info(f"Successfully retrieved system info using {api['name']}") + + # Формируем расширенный ответ с дополнительной информацией + system_info = { + "status": "online", + "hostname": result.get("hostname", "unknown"), + "model": result.get("model", "unknown"), + "version": result.get("version", "unknown"), + "uptime": result.get("uptime", 0), + "time": current_time, + "is_online": True, + "api_used": api["name"] + } + + # Сохраняем в кэше + self._cache[cache_key] = system_info + self._cache_ttl[cache_key] = current_time + + return system_info + + # Если все запросы не удались, но система онлайн, возвращаем базовую информацию + logger.warning("Failed to retrieve system info with all API methods") + return { + "status": "error", + "error": "Failed to fetch system information", + "is_online": True, + "time": current_time + } + + def shutdown_system(self) -> bool: + """Выключение системы""" + # Проверяем, включено ли устройство перед попыткой его выключить + if not self.is_online(force_check=True): + logger.info("Synology NAS is already offline, no need to shut down") + return True + + logger.info("Attempting to shutdown Synology NAS...") + + # Попробуем сначала использовать предпочтительный API для управления питанием + logger.info(f"Trying shutdown with {SYNOLOGY_POWER_API} v{SYNOLOGY_API_VERSION}") + + # Для SYNO.Core.Hardware.PowerRecovery используется метод setPowerOnState + # Для других API обычно используется метод shutdown или reboot + if SYNOLOGY_POWER_API == "SYNO.Core.Hardware.PowerRecovery": + # Для этого API нужны специальные параметры + params = {"state": "powerbtn"} # powerbtn имитирует нажатие кнопки питания + result = self._make_api_request(SYNOLOGY_POWER_API, "setPowerOnState", version=SYNOLOGY_API_VERSION, params=params) + else: + # Пробуем стандартный метод + result = self._make_api_request(SYNOLOGY_POWER_API, "shutdown", version=SYNOLOGY_API_VERSION) + + if result is not None: + logger.info(f"Successfully initiated system shutdown using {SYNOLOGY_POWER_API}") + + # Даем системе время начать процесс выключения + logger.info("Waiting for shutdown to initialize...") + sleep(5) + + # Проверяем статус + if not self.is_online(force_check=True): + logger.info("System is now offline. Shutdown confirmed successful.") + return True + else: + logger.info("System still appears to be online, but shutdown may be in progress.") + return True + else: + logger.warning(f"Failed to shutdown using {SYNOLOGY_POWER_API}, trying alternative methods") + + # Если не сработал основной метод, пробуем резервные варианты + # Проверка всех доступных методов API для выключения + apis_to_try = [ + {"name": "SYNO.Core.System", "method": "shutdown", "version": 3}, + {"name": "SYNO.Core.System.Power", "method": "shutdown", "version": 1}, + {"name": "SYNO.DSM.Power", "method": "shutdown", "version": 1}, + {"name": "SYNO.System.Power", "method": "shutdown", "version": 1}, + {"name": "SYNO.DSM.System", "method": "shutdown", "version": 1} + ] + + # Проверяем доступные API + try: + api_info_url = f"{self.base_url}/entry.cgi" + api_info_params = { + "api": "SYNO.API.Info", + "version": "1", + "method": "query", + "query": "SYNO.Core.System.Power,SYNO.DSM.Power,SYNO.Core.System,SYNO.System.Power,SYNO.DSM.System" + } + + logger.debug("Checking available shutdown APIs") + api_info_response = self.session.get( + api_info_url, + params=api_info_params, + timeout=self.default_timeout, + verify=False + ) + + if api_info_response.status_code == 200: + api_info_data = api_info_response.json() + if api_info_data.get("success"): + available_apis = api_info_data.get("data", {}) + logger.debug(f"Available APIs: {list(available_apis.keys())}") + + # Фильтруем только доступные API + filtered_apis = [] + for api in apis_to_try: + if api["name"] in available_apis: + api_info = available_apis[api["name"]] + # Проверка версии API + if api_info.get("minVersion", 1) <= api["version"] <= api_info.get("maxVersion", 1): + filtered_apis.append(api) + logger.debug(f"Adding {api['name']} to available shutdown APIs") + else: + logger.debug(f"API {api['name']} version {api['version']} not supported. Supported: {api_info.get('minVersion')}-{api_info.get('maxVersion')}") + + if filtered_apis: + apis_to_try = filtered_apis + else: + logger.warning("No compatible APIs found, trying all methods as fallback") + else: + logger.warning("Failed to query API info, using default methods") + else: + logger.warning(f"API info request failed with status {api_info_response.status_code}, using default methods") + except Exception as e: + logger.warning(f"Error querying API info: {str(e)}, using default methods") + + # Пробуем все доступные методы по порядку + for api in apis_to_try: + logger.info(f"Trying shutdown with {api['name']}.{api['method']} v{api['version']}") + result = self._make_api_request(api["name"], api["method"], version=api["version"]) + + if result is not None: + logger.info(f"Successfully initiated system shutdown using {api['name']}") + + # Даем системе время начать процесс выключения + logger.info("Waiting for shutdown to initialize...") + sleep(5) + + # Проверяем статус + if not self.is_online(force_check=True): + logger.info("System is now offline. Shutdown confirmed successful.") + return True + else: + logger.info("System still appears to be online, but shutdown may be in progress.") + return True + else: + logger.warning(f"Failed to shutdown using {api['name']}.{api['method']} v{api['version']}") + + # Если ни один метод не сработал, но система стала недоступна + if not self.is_online(force_check=True): + logger.info("System appears to be shutting down despite API errors") + return True + + logger.error("Failed to shutdown system after trying multiple APIs") + return False + + def reboot_system(self) -> bool: + """Перезагрузка системы""" + # Проверяем, включена ли система + if not self.is_online(force_check=True): + logger.error("Cannot reboot: System is offline") + return False + + logger.info("Attempting to reboot Synology NAS...") + + # Попробуем сначала использовать предпочтительный API для управления питанием + logger.info(f"Trying reboot with {SYNOLOGY_POWER_API} v{SYNOLOGY_API_VERSION}") + + # Для SYNO.Core.Hardware.PowerRecovery используется метод setPowerOnState или powerButton + # Для других API обычно используется метод restart или reboot + if SYNOLOGY_POWER_API == "SYNO.Core.Hardware.PowerRecovery": + # Для этого API нужны специальные параметры + params = {"reboot": "true"} # Передаем флаг для перезагрузки + result = self._make_api_request(SYNOLOGY_POWER_API, "setPowerOnState", version=SYNOLOGY_API_VERSION, params=params) + else: + # Пробуем стандартный метод + methods_to_try = ["restart", "reboot"] + result = None + for method in methods_to_try: + result = self._make_api_request(SYNOLOGY_POWER_API, method, version=SYNOLOGY_API_VERSION) + if result is not None: + logger.info(f"Successfully used method {method} for {SYNOLOGY_POWER_API}") + break + + if result is not None: + logger.info(f"Successfully initiated system reboot using {SYNOLOGY_POWER_API}") + + # Даем системе время начать процесс перезагрузки + logger.info("Waiting for reboot to initialize...") + sleep(5) + + # Ждем, пока система станет недоступна (признак перезагрузки) + reboot_started = False + for i in range(12): # Проверяем в течение 60 секунд (12 * 5сек) + if not self.is_online(force_check=True): + logger.info(f"System went offline after {i*5} seconds, reboot in progress") + reboot_started = True + break + logger.debug(f"System still online, waiting... ({i+1}/12)") + sleep(5) + + if reboot_started: + # Ждем, пока система снова станет доступна + logger.info("Waiting for system to come back online...") + return self.wait_for_boot(max_attempts=30, delay=10) + else: + logger.warning("System did not go offline after reboot command, but command was accepted") + # Даже если система не ушла оффлайн, команда могла быть принята + return True + else: + logger.warning(f"Failed to reboot using {SYNOLOGY_POWER_API}, trying alternative methods") + + # Если не сработал основной метод, пробуем резервные варианты + # Проверка всех доступных методов API для перезагрузки + apis_to_try = [ + {"name": "SYNO.Core.System.Power", "method": "restart", "version": 1}, + {"name": "SYNO.DSM.Power", "method": "reboot", "version": 1}, + {"name": "SYNO.Core.System", "method": "reboot", "version": 3}, + {"name": "SYNO.System.Power", "method": "reboot", "version": 1}, + {"name": "SYNO.DSM.System", "method": "reboot", "version": 1}, + {"name": "SYNO.Core.Hardware.NeedReboot", "method": "reboot", "version": 1} + ] + + # Получаем список доступных API + try: + api_info_url = f"{self.base_url}/entry.cgi" + api_info_params = { + "api": "SYNO.API.Info", + "version": "1", + "method": "query", + "query": "SYNO.Core.System.Power,SYNO.DSM.Power,SYNO.Core.System,SYNO.System.Power,SYNO.DSM.System,SYNO.Core.Hardware.NeedReboot" + } + + logger.debug("Checking available reboot APIs") + api_info_response = self.session.get( + api_info_url, + params=api_info_params, + timeout=self.default_timeout, + verify=False + ) + + if api_info_response.status_code == 200: + api_info_data = api_info_response.json() + if api_info_data.get("success"): + available_apis = api_info_data.get("data", {}) + logger.debug(f"Available APIs: {list(available_apis.keys())}") + + # Фильтруем только доступные API + filtered_apis = [] + for api in apis_to_try: + if api["name"] in available_apis: + api_info = available_apis[api["name"]] + # Проверка версии API + if api_info.get("minVersion", 1) <= api["version"] <= api_info.get("maxVersion", 1): + filtered_apis.append(api) + logger.debug(f"Adding {api['name']} to available reboot APIs") + else: + logger.debug(f"API {api['name']} version {api['version']} not supported. Supported: {api_info.get('minVersion')}-{api_info.get('maxVersion')}") + + if filtered_apis: + apis_to_try = filtered_apis + else: + logger.warning("No compatible APIs found, trying all methods as fallback") + else: + logger.warning("Failed to query API info, using default methods") + else: + logger.warning(f"API info request failed with status {api_info_response.status_code}, using default methods") + except Exception as e: + logger.warning(f"Error querying API info: {str(e)}, using default methods") + + # Пробуем все доступные методы по порядку + for api in apis_to_try: + logger.info(f"Trying reboot with {api['name']}.{api['method']} v{api['version']}") + result = self._make_api_request(api["name"], api["method"], version=api["version"]) + + if result is not None: + logger.info(f"Successfully initiated system reboot using {api['name']}") + + # Даем системе время начать процесс перезагрузки + logger.info("Waiting for reboot to initialize...") + sleep(5) + + # Ждем, пока система станет недоступна (признак перезагрузки) + reboot_started = False + for i in range(12): # Проверяем в течение 60 секунд (12 * 5сек) + if not self.is_online(force_check=True): + logger.info(f"System went offline after {i*5} seconds, reboot in progress") + reboot_started = True + break + logger.debug(f"System still online, waiting... ({i+1}/12)") + sleep(5) + + if reboot_started: + # Ждем, пока система снова станет доступна + logger.info("Waiting for system to come back online...") + return self.wait_for_boot(max_attempts=30, delay=10) + else: + logger.warning("System did not go offline after reboot command, but command was accepted") + # Даже если система не ушла оффлайн, команда могла быть принята + return True + else: + logger.warning(f"Failed to reboot using {api['name']}.{api['method']} v{api['version']}") + + logger.error("Failed to reboot system after trying multiple APIs") + return False + + def _get_error_description(self, error_code: int) -> str: + """Получение описания ошибки по коду""" + error_descriptions = { + 100: "Unknown error", + 101: "Invalid parameter", + 102: "API does not exist", + 103: "Method does not exist", + 104: "Version does not support", + 105: "Permission denied", + 106: "Session timeout", + 107: "Session interrupted by duplicate login", + 400: "Invalid credentials", + 401: "Account disabled", + 402: "Permission denied", + 403: "2FA required", + 404: "Failed to authenticate with 2FA" + } + return error_descriptions.get(error_code, "Unknown error code") + + def _check_tcp_connection(self) -> bool: + """Проверка базового TCP-соединения с Synology NAS""" + try: + socket_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + socket_obj.settimeout(SYNOLOGY_TIMEOUT) + result = socket_obj.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT)) + socket_obj.close() + + return result == 0 + except socket.error as e: + logger.error(f"Socket error during connection check: {str(e)}") + return False + except Exception as e: + logger.error(f"Unexpected error during connection check: {str(e)}") + return False + + def is_online(self, force_check=False) -> bool: + """Проверка онлайн-статуса Synology NAS""" + # Используем кэшированное значение, если доступно и не устарело + current_time = time.time() + if not force_check and (current_time - self._last_online_check) < self._online_check_interval: + logger.debug(f"Using cached online status: {self._last_online_status}") + return self._last_online_status + + logger.info("Checking if NAS is online...") + + # Проверяем TCP-соединение + online_status = self._check_tcp_connection() + logger.info(f"Detected Synology NAS online status: {online_status}") + + # Если TCP-соединение успешно и у нас есть действующий SID, + # попробуем более детальную проверку через API + if online_status and self.sid: + logger.info("Trying to fetch more detailed online status through API...") + + # Пробуем разные API для проверки онлайн-статуса + api_checks = [ + {"api": "SYNO.DSM.Info", "version": "2", "method": "getinfo"}, + {"api": "SYNO.Core.System", "version": "1", "method": "info"}, + {"api": "SYNO.Core.System.Status", "version": "1", "method": "get"} + ] + + api_success = False + for api_check in api_checks: + try: + url = f"{self.base_url}/entry.cgi" + params = { + "api": api_check["api"], + "version": api_check["version"], + "method": api_check["method"], + "sid": self.sid + } + + logger.debug(f"Trying online status check with {api_check['api']}") + response = self.session.get( + url, + params=params, + timeout=self.default_timeout, + verify=False + ) + + if response.status_code == 200: + data = response.json() + if data.get("success"): + logger.info(f"API request successful for {api_check['api']}") + logger.info("Synology NAS is online with API access") + api_success = True + break + else: + error_code = data.get("error", {}).get("code", -1) + logger.warning(f"API response indicates an error: {error_code}, but NAS is reachable") + else: + logger.warning(f"API returned status code {response.status_code}") + except Exception as e: + logger.warning(f"API check failed for {api_check['api']}, but TCP connection succeeded: {str(e)}") + + if not api_success: + logger.warning("All API checks failed, but TCP connection is successful") + + # Обновляем кэшированное значение + self._last_online_check = current_time + self._last_online_status = online_status + + return online_status + + def wake_on_lan(self) -> bool: + """Отправка Wake-on-LAN пакета для включения Synology NAS""" + if not SYNOLOGY_MAC: + logger.error("MAC address not configured") + return False + + try: + # Преобразование MAC-адреса в байты + mac_address = SYNOLOGY_MAC.replace(':', '').replace('-', '') + if len(mac_address) != 12: + logger.error(f"Invalid MAC address format: {SYNOLOGY_MAC}") + return False + + try: + mac_bytes = bytes.fromhex(mac_address) + except ValueError as e: + logger.error(f"Failed to parse MAC address: {str(e)}") + logger.error(f"MAC address must contain only hex characters: {SYNOLOGY_MAC}") + return False + + # Создание Magic Packet + magic_packet = b'\xff' * 6 + mac_bytes * 16 + + # Отправка пакета на конкретный адрес + try: + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) + sock.sendto(magic_packet, (SYNOLOGY_HOST, WOL_PORT)) + sock.close() + logger.info(f"Wake-on-LAN packet sent to {SYNOLOGY_MAC} at {SYNOLOGY_HOST}:{WOL_PORT}") + except Exception as e: + logger.error(f"Error sending directed WoL packet: {str(e)}") + return False + + # Для надежности отправляем также широковещательный пакет + try: + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) + + # Используем стандартный широковещательный адрес + broadcast_addr = "255.255.255.255" + sock.sendto(magic_packet, (broadcast_addr, WOL_PORT)) + sock.close() + logger.info(f"Wake-on-LAN broadcast packet sent to {broadcast_addr}:{WOL_PORT}") + except Exception as e: + logger.warning(f"Error sending broadcast WoL packet (non-critical): {str(e)}") + # Не считаем ошибкой, т.к. основной пакет уже отправлен + + return True + + except Exception as e: + logger.error(f"Unexpected error in wake_on_lan: {str(e)}") + return False + + def wait_for_boot(self, max_attempts: int = 30, delay: int = 10) -> bool: + """Ожидание загрузки Synology NAS""" + logger.info(f"Waiting for Synology NAS to boot (timeout: {max_attempts * delay}s)...") + + for attempt in range(max_attempts): + # Принудительно проверяем статус без использования кэша + if self.is_online(force_check=True): + logger.info(f"Synology NAS is online after {attempt + 1} attempts ({(attempt + 1) * delay}s)") + + # Проверяем, что не только сеть доступна, но и API загрузился + api_ready = False + logger.info("Waiting for API services to initialize...") + + for api_check in range(5): # Даем еще до 50 секунд для загрузки API + if self.sid or self.login(): + api_ready = True + logger.info(f"API services are ready after {api_check + 1} attempts") + break + logger.debug(f"API not ready yet, waiting... ({api_check + 1}/5)") + sleep(10) + + if not api_ready: + logger.warning("System is online but API services may not be fully initialized") + + # Дадим дополнительное время для полной загрузки всех сервисов + sleep(delay) + return True + + sleep(delay) + logger.info(f"Waiting for boot... Attempt {attempt + 1}/{max_attempts}") + + logger.error(f"Synology NAS didn't come online after {max_attempts * delay} seconds") + return False + + def power_on(self) -> bool: + """Включение Synology NAS""" + # Принудительная проверка статуса + if self.is_online(force_check=True): + logger.info("Synology NAS is already online") + return True + + logger.info("Powering on Synology NAS via Wake-on-LAN...") + + # Проверяем, настроен ли MAC-адрес + if not SYNOLOGY_MAC: + logger.error("Cannot power on: MAC address not configured in settings") + return False + + # Пробуем отправить несколько WoL пакетов для надежности + success = False + for attempt in range(3): + logger.debug(f"Sending WoL packet, attempt {attempt + 1}/3") + if self.wake_on_lan(): + success = True + break + sleep(1) + + if not success: + logger.error("Failed to send Wake-on-LAN packets") + return False + + # Ожидание загрузки + logger.info("WoL packets sent successfully, waiting for system to boot...") + boot_result = self.wait_for_boot(max_attempts=30, delay=10) + + if boot_result: + # Проверяем доступность API после загрузки + system_status = self.get_system_status() + if system_status.get("status") == "online": + logger.info("System booted successfully with API access") + return True + else: + logger.warning("System appears to be online but API may not be fully ready") + return True + else: + logger.error("System did not come online after WoL") + return False + + def power_off(self) -> bool: + """Выключение Synology NAS""" + if not self.is_online(force_check=True): + logger.info("Synology NAS is already offline") + return True + + logger.info("Powering off Synology NAS...") + result = self.shutdown_system() + + if result: + # Дополнительная проверка, что система действительно выключилась + logger.info("Verifying system is offline...") + for attempt in range(12): # Проверяем в течение 2 минут (12 * 10 сек) + if not self.is_online(force_check=True): + logger.info(f"System confirmed offline after {attempt * 10} seconds") + return True + logger.debug(f"System still shutting down, checking again... ({attempt + 1}/12)") + sleep(10) + + logger.warning("System still appears to be online after shutdown command") + return False + else: + logger.error("Failed to initiate shutdown") + return False + + # Заглушки для расширенных методов + def get_shared_folders(self) -> List[Dict[str, Any]]: + """Получение списка общих папок (заглушка)""" + logger.warning("Function get_shared_folders() is not implemented yet") + return [] + + def get_system_load(self) -> Dict[str, Any]: + """Получение информации о загрузке системы (заглушка)""" + logger.warning("Function get_system_load() is not implemented yet") + return {} + + def get_storage_status(self) -> Dict[str, Any]: + """Получение подробной информации о хранилище (заглушка)""" + logger.warning("Function get_storage_status() is not implemented yet") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for storage status request") + return {"error": "authentication_failed"} + + try: + # Здесь будет реализация после добавления соответствующего API + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.Storage.CGI.Storage", + "version": "1", + "method": "load_info", + "sid": self.sid + } + + # В текущей версии просто возвращаем заглушку + return { + "volumes": [], + "disks": [], + "total_size": 0, + "total_used": 0, + "error": "not_implemented" + } + except Exception as e: + logger.error(f"Error in get_storage_status: {str(e)}") + return {"error": str(e)} + + def get_security_status(self) -> Dict[str, Any]: + """Получение информации о состоянии безопасности (заглушка)""" + logger.warning("Function get_security_status() is not implemented yet") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for security status request") + return {"success": False, "error": "authentication_failed"} + + try: + # Здесь будет реализация после добавления соответствующего API + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.Core.SecurityScan.Status", + "version": "1", + "method": "get", + "sid": self.sid + } + + # В текущей версии просто возвращаем заглушку + return { + "success": False, + "status": "not_implemented", + "last_check": None, + "is_secure": False, + "error": "not_implemented" + } + except Exception as e: + logger.error(f"Error in get_security_status: {str(e)}") + return {"success": False, "error": str(e)} diff --git a/.history/src/api/synology_20250830082853.py b/.history/src/api/synology_20250830082853.py new file mode 100644 index 0000000..2964a3e --- /dev/null +++ b/.history/src/api/synology_20250830082853.py @@ -0,0 +1,1150 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Модуль для взаимодействия с API Synology NAS +""" + +import requests +from requests.adapters import HTTPAdapter +import json +import logging +import time +import urllib3 +from urllib3.util import Retry +from typing import Dict, Any, Optional, List +import socket +import struct +from time import sleep + +from src.config.config import ( + SYNOLOGY_HOST, + SYNOLOGY_PORT, + SYNOLOGY_USERNAME, + SYNOLOGY_PASSWORD, + SYNOLOGY_SECURE, + SYNOLOGY_TIMEOUT, + SYNOLOGY_MAC, + WOL_PORT, + SYNOLOGY_API_VERSION, + SYNOLOGY_POWER_API, + SYNOLOGY_INFO_API +) +from src.api.api_discovery import discover_available_apis, find_compatible_api + +# Отключение предупреждений о небезопасных SSL-соединениях +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + +logger = logging.getLogger(__name__) + +class SynologyAPI: + """Класс для взаимодействия с API Synology NAS""" + + def __init__(self): + """Инициализация класса SynologyAPI""" + logger.info("Creating API with auto-retry and connection pool") + logger.debug(f"Connection details: Host={SYNOLOGY_HOST}, Port={SYNOLOGY_PORT}, Secure={SYNOLOGY_SECURE}") + + self.protocol = "https" if SYNOLOGY_SECURE else "http" + self.base_url = f"{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi" + self.sid = None + self.session = requests.Session() + + # Настройка SSL + if self.protocol == "https": + logger.debug("SSL enabled, disabling certificate verification for internal network") + self.session.verify = False # Отключаем проверку SSL для внутренней сети + + # Добавляем повторные попытки для HTTP-запросов + retry_strategy = Retry( + total=5, # Увеличиваем количество попыток + status_forcelist=[429, 500, 502, 503, 504, 404], + allowed_methods=["GET", "POST"], + backoff_factor=1.5, # Увеличиваем задержку между попытками + respect_retry_after_header=True + ) + adapter = HTTPAdapter( + max_retries=retry_strategy, + pool_connections=3, + pool_maxsize=10 + ) + self.session.mount("http://", adapter) + self.session.mount("https://", adapter) + + # Таймауты будут указаны в запросах + self.default_timeout = (SYNOLOGY_TIMEOUT, SYNOLOGY_TIMEOUT*2) + logger.debug(f"Setting default request timeout: {self.default_timeout}") + + # Кэш для хранения результатов запросов + self._cache = {} + self._cache_ttl = {} + self._last_online_check = 0 + self._last_online_status = False + self._online_check_interval = 30 # Интервал для кэширования проверок онлайн-статуса + + # Время последней успешной аутентификации и срок действия сессии + self._last_auth_time = 0 + self._auth_expiry = 3600 # По умолчанию 1 час + + # Информация о доступных API + self._available_apis = {} + self._api_info_ttl = 0 + + def login(self) -> bool: + """Авторизация в API Synology NAS""" + # Сбрасываем SID для новой сессии + self.sid = None + + logger.info("Attempting to authenticate with Synology NAS...") + logger.debug(f"Base URL: {self.base_url}") + + # Проверяем доступность NAS перед запросом с помощью прямой TCP-проверки + # Избегаем вызова is_online(), чтобы не создавать рекурсию + online_status = self._check_tcp_connection() + if not online_status: + logger.error("Cannot login: Synology NAS is not reachable") + return False + + # Пробуем различные версии API для аутентификации + auth_versions_to_try = [6, 3, 2, 1] # Пробуем от более новых к более старым версиям + + for auth_version in auth_versions_to_try: + try: + # Определяем путь к API аутентификации + auth_path = "auth.cgi" # Значение по умолчанию + + # Проверка информации API для определения доступных версий API + api_info_url = f"{self.base_url}/entry.cgi" + api_info_params = { + "api": "SYNO.API.Info", + "version": "1", + "method": "query", + "query": "SYNO.API.Auth" + } + + logger.debug(f"Querying API info for auth version {auth_version}") + try: + api_info_response = self.session.get( + api_info_url, + params=api_info_params, + timeout=self.default_timeout, + verify=False + ) + + if api_info_response.status_code == 200: + api_info_data = api_info_response.json() + if api_info_data.get("success"): + auth_info = api_info_data.get("data", {}).get("SYNO.API.Auth", {}) + max_version = auth_info.get("maxVersion", 6) + min_version = auth_info.get("minVersion", 1) + auth_path = auth_info.get("path", "auth.cgi") + logger.debug(f"Auth API versions: min={min_version}, max={max_version}, path={auth_path}") + + # Проверяем поддержку текущей версии + if auth_version < min_version or auth_version > max_version: + logger.warning(f"Auth API version {auth_version} not supported (min={min_version}, max={max_version}), trying next version") + continue + else: + logger.warning("Failed to query API info, using default auth path") + else: + logger.warning(f"API info request failed with status {api_info_response.status_code}, using default auth path") + except Exception as e: + logger.warning(f"Error querying API info: {str(e)}, using default auth path") + + # Основной запрос авторизации + url = f"{self.base_url}/{auth_path}" + params = { + "api": "SYNO.API.Auth", + "version": str(auth_version), + "method": "login", + "account": SYNOLOGY_USERNAME, + "passwd": SYNOLOGY_PASSWORD, + "session": "SynologyPowerControlBot", + "format": "cookie" + } + + # Для версии 6+ используем немного другой формат + if auth_version >= 6: + params["enable_syno_token"] = "yes" + + logger.debug(f"Sending auth request to {url} with API version {auth_version}") + start_time = time.time() + response = self.session.get( + url, + params=params, + timeout=self.default_timeout, + verify=False + ) + elapsed_time = time.time() - start_time + logger.debug(f"Auth request completed in {elapsed_time:.2f}s with status code {response.status_code}") + + # Проверка статуса HTTP + if response.status_code != 200: + logger.error(f"HTTP error: {response.status_code}") + logger.debug(f"Response content: {response.text[:200]}") + continue # Пробуем следующую версию + + try: + data = response.json() + except json.JSONDecodeError: + logger.error("Failed to decode JSON response") + logger.debug(f"Response content: {response.text[:200]}") + continue # Пробуем следующую версию + + if data.get("success"): + self.sid = data.get("data", {}).get("sid") + self._last_auth_time = time.time() + logger.info(f"Successfully logged in to Synology NAS using API version {auth_version}") + logger.debug(f"Session ID obtained: {self.sid[:5] if self.sid else 'None'}...") + + # Проверка валидности полученной сессии + if not self._validate_session(): + logger.warning("Session validation failed, trying next auth version") + self.sid = None + continue + + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to log in with version {auth_version}: Error code {error_code}") + + # Если ошибка связана с версией API, пробуем следующую версию + if error_code in [104, 105]: + logger.warning(f"Auth version {auth_version} not supported, trying next version") + continue + + # Дополнительная диагностика + if error_code == 400: + logger.error("Authentication error: Invalid credentials") + elif error_code == 401: + logger.error("Authentication error: Account disabled") + elif error_code == 402: + logger.error("Authentication error: Permission denied") + elif error_code == 403: + logger.error("Authentication error: 2-factor authentication required") + elif error_code == 404: + logger.error("Authentication error: Failed to authenticate with 2-factor authentication") + + # Если ошибка связана с неверными учетными данными, нет смысла пробовать другие версии API + if error_code in [400, 401, 402, 403, 404]: + return False + + except requests.exceptions.Timeout: + logger.error(f"Connection timeout during auth with version {auth_version}") + continue # Пробуем следующую версию + except requests.exceptions.ConnectionError as e: + logger.error(f"Connection error during auth with version {auth_version}: {str(e)}") + continue # Пробуем следующую версию + except requests.RequestException as e: + logger.error(f"Request error during auth with version {auth_version}: {str(e)}") + continue # Пробуем следующую версию + except Exception as e: + logger.error(f"Unexpected error during login with version {auth_version}: {str(e)}", exc_info=True) + continue # Пробуем следующую версию + + # Если все версии не сработали + logger.error("Failed to authenticate with any API version") + return False + + def _validate_session(self) -> bool: + """Проверяет валидность сессии после авторизации""" + if not self.sid: + return False + + # Попробуем сделать простой запрос для проверки сессии + test_apis = [ + {"api": "SYNO.Core.System", "method": "info", "version": 1}, + {"api": "SYNO.DSM.Info", "method": "getinfo", "version": 1} + ] + + for test_api in test_apis: + try: + url = f"{self.base_url}/entry.cgi" + params = { + "api": test_api["api"], + "version": str(test_api["version"]), + "method": test_api["method"], + "_sid": self.sid + } + + response = self.session.get(url, params=params, timeout=self.default_timeout, verify=False) + + if response.status_code == 200: + data = response.json() + if data.get("success"): + logger.debug(f"Session validation successful using {test_api['api']}") + return True + else: + error_code = data.get("error", {}).get("code", -1) + if error_code != 119: # Не сессия истекла + logger.debug(f"Session validation with {test_api['api']} returned error code {error_code}") + return True # Считаем сессию валидной, если ошибка не связана с истечением сессии + except Exception as e: + logger.warning(f"Error during session validation with {test_api['api']}: {str(e)}") + + logger.warning("Session validation failed with all test APIs") + return False + + def logout(self) -> bool: + """Выход из API Synology NAS""" + if not self.sid: + return True + + try: + url = f"{self.base_url}/auth.cgi" + params = { + "api": "SYNO.API.Auth", + "version": "1", + "method": "logout", + "session": "SynologyPowerControlBot", + "_sid": self.sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + self.sid = None + logger.info("Successfully logged out from Synology NAS") + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to log out from Synology NAS: Error code {error_code}") + return False + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + return False + + def _make_api_request(self, api_name: str, method: str, version: int = 1, + params: Optional[Dict[str, Any]] = None, retry_auth: bool = True) -> Optional[Dict[str, Any]]: + """Обобщенный метод для выполнения API запросов с обработкой ошибок""" + # Проверка наличия авторизации + if not self.sid and not self.login(): + logger.error(f"Not authenticated for API request: {api_name}.{method}") + return None + + # Проверка информации API для определения пути и поддерживаемой версии + api_info_url = f"{self.base_url}/entry.cgi" + api_info_params = { + "api": "SYNO.API.Info", + "version": "1", + "method": "query", + "query": api_name + } + + api_path = "entry.cgi" + try: + logger.debug(f"Querying API info for {api_name}") + api_info_response = self.session.get( + api_info_url, + params=api_info_params, + timeout=self.default_timeout, + verify=False + ) + + if api_info_response.status_code == 200: + api_info_data = api_info_response.json() + if api_info_data.get("success"): + api_info = api_info_data.get("data", {}).get(api_name, {}) + if api_info: + max_version = api_info.get("maxVersion", version) + min_version = api_info.get("minVersion", version) + api_path = api_info.get("path", "entry.cgi") + + # Проверка, поддерживается ли запрошенная версия + if version < min_version: + logger.warning(f"API version {version} for {api_name} is below minimum {min_version}, using {min_version}") + version = min_version + elif version > max_version: + logger.warning(f"API version {version} for {api_name} exceeds maximum {max_version}, using {max_version}") + version = max_version + + logger.debug(f"Using API path: {api_path}, version: {version}") + else: + logger.warning(f"API {api_name} not found in API info, using defaults") + except Exception as e: + logger.warning(f"Error querying API info for {api_name}: {str(e)}, using defaults") + + # Подготовка базовых параметров запроса + base_params = { + "api": api_name, + "version": str(version), + "method": method, + "sid": self.sid + } + + # Добавление дополнительных параметров, если они заданы + if params: + base_params.update(params) + + url = f"{self.base_url}/{api_path}" + logger.debug(f"API request: {api_name}.{method} v{version} to {url}") + logger.debug(f"Full request params: {base_params}") + + try: + start_time = time.time() + response = self.session.get( + url, + params=base_params, + timeout=self.default_timeout, + verify=False + ) + elapsed_time = time.time() - start_time + logger.debug(f"API request completed in {elapsed_time:.2f}s with status {response.status_code}") + + # Подробное логирование для отладки + logger.debug(f"Response: {response.text[:500]}") + + # Проверка статуса HTTP + if response.status_code != 200: + logger.error(f"HTTP error: {response.status_code} for {api_name}.{method}") + return None + + try: + data = response.json() + except json.JSONDecodeError: + logger.error(f"Failed to decode JSON response for {api_name}.{method}") + logger.debug(f"Response content: {response.text[:200]}") + return None + + if data.get("success"): + logger.info(f"API request successful for {api_name}.{method}") + return data.get("data", {}) + else: + error_code = data.get("error", {}).get("code", -1) + error_desc = self._get_error_description(error_code) + logger.error(f"API error for {api_name}.{method}: {error_code} - {error_desc}") + + # Ошибки доступа или прав часто встречаются, но они не критичные + # Например, ошибка 102 означает, что нет прав, но NAS доступен + if error_code in [102, 103, 104, 105]: + logger.warning(f"Permission-related error for {api_name}.{method}: {error_code}") + # Возвращаем пустой словарь вместо None, + # чтобы вызывающий код мог понять, что запрос выполнен + return {} + + # Если ошибка связана с авторизацией и нам разрешено повторить попытку + if error_code in [106, 107, 119] and retry_auth: + logger.info("Session may have expired, re-authenticating...") + self.sid = None # Сбрасываем SID + if self.login(): + logger.info("Re-authenticated, retrying API request...") + # Рекурсивный вызов, но без повторной авторизации во избежание зацикливания + return self._make_api_request(api_name, method, version, params, False) + + return None + + except requests.exceptions.Timeout: + logger.error(f"Request timeout for {api_name}.{method}") + return None + except requests.exceptions.ConnectionError as e: + logger.error(f"Connection error for {api_name}.{method}: {str(e)}") + return None + except Exception as e: + logger.error(f"Unexpected error during {api_name}.{method}: {str(e)}") + return None + + def get_system_status(self) -> Dict[str, Any]: + """Получение статуса системы""" + # Проверяем доступность системы + if not self.is_online(): + logger.info("Device is offline, skipping API request") + return {"status": "offline"} + + # Проверяем, есть ли кэшированный результат + cache_key = "system_status" + current_time = time.time() + if cache_key in self._cache and current_time - self._cache_ttl.get(cache_key, 0) < 60: + logger.debug("Using cached system status") + return self._cache[cache_key] + + # Используем рекомендованный API для получения информации о системе + logger.info(f"Getting system info using {SYNOLOGY_INFO_API} v{SYNOLOGY_API_VERSION}") + + # Для SYNO.DSM.Info метод getinfo, для других обычно используется get или info + if SYNOLOGY_INFO_API == "SYNO.DSM.Info": + method = "getinfo" + else: + method = "get" + + result = self._make_api_request(SYNOLOGY_INFO_API, method, version=SYNOLOGY_API_VERSION) + + if result: + logger.info(f"Successfully retrieved system info using {SYNOLOGY_INFO_API}") + + # Формируем расширенный ответ с дополнительной информацией + system_info = { + "status": "online", + "hostname": result.get("hostname", "unknown"), + "model": result.get("model", "unknown"), + "version": result.get("version", "unknown"), + "uptime": result.get("uptime", 0), + "time": current_time, + "is_online": True, + "api_used": SYNOLOGY_INFO_API + } + + # Сохраняем в кэше + self._cache[cache_key] = system_info + self._cache_ttl[cache_key] = current_time + + return system_info + + # Если основной API не сработал, пробуем резервные варианты + logger.warning(f"Failed to retrieve system info with {SYNOLOGY_INFO_API}, trying fallback APIs") + + # Пробуем резервные API + apis_to_try = [ + {"name": "SYNO.DSM.Info", "method": "getinfo", "version": 2}, + {"name": "SYNO.Core.System", "method": "info", "version": 1}, + {"name": "SYNO.Core.System.Status", "method": "get", "version": 1}, + {"name": "SYNO.Core.System.Info", "method": "get", "version": 1}, + ] + + for api in apis_to_try: + if api["name"] == SYNOLOGY_INFO_API: + continue # Пропускаем уже проверенный API + + logger.info(f"Trying to get system info using {api['name']}.{api['method']} v{api['version']}") + result = self._make_api_request(api["name"], api["method"], version=api["version"]) + + if result: + logger.info(f"Successfully retrieved system info using {api['name']}") + + # Формируем расширенный ответ с дополнительной информацией + system_info = { + "status": "online", + "hostname": result.get("hostname", "unknown"), + "model": result.get("model", "unknown"), + "version": result.get("version", "unknown"), + "uptime": result.get("uptime", 0), + "time": current_time, + "is_online": True, + "api_used": api["name"] + } + + # Сохраняем в кэше + self._cache[cache_key] = system_info + self._cache_ttl[cache_key] = current_time + + return system_info + + # Если все запросы не удались, но система онлайн, возвращаем базовую информацию + logger.warning("Failed to retrieve system info with all API methods") + return { + "status": "error", + "error": "Failed to fetch system information", + "is_online": True, + "time": current_time + } + + def shutdown_system(self) -> bool: + """Выключение системы""" + # Проверяем, включено ли устройство перед попыткой его выключить + if not self.is_online(force_check=True): + logger.info("Synology NAS is already offline, no need to shut down") + return True + + logger.info("Attempting to shutdown Synology NAS...") + + # Попробуем сначала использовать предпочтительный API для управления питанием + logger.info(f"Trying shutdown with {SYNOLOGY_POWER_API} v{SYNOLOGY_API_VERSION}") + + # Для SYNO.Core.Hardware.PowerRecovery используется метод setPowerOnState + # Для других API обычно используется метод shutdown или reboot + if SYNOLOGY_POWER_API == "SYNO.Core.Hardware.PowerRecovery": + # Для этого API нужны специальные параметры + params = {"state": "powerbtn"} # powerbtn имитирует нажатие кнопки питания + result = self._make_api_request(SYNOLOGY_POWER_API, "setPowerOnState", version=SYNOLOGY_API_VERSION, params=params) + else: + # Пробуем стандартный метод + result = self._make_api_request(SYNOLOGY_POWER_API, "shutdown", version=SYNOLOGY_API_VERSION) + + if result is not None: + logger.info(f"Successfully initiated system shutdown using {SYNOLOGY_POWER_API}") + + # Даем системе время начать процесс выключения + logger.info("Waiting for shutdown to initialize...") + sleep(5) + + # Проверяем статус + if not self.is_online(force_check=True): + logger.info("System is now offline. Shutdown confirmed successful.") + return True + else: + logger.info("System still appears to be online, but shutdown may be in progress.") + return True + else: + logger.warning(f"Failed to shutdown using {SYNOLOGY_POWER_API}, trying alternative methods") + + # Если не сработал основной метод, пробуем резервные варианты + # Проверка всех доступных методов API для выключения + apis_to_try = [ + {"name": "SYNO.Core.System", "method": "shutdown", "version": 3}, + {"name": "SYNO.Core.System.Power", "method": "shutdown", "version": 1}, + {"name": "SYNO.DSM.Power", "method": "shutdown", "version": 1}, + {"name": "SYNO.System.Power", "method": "shutdown", "version": 1}, + {"name": "SYNO.DSM.System", "method": "shutdown", "version": 1} + ] + + # Проверяем доступные API + try: + api_info_url = f"{self.base_url}/entry.cgi" + api_info_params = { + "api": "SYNO.API.Info", + "version": "1", + "method": "query", + "query": "SYNO.Core.System.Power,SYNO.DSM.Power,SYNO.Core.System,SYNO.System.Power,SYNO.DSM.System" + } + + logger.debug("Checking available shutdown APIs") + api_info_response = self.session.get( + api_info_url, + params=api_info_params, + timeout=self.default_timeout, + verify=False + ) + + if api_info_response.status_code == 200: + api_info_data = api_info_response.json() + if api_info_data.get("success"): + available_apis = api_info_data.get("data", {}) + logger.debug(f"Available APIs: {list(available_apis.keys())}") + + # Фильтруем только доступные API + filtered_apis = [] + for api in apis_to_try: + if api["name"] in available_apis: + api_info = available_apis[api["name"]] + # Проверка версии API + if api_info.get("minVersion", 1) <= api["version"] <= api_info.get("maxVersion", 1): + filtered_apis.append(api) + logger.debug(f"Adding {api['name']} to available shutdown APIs") + else: + logger.debug(f"API {api['name']} version {api['version']} not supported. Supported: {api_info.get('minVersion')}-{api_info.get('maxVersion')}") + + if filtered_apis: + apis_to_try = filtered_apis + else: + logger.warning("No compatible APIs found, trying all methods as fallback") + else: + logger.warning("Failed to query API info, using default methods") + else: + logger.warning(f"API info request failed with status {api_info_response.status_code}, using default methods") + except Exception as e: + logger.warning(f"Error querying API info: {str(e)}, using default methods") + + # Пробуем все доступные методы по порядку + for api in apis_to_try: + logger.info(f"Trying shutdown with {api['name']}.{api['method']} v{api['version']}") + result = self._make_api_request(api["name"], api["method"], version=api["version"]) + + if result is not None: + logger.info(f"Successfully initiated system shutdown using {api['name']}") + + # Даем системе время начать процесс выключения + logger.info("Waiting for shutdown to initialize...") + sleep(5) + + # Проверяем статус + if not self.is_online(force_check=True): + logger.info("System is now offline. Shutdown confirmed successful.") + return True + else: + logger.info("System still appears to be online, but shutdown may be in progress.") + return True + else: + logger.warning(f"Failed to shutdown using {api['name']}.{api['method']} v{api['version']}") + + # Если ни один метод не сработал, но система стала недоступна + if not self.is_online(force_check=True): + logger.info("System appears to be shutting down despite API errors") + return True + + logger.error("Failed to shutdown system after trying multiple APIs") + return False + + def reboot_system(self) -> bool: + """Перезагрузка системы""" + # Проверяем, включена ли система + if not self.is_online(force_check=True): + logger.error("Cannot reboot: System is offline") + return False + + logger.info("Attempting to reboot Synology NAS...") + + # Попробуем сначала использовать предпочтительный API для управления питанием + logger.info(f"Trying reboot with {SYNOLOGY_POWER_API} v{SYNOLOGY_API_VERSION}") + + # Для SYNO.Core.Hardware.PowerRecovery используется метод setPowerOnState или powerButton + # Для других API обычно используется метод restart или reboot + if SYNOLOGY_POWER_API == "SYNO.Core.Hardware.PowerRecovery": + # Для этого API нужны специальные параметры + params = {"reboot": "true"} # Передаем флаг для перезагрузки + result = self._make_api_request(SYNOLOGY_POWER_API, "setPowerOnState", version=SYNOLOGY_API_VERSION, params=params) + else: + # Пробуем стандартный метод + methods_to_try = ["restart", "reboot"] + result = None + for method in methods_to_try: + result = self._make_api_request(SYNOLOGY_POWER_API, method, version=SYNOLOGY_API_VERSION) + if result is not None: + logger.info(f"Successfully used method {method} for {SYNOLOGY_POWER_API}") + break + + if result is not None: + logger.info(f"Successfully initiated system reboot using {SYNOLOGY_POWER_API}") + + # Даем системе время начать процесс перезагрузки + logger.info("Waiting for reboot to initialize...") + sleep(5) + + # Ждем, пока система станет недоступна (признак перезагрузки) + reboot_started = False + for i in range(12): # Проверяем в течение 60 секунд (12 * 5сек) + if not self.is_online(force_check=True): + logger.info(f"System went offline after {i*5} seconds, reboot in progress") + reboot_started = True + break + logger.debug(f"System still online, waiting... ({i+1}/12)") + sleep(5) + + if reboot_started: + # Ждем, пока система снова станет доступна + logger.info("Waiting for system to come back online...") + return self.wait_for_boot(max_attempts=30, delay=10) + else: + logger.warning("System did not go offline after reboot command, but command was accepted") + # Даже если система не ушла оффлайн, команда могла быть принята + return True + else: + logger.warning(f"Failed to reboot using {SYNOLOGY_POWER_API}, trying alternative methods") + + # Если не сработал основной метод, пробуем резервные варианты + # Проверка всех доступных методов API для перезагрузки + apis_to_try = [ + {"name": "SYNO.Core.System.Power", "method": "restart", "version": 1}, + {"name": "SYNO.DSM.Power", "method": "reboot", "version": 1}, + {"name": "SYNO.Core.System", "method": "reboot", "version": 3}, + {"name": "SYNO.System.Power", "method": "reboot", "version": 1}, + {"name": "SYNO.DSM.System", "method": "reboot", "version": 1}, + {"name": "SYNO.Core.Hardware.NeedReboot", "method": "reboot", "version": 1} + ] + + # Получаем список доступных API + try: + api_info_url = f"{self.base_url}/entry.cgi" + api_info_params = { + "api": "SYNO.API.Info", + "version": "1", + "method": "query", + "query": "SYNO.Core.System.Power,SYNO.DSM.Power,SYNO.Core.System,SYNO.System.Power,SYNO.DSM.System,SYNO.Core.Hardware.NeedReboot" + } + + logger.debug("Checking available reboot APIs") + api_info_response = self.session.get( + api_info_url, + params=api_info_params, + timeout=self.default_timeout, + verify=False + ) + + if api_info_response.status_code == 200: + api_info_data = api_info_response.json() + if api_info_data.get("success"): + available_apis = api_info_data.get("data", {}) + logger.debug(f"Available APIs: {list(available_apis.keys())}") + + # Фильтруем только доступные API + filtered_apis = [] + for api in apis_to_try: + if api["name"] in available_apis: + api_info = available_apis[api["name"]] + # Проверка версии API + if api_info.get("minVersion", 1) <= api["version"] <= api_info.get("maxVersion", 1): + filtered_apis.append(api) + logger.debug(f"Adding {api['name']} to available reboot APIs") + else: + logger.debug(f"API {api['name']} version {api['version']} not supported. Supported: {api_info.get('minVersion')}-{api_info.get('maxVersion')}") + + if filtered_apis: + apis_to_try = filtered_apis + else: + logger.warning("No compatible APIs found, trying all methods as fallback") + else: + logger.warning("Failed to query API info, using default methods") + else: + logger.warning(f"API info request failed with status {api_info_response.status_code}, using default methods") + except Exception as e: + logger.warning(f"Error querying API info: {str(e)}, using default methods") + + # Пробуем все доступные методы по порядку + for api in apis_to_try: + logger.info(f"Trying reboot with {api['name']}.{api['method']} v{api['version']}") + result = self._make_api_request(api["name"], api["method"], version=api["version"]) + + if result is not None: + logger.info(f"Successfully initiated system reboot using {api['name']}") + + # Даем системе время начать процесс перезагрузки + logger.info("Waiting for reboot to initialize...") + sleep(5) + + # Ждем, пока система станет недоступна (признак перезагрузки) + reboot_started = False + for i in range(12): # Проверяем в течение 60 секунд (12 * 5сек) + if not self.is_online(force_check=True): + logger.info(f"System went offline after {i*5} seconds, reboot in progress") + reboot_started = True + break + logger.debug(f"System still online, waiting... ({i+1}/12)") + sleep(5) + + if reboot_started: + # Ждем, пока система снова станет доступна + logger.info("Waiting for system to come back online...") + return self.wait_for_boot(max_attempts=30, delay=10) + else: + logger.warning("System did not go offline after reboot command, but command was accepted") + # Даже если система не ушла оффлайн, команда могла быть принята + return True + else: + logger.warning(f"Failed to reboot using {api['name']}.{api['method']} v{api['version']}") + + logger.error("Failed to reboot system after trying multiple APIs") + return False + + def _get_error_description(self, error_code: int) -> str: + """Получение описания ошибки по коду""" + error_descriptions = { + 100: "Unknown error", + 101: "Invalid parameter", + 102: "API does not exist", + 103: "Method does not exist", + 104: "Version does not support", + 105: "Permission denied", + 106: "Session timeout", + 107: "Session interrupted by duplicate login", + 400: "Invalid credentials", + 401: "Account disabled", + 402: "Permission denied", + 403: "2FA required", + 404: "Failed to authenticate with 2FA" + } + return error_descriptions.get(error_code, "Unknown error code") + + def _check_tcp_connection(self) -> bool: + """Проверка базового TCP-соединения с Synology NAS""" + try: + socket_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + socket_obj.settimeout(SYNOLOGY_TIMEOUT) + result = socket_obj.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT)) + socket_obj.close() + + return result == 0 + except socket.error as e: + logger.error(f"Socket error during connection check: {str(e)}") + return False + except Exception as e: + logger.error(f"Unexpected error during connection check: {str(e)}") + return False + + def is_online(self, force_check=False) -> bool: + """Проверка онлайн-статуса Synology NAS""" + # Используем кэшированное значение, если доступно и не устарело + current_time = time.time() + if not force_check and (current_time - self._last_online_check) < self._online_check_interval: + logger.debug(f"Using cached online status: {self._last_online_status}") + return self._last_online_status + + logger.info("Checking if NAS is online...") + + # Проверяем TCP-соединение + online_status = self._check_tcp_connection() + logger.info(f"Detected Synology NAS online status: {online_status}") + + # Если TCP-соединение успешно и у нас есть действующий SID, + # попробуем более детальную проверку через API + if online_status and self.sid: + logger.info("Trying to fetch more detailed online status through API...") + + # Пробуем разные API для проверки онлайн-статуса + api_checks = [ + {"api": "SYNO.DSM.Info", "version": "2", "method": "getinfo"}, + {"api": "SYNO.Core.System", "version": "1", "method": "info"}, + {"api": "SYNO.Core.System.Status", "version": "1", "method": "get"} + ] + + api_success = False + for api_check in api_checks: + try: + url = f"{self.base_url}/entry.cgi" + params = { + "api": api_check["api"], + "version": api_check["version"], + "method": api_check["method"], + "sid": self.sid + } + + logger.debug(f"Trying online status check with {api_check['api']}") + response = self.session.get( + url, + params=params, + timeout=self.default_timeout, + verify=False + ) + + if response.status_code == 200: + data = response.json() + if data.get("success"): + logger.info(f"API request successful for {api_check['api']}") + logger.info("Synology NAS is online with API access") + api_success = True + break + else: + error_code = data.get("error", {}).get("code", -1) + logger.warning(f"API response indicates an error: {error_code}, but NAS is reachable") + else: + logger.warning(f"API returned status code {response.status_code}") + except Exception as e: + logger.warning(f"API check failed for {api_check['api']}, but TCP connection succeeded: {str(e)}") + + if not api_success: + logger.warning("All API checks failed, but TCP connection is successful") + + # Обновляем кэшированное значение + self._last_online_check = current_time + self._last_online_status = online_status + + return online_status + + def wake_on_lan(self) -> bool: + """Отправка Wake-on-LAN пакета для включения Synology NAS""" + if not SYNOLOGY_MAC: + logger.error("MAC address not configured") + return False + + try: + # Преобразование MAC-адреса в байты + mac_address = SYNOLOGY_MAC.replace(':', '').replace('-', '') + if len(mac_address) != 12: + logger.error(f"Invalid MAC address format: {SYNOLOGY_MAC}") + return False + + try: + mac_bytes = bytes.fromhex(mac_address) + except ValueError as e: + logger.error(f"Failed to parse MAC address: {str(e)}") + logger.error(f"MAC address must contain only hex characters: {SYNOLOGY_MAC}") + return False + + # Создание Magic Packet + magic_packet = b'\xff' * 6 + mac_bytes * 16 + + # Отправка пакета на конкретный адрес + try: + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) + sock.sendto(magic_packet, (SYNOLOGY_HOST, WOL_PORT)) + sock.close() + logger.info(f"Wake-on-LAN packet sent to {SYNOLOGY_MAC} at {SYNOLOGY_HOST}:{WOL_PORT}") + except Exception as e: + logger.error(f"Error sending directed WoL packet: {str(e)}") + return False + + # Для надежности отправляем также широковещательный пакет + try: + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) + + # Используем стандартный широковещательный адрес + broadcast_addr = "255.255.255.255" + sock.sendto(magic_packet, (broadcast_addr, WOL_PORT)) + sock.close() + logger.info(f"Wake-on-LAN broadcast packet sent to {broadcast_addr}:{WOL_PORT}") + except Exception as e: + logger.warning(f"Error sending broadcast WoL packet (non-critical): {str(e)}") + # Не считаем ошибкой, т.к. основной пакет уже отправлен + + return True + + except Exception as e: + logger.error(f"Unexpected error in wake_on_lan: {str(e)}") + return False + + def wait_for_boot(self, max_attempts: int = 30, delay: int = 10) -> bool: + """Ожидание загрузки Synology NAS""" + logger.info(f"Waiting for Synology NAS to boot (timeout: {max_attempts * delay}s)...") + + for attempt in range(max_attempts): + # Принудительно проверяем статус без использования кэша + if self.is_online(force_check=True): + logger.info(f"Synology NAS is online after {attempt + 1} attempts ({(attempt + 1) * delay}s)") + + # Проверяем, что не только сеть доступна, но и API загрузился + api_ready = False + logger.info("Waiting for API services to initialize...") + + for api_check in range(5): # Даем еще до 50 секунд для загрузки API + if self.sid or self.login(): + api_ready = True + logger.info(f"API services are ready after {api_check + 1} attempts") + break + logger.debug(f"API not ready yet, waiting... ({api_check + 1}/5)") + sleep(10) + + if not api_ready: + logger.warning("System is online but API services may not be fully initialized") + + # Дадим дополнительное время для полной загрузки всех сервисов + sleep(delay) + return True + + sleep(delay) + logger.info(f"Waiting for boot... Attempt {attempt + 1}/{max_attempts}") + + logger.error(f"Synology NAS didn't come online after {max_attempts * delay} seconds") + return False + + def power_on(self) -> bool: + """Включение Synology NAS""" + # Принудительная проверка статуса + if self.is_online(force_check=True): + logger.info("Synology NAS is already online") + return True + + logger.info("Powering on Synology NAS via Wake-on-LAN...") + + # Проверяем, настроен ли MAC-адрес + if not SYNOLOGY_MAC: + logger.error("Cannot power on: MAC address not configured in settings") + return False + + # Пробуем отправить несколько WoL пакетов для надежности + success = False + for attempt in range(3): + logger.debug(f"Sending WoL packet, attempt {attempt + 1}/3") + if self.wake_on_lan(): + success = True + break + sleep(1) + + if not success: + logger.error("Failed to send Wake-on-LAN packets") + return False + + # Ожидание загрузки + logger.info("WoL packets sent successfully, waiting for system to boot...") + boot_result = self.wait_for_boot(max_attempts=30, delay=10) + + if boot_result: + # Проверяем доступность API после загрузки + system_status = self.get_system_status() + if system_status.get("status") == "online": + logger.info("System booted successfully with API access") + return True + else: + logger.warning("System appears to be online but API may not be fully ready") + return True + else: + logger.error("System did not come online after WoL") + return False + + def power_off(self) -> bool: + """Выключение Synology NAS""" + if not self.is_online(force_check=True): + logger.info("Synology NAS is already offline") + return True + + logger.info("Powering off Synology NAS...") + result = self.shutdown_system() + + if result: + # Дополнительная проверка, что система действительно выключилась + logger.info("Verifying system is offline...") + for attempt in range(12): # Проверяем в течение 2 минут (12 * 10 сек) + if not self.is_online(force_check=True): + logger.info(f"System confirmed offline after {attempt * 10} seconds") + return True + logger.debug(f"System still shutting down, checking again... ({attempt + 1}/12)") + sleep(10) + + logger.warning("System still appears to be online after shutdown command") + return False + else: + logger.error("Failed to initiate shutdown") + return False + + # Заглушки для расширенных методов + def get_shared_folders(self) -> List[Dict[str, Any]]: + """Получение списка общих папок (заглушка)""" + logger.warning("Function get_shared_folders() is not implemented yet") + return [] + + def get_system_load(self) -> Dict[str, Any]: + """Получение информации о загрузке системы (заглушка)""" + logger.warning("Function get_system_load() is not implemented yet") + return {} + + def get_storage_status(self) -> Dict[str, Any]: + """Получение подробной информации о хранилище (заглушка)""" + logger.warning("Function get_storage_status() is not implemented yet") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for storage status request") + return {"error": "authentication_failed"} + + try: + # Здесь будет реализация после добавления соответствующего API + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.Storage.CGI.Storage", + "version": "1", + "method": "load_info", + "sid": self.sid + } + + # В текущей версии просто возвращаем заглушку + return { + "volumes": [], + "disks": [], + "total_size": 0, + "total_used": 0, + "error": "not_implemented" + } + except Exception as e: + logger.error(f"Error in get_storage_status: {str(e)}") + return {"error": str(e)} + + def get_security_status(self) -> Dict[str, Any]: + """Получение информации о состоянии безопасности (заглушка)""" + logger.warning("Function get_security_status() is not implemented yet") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for security status request") + return {"success": False, "error": "authentication_failed"} + + try: + # Здесь будет реализация после добавления соответствующего API + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.Core.SecurityScan.Status", + "version": "1", + "method": "get", + "sid": self.sid + } + + # В текущей версии просто возвращаем заглушку + return { + "success": False, + "status": "not_implemented", + "last_check": None, + "is_secure": False, + "error": "not_implemented" + } + except Exception as e: + logger.error(f"Error in get_security_status: {str(e)}") + return {"success": False, "error": str(e)} diff --git a/.history/src/api/synology_20250830082954.py b/.history/src/api/synology_20250830082954.py new file mode 100644 index 0000000..f1b3e29 --- /dev/null +++ b/.history/src/api/synology_20250830082954.py @@ -0,0 +1,1189 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Модуль для взаимодействия с API Synology NAS +""" + +import requests +from requests.adapters import HTTPAdapter +import json +import logging +import time +import urllib3 +from urllib3.util import Retry +from typing import Dict, Any, Optional, List +import socket +import struct +from time import sleep + +from src.config.config import ( + SYNOLOGY_HOST, + SYNOLOGY_PORT, + SYNOLOGY_USERNAME, + SYNOLOGY_PASSWORD, + SYNOLOGY_SECURE, + SYNOLOGY_TIMEOUT, + SYNOLOGY_MAC, + WOL_PORT, + SYNOLOGY_API_VERSION, + SYNOLOGY_POWER_API, + SYNOLOGY_INFO_API +) +from src.api.api_discovery import discover_available_apis, find_compatible_api + +# Отключение предупреждений о небезопасных SSL-соединениях +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + +logger = logging.getLogger(__name__) + +class SynologyAPI: + """Класс для взаимодействия с API Synology NAS""" + + def __init__(self): + """Инициализация класса SynologyAPI""" + logger.info("Creating API with auto-retry and connection pool") + logger.debug(f"Connection details: Host={SYNOLOGY_HOST}, Port={SYNOLOGY_PORT}, Secure={SYNOLOGY_SECURE}") + + self.protocol = "https" if SYNOLOGY_SECURE else "http" + self.base_url = f"{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi" + self.sid = None + self.session = requests.Session() + + # Настройка SSL + if self.protocol == "https": + logger.debug("SSL enabled, disabling certificate verification for internal network") + self.session.verify = False # Отключаем проверку SSL для внутренней сети + + # Добавляем повторные попытки для HTTP-запросов + retry_strategy = Retry( + total=5, # Увеличиваем количество попыток + status_forcelist=[429, 500, 502, 503, 504, 404], + allowed_methods=["GET", "POST"], + backoff_factor=1.5, # Увеличиваем задержку между попытками + respect_retry_after_header=True + ) + adapter = HTTPAdapter( + max_retries=retry_strategy, + pool_connections=3, + pool_maxsize=10 + ) + self.session.mount("http://", adapter) + self.session.mount("https://", adapter) + + # Таймауты будут указаны в запросах + self.default_timeout = (SYNOLOGY_TIMEOUT, SYNOLOGY_TIMEOUT*2) + logger.debug(f"Setting default request timeout: {self.default_timeout}") + + # Кэш для хранения результатов запросов + self._cache = {} + self._cache_ttl = {} + self._last_online_check = 0 + self._last_online_status = False + self._online_check_interval = 30 # Интервал для кэширования проверок онлайн-статуса + + # Время последней успешной аутентификации и срок действия сессии + self._last_auth_time = 0 + self._auth_expiry = 3600 # По умолчанию 1 час + + # Информация о доступных API + self._available_apis = {} + self._api_info_ttl = 0 + + def login(self) -> bool: + """Авторизация в API Synology NAS""" + # Сбрасываем SID для новой сессии + self.sid = None + + logger.info("Attempting to authenticate with Synology NAS...") + logger.debug(f"Base URL: {self.base_url}") + + # Проверяем доступность NAS перед запросом с помощью прямой TCP-проверки + # Избегаем вызова is_online(), чтобы не создавать рекурсию + online_status = self._check_tcp_connection() + if not online_status: + logger.error("Cannot login: Synology NAS is not reachable") + return False + + # Пробуем различные версии API для аутентификации + auth_versions_to_try = [6, 3, 2, 1] # Пробуем от более новых к более старым версиям + + for auth_version in auth_versions_to_try: + try: + # Определяем путь к API аутентификации + auth_path = "auth.cgi" # Значение по умолчанию + + # Проверка информации API для определения доступных версий API + api_info_url = f"{self.base_url}/entry.cgi" + api_info_params = { + "api": "SYNO.API.Info", + "version": "1", + "method": "query", + "query": "SYNO.API.Auth" + } + + logger.debug(f"Querying API info for auth version {auth_version}") + try: + api_info_response = self.session.get( + api_info_url, + params=api_info_params, + timeout=self.default_timeout, + verify=False + ) + + if api_info_response.status_code == 200: + api_info_data = api_info_response.json() + if api_info_data.get("success"): + auth_info = api_info_data.get("data", {}).get("SYNO.API.Auth", {}) + max_version = auth_info.get("maxVersion", 6) + min_version = auth_info.get("minVersion", 1) + auth_path = auth_info.get("path", "auth.cgi") + logger.debug(f"Auth API versions: min={min_version}, max={max_version}, path={auth_path}") + + # Проверяем поддержку текущей версии + if auth_version < min_version or auth_version > max_version: + logger.warning(f"Auth API version {auth_version} not supported (min={min_version}, max={max_version}), trying next version") + continue + else: + logger.warning("Failed to query API info, using default auth path") + else: + logger.warning(f"API info request failed with status {api_info_response.status_code}, using default auth path") + except Exception as e: + logger.warning(f"Error querying API info: {str(e)}, using default auth path") + + # Основной запрос авторизации + url = f"{self.base_url}/{auth_path}" + params = { + "api": "SYNO.API.Auth", + "version": str(auth_version), + "method": "login", + "account": SYNOLOGY_USERNAME, + "passwd": SYNOLOGY_PASSWORD, + "session": "SynologyPowerControlBot", + "format": "cookie" + } + + # Для версии 6+ используем немного другой формат + if auth_version >= 6: + params["enable_syno_token"] = "yes" + + logger.debug(f"Sending auth request to {url} with API version {auth_version}") + start_time = time.time() + response = self.session.get( + url, + params=params, + timeout=self.default_timeout, + verify=False + ) + elapsed_time = time.time() - start_time + logger.debug(f"Auth request completed in {elapsed_time:.2f}s with status code {response.status_code}") + + # Проверка статуса HTTP + if response.status_code != 200: + logger.error(f"HTTP error: {response.status_code}") + logger.debug(f"Response content: {response.text[:200]}") + continue # Пробуем следующую версию + + try: + data = response.json() + except json.JSONDecodeError: + logger.error("Failed to decode JSON response") + logger.debug(f"Response content: {response.text[:200]}") + continue # Пробуем следующую версию + + if data.get("success"): + self.sid = data.get("data", {}).get("sid") + self._last_auth_time = time.time() + logger.info(f"Successfully logged in to Synology NAS using API version {auth_version}") + logger.debug(f"Session ID obtained: {self.sid[:5] if self.sid else 'None'}...") + + # Проверка валидности полученной сессии + if not self._validate_session(): + logger.warning("Session validation failed, trying next auth version") + self.sid = None + continue + + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to log in with version {auth_version}: Error code {error_code}") + + # Если ошибка связана с версией API, пробуем следующую версию + if error_code in [104, 105]: + logger.warning(f"Auth version {auth_version} not supported, trying next version") + continue + + # Дополнительная диагностика + if error_code == 400: + logger.error("Authentication error: Invalid credentials") + elif error_code == 401: + logger.error("Authentication error: Account disabled") + elif error_code == 402: + logger.error("Authentication error: Permission denied") + elif error_code == 403: + logger.error("Authentication error: 2-factor authentication required") + elif error_code == 404: + logger.error("Authentication error: Failed to authenticate with 2-factor authentication") + + # Если ошибка связана с неверными учетными данными, нет смысла пробовать другие версии API + if error_code in [400, 401, 402, 403, 404]: + return False + + except requests.exceptions.Timeout: + logger.error(f"Connection timeout during auth with version {auth_version}") + continue # Пробуем следующую версию + except requests.exceptions.ConnectionError as e: + logger.error(f"Connection error during auth with version {auth_version}: {str(e)}") + continue # Пробуем следующую версию + except requests.RequestException as e: + logger.error(f"Request error during auth with version {auth_version}: {str(e)}") + continue # Пробуем следующую версию + except Exception as e: + logger.error(f"Unexpected error during login with version {auth_version}: {str(e)}", exc_info=True) + continue # Пробуем следующую версию + + # Если все версии не сработали + logger.error("Failed to authenticate with any API version") + return False + + def _validate_session(self) -> bool: + """Проверяет валидность сессии после авторизации""" + if not self.sid: + return False + + # Попробуем сделать простой запрос для проверки сессии + test_apis = [ + {"api": "SYNO.Core.System", "method": "info", "version": 1}, + {"api": "SYNO.DSM.Info", "method": "getinfo", "version": 1} + ] + + for test_api in test_apis: + try: + url = f"{self.base_url}/entry.cgi" + params = { + "api": test_api["api"], + "version": str(test_api["version"]), + "method": test_api["method"], + "_sid": self.sid + } + + response = self.session.get(url, params=params, timeout=self.default_timeout, verify=False) + + if response.status_code == 200: + data = response.json() + if data.get("success"): + logger.debug(f"Session validation successful using {test_api['api']}") + return True + else: + error_code = data.get("error", {}).get("code", -1) + if error_code != 119: # Не сессия истекла + logger.debug(f"Session validation with {test_api['api']} returned error code {error_code}") + return True # Считаем сессию валидной, если ошибка не связана с истечением сессии + except Exception as e: + logger.warning(f"Error during session validation with {test_api['api']}: {str(e)}") + + logger.warning("Session validation failed with all test APIs") + return False + + def logout(self) -> bool: + """Выход из API Synology NAS""" + if not self.sid: + return True + + try: + url = f"{self.base_url}/auth.cgi" + params = { + "api": "SYNO.API.Auth", + "version": "1", + "method": "logout", + "session": "SynologyPowerControlBot", + "_sid": self.sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + self.sid = None + logger.info("Successfully logged out from Synology NAS") + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to log out from Synology NAS: Error code {error_code}") + return False + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + return False + + def _make_api_request(self, api_name: str, method: str, version: int = 1, + params: Optional[Dict[str, Any]] = None, retry_auth: bool = True, retry_count: int = 0) -> Optional[Dict[str, Any]]: + """Обобщенный метод для выполнения API запросов с обработкой ошибок""" + # Ограничение на количество повторных попыток + if retry_count >= 3: + logger.error(f"Too many retries for {api_name}.{method}, giving up") + return None + + # Проверка наличия авторизации + if not self.sid and not self.login(): + logger.error(f"Not authenticated for API request: {api_name}.{method}") + return None + + # Проверка информации API для определения пути и поддерживаемой версии + api_info_url = f"{self.base_url}/entry.cgi" + api_info_params = { + "api": "SYNO.API.Info", + "version": "1", + "method": "query", + "query": api_name + } + + api_path = "entry.cgi" + try: + logger.debug(f"Querying API info for {api_name}") + api_info_response = self.session.get( + api_info_url, + params=api_info_params, + timeout=self.default_timeout, + verify=False + ) + + if api_info_response.status_code == 200: + api_info_data = api_info_response.json() + if api_info_data.get("success"): + api_info = api_info_data.get("data", {}).get(api_name, {}) + if api_info: + max_version = api_info.get("maxVersion", version) + min_version = api_info.get("minVersion", version) + api_path = api_info.get("path", "entry.cgi") + + # Проверка, поддерживается ли запрошенная версия + if version < min_version: + logger.warning(f"API version {version} for {api_name} is below minimum {min_version}, using {min_version}") + version = min_version + elif version > max_version: + logger.warning(f"API version {version} for {api_name} exceeds maximum {max_version}, using {max_version}") + version = max_version + + logger.debug(f"Using API path: {api_path}, version: {version}") + else: + logger.warning(f"API {api_name} not found in API info, using defaults") + except Exception as e: + logger.warning(f"Error querying API info for {api_name}: {str(e)}, using defaults") + + # Подготовка базовых параметров запроса + base_params = { + "api": api_name, + "version": str(version), + "method": method, + "_sid": self.sid # Используем _sid вместо sid для лучшей совместимости + } + + # Добавление дополнительных параметров, если они заданы + if params: + base_params.update(params) + + url = f"{self.base_url}/{api_path}" + logger.debug(f"API request: {api_name}.{method} v{version} to {url}") + logger.debug(f"Full request params: {base_params}") + + try: + start_time = time.time() + response = self.session.get( + url, + params=base_params, + timeout=self.default_timeout, + verify=False + ) + elapsed_time = time.time() - start_time + logger.debug(f"API request completed in {elapsed_time:.2f}s with status {response.status_code}") + + # Проверка статуса HTTP + if response.status_code != 200: + logger.error(f"HTTP error: {response.status_code} for {api_name}.{method}") + + # Повторная попытка при ошибках соединения + if response.status_code in [500, 502, 503, 504]: + logger.info(f"Server error, retrying request for {api_name}.{method}") + sleep(2) + return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1) + + return None + + try: + data = response.json() + except json.JSONDecodeError: + logger.error(f"Failed to decode JSON response for {api_name}.{method}") + logger.debug(f"Response content: {response.text[:200]}") + + # Повторная попытка при ошибках декодирования + logger.info(f"JSON decode error, retrying request for {api_name}.{method}") + sleep(2) + return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1) + + if data.get("success"): + logger.info(f"API request successful for {api_name}.{method}") + return data.get("data", {}) + else: + error_code = data.get("error", {}).get("code", -1) + error_desc = self._get_error_description(error_code) + logger.error(f"API error for {api_name}.{method}: {error_code} - {error_desc}") + + # Ошибки доступа или прав часто встречаются, но они не критичные + # Например, ошибка 102 означает, что нет прав, но NAS доступен + if error_code in [102, 103, 104, 105]: + logger.warning(f"Permission-related error for {api_name}.{method}: {error_code}") + # Возвращаем пустой словарь вместо None, + # чтобы вызывающий код мог понять, что запрос выполнен + return {} + + # Если ошибка связана с авторизацией и нам разрешено повторить попытку + if error_code in [106, 107, 119] and retry_auth: + logger.info(f"Session error (code {error_code}), creating fresh session...") + self.sid = None # Сбрасываем SID + + # Для ошибки 119 (Session timeout) дадим системе немного времени + if error_code == 119: + logger.info("Session timeout detected, waiting before retry...") + sleep(3) + + if self.login(): + logger.info("Re-authenticated with fresh session, retrying API request...") + # Рекурсивный вызов, но со счетчиком повторов + return self._make_api_request(api_name, method, version, params, False, retry_count + 1) + + # Для некоторых ошибок можно автоматически повторить запрос + if error_code in [408, 429, 500, 502, 503, 504]: + logger.info(f"Temporary error {error_code}, retrying request for {api_name}.{method}") + sleep(2) + return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1) + + return None + + except requests.exceptions.Timeout: + logger.error(f"Request timeout for {api_name}.{method}") + + # Повторная попытка при таймауте + if retry_count < 2: + logger.info(f"Timeout, retrying request for {api_name}.{method}") + sleep(2) + return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1) + + return None + except requests.exceptions.ConnectionError as e: + logger.error(f"Connection error for {api_name}.{method}: {str(e)}") + + # Повторная попытка при ошибке соединения + if retry_count < 2: + logger.info(f"Connection error, retrying request for {api_name}.{method}") + sleep(2) + return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1) + + return None + except Exception as e: + logger.error(f"Unexpected error during {api_name}.{method}: {str(e)}") + return None + + def get_system_status(self) -> Dict[str, Any]: + """Получение статуса системы""" + # Проверяем доступность системы + if not self.is_online(): + logger.info("Device is offline, skipping API request") + return {"status": "offline"} + + # Проверяем, есть ли кэшированный результат + cache_key = "system_status" + current_time = time.time() + if cache_key in self._cache and current_time - self._cache_ttl.get(cache_key, 0) < 60: + logger.debug("Using cached system status") + return self._cache[cache_key] + + # Используем рекомендованный API для получения информации о системе + logger.info(f"Getting system info using {SYNOLOGY_INFO_API} v{SYNOLOGY_API_VERSION}") + + # Для SYNO.DSM.Info метод getinfo, для других обычно используется get или info + if SYNOLOGY_INFO_API == "SYNO.DSM.Info": + method = "getinfo" + else: + method = "get" + + result = self._make_api_request(SYNOLOGY_INFO_API, method, version=SYNOLOGY_API_VERSION) + + if result: + logger.info(f"Successfully retrieved system info using {SYNOLOGY_INFO_API}") + + # Формируем расширенный ответ с дополнительной информацией + system_info = { + "status": "online", + "hostname": result.get("hostname", "unknown"), + "model": result.get("model", "unknown"), + "version": result.get("version", "unknown"), + "uptime": result.get("uptime", 0), + "time": current_time, + "is_online": True, + "api_used": SYNOLOGY_INFO_API + } + + # Сохраняем в кэше + self._cache[cache_key] = system_info + self._cache_ttl[cache_key] = current_time + + return system_info + + # Если основной API не сработал, пробуем резервные варианты + logger.warning(f"Failed to retrieve system info with {SYNOLOGY_INFO_API}, trying fallback APIs") + + # Пробуем резервные API + apis_to_try = [ + {"name": "SYNO.DSM.Info", "method": "getinfo", "version": 2}, + {"name": "SYNO.Core.System", "method": "info", "version": 1}, + {"name": "SYNO.Core.System.Status", "method": "get", "version": 1}, + {"name": "SYNO.Core.System.Info", "method": "get", "version": 1}, + ] + + for api in apis_to_try: + if api["name"] == SYNOLOGY_INFO_API: + continue # Пропускаем уже проверенный API + + logger.info(f"Trying to get system info using {api['name']}.{api['method']} v{api['version']}") + result = self._make_api_request(api["name"], api["method"], version=api["version"]) + + if result: + logger.info(f"Successfully retrieved system info using {api['name']}") + + # Формируем расширенный ответ с дополнительной информацией + system_info = { + "status": "online", + "hostname": result.get("hostname", "unknown"), + "model": result.get("model", "unknown"), + "version": result.get("version", "unknown"), + "uptime": result.get("uptime", 0), + "time": current_time, + "is_online": True, + "api_used": api["name"] + } + + # Сохраняем в кэше + self._cache[cache_key] = system_info + self._cache_ttl[cache_key] = current_time + + return system_info + + # Если все запросы не удались, но система онлайн, возвращаем базовую информацию + logger.warning("Failed to retrieve system info with all API methods") + return { + "status": "error", + "error": "Failed to fetch system information", + "is_online": True, + "time": current_time + } + + def shutdown_system(self) -> bool: + """Выключение системы""" + # Проверяем, включено ли устройство перед попыткой его выключить + if not self.is_online(force_check=True): + logger.info("Synology NAS is already offline, no need to shut down") + return True + + logger.info("Attempting to shutdown Synology NAS...") + + # Попробуем сначала использовать предпочтительный API для управления питанием + logger.info(f"Trying shutdown with {SYNOLOGY_POWER_API} v{SYNOLOGY_API_VERSION}") + + # Для SYNO.Core.Hardware.PowerRecovery используется метод setPowerOnState + # Для других API обычно используется метод shutdown или reboot + if SYNOLOGY_POWER_API == "SYNO.Core.Hardware.PowerRecovery": + # Для этого API нужны специальные параметры + params = {"state": "powerbtn"} # powerbtn имитирует нажатие кнопки питания + result = self._make_api_request(SYNOLOGY_POWER_API, "setPowerOnState", version=SYNOLOGY_API_VERSION, params=params) + else: + # Пробуем стандартный метод + result = self._make_api_request(SYNOLOGY_POWER_API, "shutdown", version=SYNOLOGY_API_VERSION) + + if result is not None: + logger.info(f"Successfully initiated system shutdown using {SYNOLOGY_POWER_API}") + + # Даем системе время начать процесс выключения + logger.info("Waiting for shutdown to initialize...") + sleep(5) + + # Проверяем статус + if not self.is_online(force_check=True): + logger.info("System is now offline. Shutdown confirmed successful.") + return True + else: + logger.info("System still appears to be online, but shutdown may be in progress.") + return True + else: + logger.warning(f"Failed to shutdown using {SYNOLOGY_POWER_API}, trying alternative methods") + + # Если не сработал основной метод, пробуем резервные варианты + # Проверка всех доступных методов API для выключения + apis_to_try = [ + {"name": "SYNO.Core.System", "method": "shutdown", "version": 3}, + {"name": "SYNO.Core.System.Power", "method": "shutdown", "version": 1}, + {"name": "SYNO.DSM.Power", "method": "shutdown", "version": 1}, + {"name": "SYNO.System.Power", "method": "shutdown", "version": 1}, + {"name": "SYNO.DSM.System", "method": "shutdown", "version": 1} + ] + + # Проверяем доступные API + try: + api_info_url = f"{self.base_url}/entry.cgi" + api_info_params = { + "api": "SYNO.API.Info", + "version": "1", + "method": "query", + "query": "SYNO.Core.System.Power,SYNO.DSM.Power,SYNO.Core.System,SYNO.System.Power,SYNO.DSM.System" + } + + logger.debug("Checking available shutdown APIs") + api_info_response = self.session.get( + api_info_url, + params=api_info_params, + timeout=self.default_timeout, + verify=False + ) + + if api_info_response.status_code == 200: + api_info_data = api_info_response.json() + if api_info_data.get("success"): + available_apis = api_info_data.get("data", {}) + logger.debug(f"Available APIs: {list(available_apis.keys())}") + + # Фильтруем только доступные API + filtered_apis = [] + for api in apis_to_try: + if api["name"] in available_apis: + api_info = available_apis[api["name"]] + # Проверка версии API + if api_info.get("minVersion", 1) <= api["version"] <= api_info.get("maxVersion", 1): + filtered_apis.append(api) + logger.debug(f"Adding {api['name']} to available shutdown APIs") + else: + logger.debug(f"API {api['name']} version {api['version']} not supported. Supported: {api_info.get('minVersion')}-{api_info.get('maxVersion')}") + + if filtered_apis: + apis_to_try = filtered_apis + else: + logger.warning("No compatible APIs found, trying all methods as fallback") + else: + logger.warning("Failed to query API info, using default methods") + else: + logger.warning(f"API info request failed with status {api_info_response.status_code}, using default methods") + except Exception as e: + logger.warning(f"Error querying API info: {str(e)}, using default methods") + + # Пробуем все доступные методы по порядку + for api in apis_to_try: + logger.info(f"Trying shutdown with {api['name']}.{api['method']} v{api['version']}") + result = self._make_api_request(api["name"], api["method"], version=api["version"]) + + if result is not None: + logger.info(f"Successfully initiated system shutdown using {api['name']}") + + # Даем системе время начать процесс выключения + logger.info("Waiting for shutdown to initialize...") + sleep(5) + + # Проверяем статус + if not self.is_online(force_check=True): + logger.info("System is now offline. Shutdown confirmed successful.") + return True + else: + logger.info("System still appears to be online, but shutdown may be in progress.") + return True + else: + logger.warning(f"Failed to shutdown using {api['name']}.{api['method']} v{api['version']}") + + # Если ни один метод не сработал, но система стала недоступна + if not self.is_online(force_check=True): + logger.info("System appears to be shutting down despite API errors") + return True + + logger.error("Failed to shutdown system after trying multiple APIs") + return False + + def reboot_system(self) -> bool: + """Перезагрузка системы""" + # Проверяем, включена ли система + if not self.is_online(force_check=True): + logger.error("Cannot reboot: System is offline") + return False + + logger.info("Attempting to reboot Synology NAS...") + + # Попробуем сначала использовать предпочтительный API для управления питанием + logger.info(f"Trying reboot with {SYNOLOGY_POWER_API} v{SYNOLOGY_API_VERSION}") + + # Для SYNO.Core.Hardware.PowerRecovery используется метод setPowerOnState или powerButton + # Для других API обычно используется метод restart или reboot + if SYNOLOGY_POWER_API == "SYNO.Core.Hardware.PowerRecovery": + # Для этого API нужны специальные параметры + params = {"reboot": "true"} # Передаем флаг для перезагрузки + result = self._make_api_request(SYNOLOGY_POWER_API, "setPowerOnState", version=SYNOLOGY_API_VERSION, params=params) + else: + # Пробуем стандартный метод + methods_to_try = ["restart", "reboot"] + result = None + for method in methods_to_try: + result = self._make_api_request(SYNOLOGY_POWER_API, method, version=SYNOLOGY_API_VERSION) + if result is not None: + logger.info(f"Successfully used method {method} for {SYNOLOGY_POWER_API}") + break + + if result is not None: + logger.info(f"Successfully initiated system reboot using {SYNOLOGY_POWER_API}") + + # Даем системе время начать процесс перезагрузки + logger.info("Waiting for reboot to initialize...") + sleep(5) + + # Ждем, пока система станет недоступна (признак перезагрузки) + reboot_started = False + for i in range(12): # Проверяем в течение 60 секунд (12 * 5сек) + if not self.is_online(force_check=True): + logger.info(f"System went offline after {i*5} seconds, reboot in progress") + reboot_started = True + break + logger.debug(f"System still online, waiting... ({i+1}/12)") + sleep(5) + + if reboot_started: + # Ждем, пока система снова станет доступна + logger.info("Waiting for system to come back online...") + return self.wait_for_boot(max_attempts=30, delay=10) + else: + logger.warning("System did not go offline after reboot command, but command was accepted") + # Даже если система не ушла оффлайн, команда могла быть принята + return True + else: + logger.warning(f"Failed to reboot using {SYNOLOGY_POWER_API}, trying alternative methods") + + # Если не сработал основной метод, пробуем резервные варианты + # Проверка всех доступных методов API для перезагрузки + apis_to_try = [ + {"name": "SYNO.Core.System.Power", "method": "restart", "version": 1}, + {"name": "SYNO.DSM.Power", "method": "reboot", "version": 1}, + {"name": "SYNO.Core.System", "method": "reboot", "version": 3}, + {"name": "SYNO.System.Power", "method": "reboot", "version": 1}, + {"name": "SYNO.DSM.System", "method": "reboot", "version": 1}, + {"name": "SYNO.Core.Hardware.NeedReboot", "method": "reboot", "version": 1} + ] + + # Получаем список доступных API + try: + api_info_url = f"{self.base_url}/entry.cgi" + api_info_params = { + "api": "SYNO.API.Info", + "version": "1", + "method": "query", + "query": "SYNO.Core.System.Power,SYNO.DSM.Power,SYNO.Core.System,SYNO.System.Power,SYNO.DSM.System,SYNO.Core.Hardware.NeedReboot" + } + + logger.debug("Checking available reboot APIs") + api_info_response = self.session.get( + api_info_url, + params=api_info_params, + timeout=self.default_timeout, + verify=False + ) + + if api_info_response.status_code == 200: + api_info_data = api_info_response.json() + if api_info_data.get("success"): + available_apis = api_info_data.get("data", {}) + logger.debug(f"Available APIs: {list(available_apis.keys())}") + + # Фильтруем только доступные API + filtered_apis = [] + for api in apis_to_try: + if api["name"] in available_apis: + api_info = available_apis[api["name"]] + # Проверка версии API + if api_info.get("minVersion", 1) <= api["version"] <= api_info.get("maxVersion", 1): + filtered_apis.append(api) + logger.debug(f"Adding {api['name']} to available reboot APIs") + else: + logger.debug(f"API {api['name']} version {api['version']} not supported. Supported: {api_info.get('minVersion')}-{api_info.get('maxVersion')}") + + if filtered_apis: + apis_to_try = filtered_apis + else: + logger.warning("No compatible APIs found, trying all methods as fallback") + else: + logger.warning("Failed to query API info, using default methods") + else: + logger.warning(f"API info request failed with status {api_info_response.status_code}, using default methods") + except Exception as e: + logger.warning(f"Error querying API info: {str(e)}, using default methods") + + # Пробуем все доступные методы по порядку + for api in apis_to_try: + logger.info(f"Trying reboot with {api['name']}.{api['method']} v{api['version']}") + result = self._make_api_request(api["name"], api["method"], version=api["version"]) + + if result is not None: + logger.info(f"Successfully initiated system reboot using {api['name']}") + + # Даем системе время начать процесс перезагрузки + logger.info("Waiting for reboot to initialize...") + sleep(5) + + # Ждем, пока система станет недоступна (признак перезагрузки) + reboot_started = False + for i in range(12): # Проверяем в течение 60 секунд (12 * 5сек) + if not self.is_online(force_check=True): + logger.info(f"System went offline after {i*5} seconds, reboot in progress") + reboot_started = True + break + logger.debug(f"System still online, waiting... ({i+1}/12)") + sleep(5) + + if reboot_started: + # Ждем, пока система снова станет доступна + logger.info("Waiting for system to come back online...") + return self.wait_for_boot(max_attempts=30, delay=10) + else: + logger.warning("System did not go offline after reboot command, but command was accepted") + # Даже если система не ушла оффлайн, команда могла быть принята + return True + else: + logger.warning(f"Failed to reboot using {api['name']}.{api['method']} v{api['version']}") + + logger.error("Failed to reboot system after trying multiple APIs") + return False + + def _get_error_description(self, error_code: int) -> str: + """Получение описания ошибки по коду""" + error_descriptions = { + 100: "Unknown error", + 101: "Invalid parameter", + 102: "API does not exist", + 103: "Method does not exist", + 104: "Version does not support", + 105: "Permission denied", + 106: "Session timeout", + 107: "Session interrupted by duplicate login", + 400: "Invalid credentials", + 401: "Account disabled", + 402: "Permission denied", + 403: "2FA required", + 404: "Failed to authenticate with 2FA" + } + return error_descriptions.get(error_code, "Unknown error code") + + def _check_tcp_connection(self) -> bool: + """Проверка базового TCP-соединения с Synology NAS""" + try: + socket_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + socket_obj.settimeout(SYNOLOGY_TIMEOUT) + result = socket_obj.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT)) + socket_obj.close() + + return result == 0 + except socket.error as e: + logger.error(f"Socket error during connection check: {str(e)}") + return False + except Exception as e: + logger.error(f"Unexpected error during connection check: {str(e)}") + return False + + def is_online(self, force_check=False) -> bool: + """Проверка онлайн-статуса Synology NAS""" + # Используем кэшированное значение, если доступно и не устарело + current_time = time.time() + if not force_check and (current_time - self._last_online_check) < self._online_check_interval: + logger.debug(f"Using cached online status: {self._last_online_status}") + return self._last_online_status + + logger.info("Checking if NAS is online...") + + # Проверяем TCP-соединение + online_status = self._check_tcp_connection() + logger.info(f"Detected Synology NAS online status: {online_status}") + + # Если TCP-соединение успешно и у нас есть действующий SID, + # попробуем более детальную проверку через API + if online_status and self.sid: + logger.info("Trying to fetch more detailed online status through API...") + + # Пробуем разные API для проверки онлайн-статуса + api_checks = [ + {"api": "SYNO.DSM.Info", "version": "2", "method": "getinfo"}, + {"api": "SYNO.Core.System", "version": "1", "method": "info"}, + {"api": "SYNO.Core.System.Status", "version": "1", "method": "get"} + ] + + api_success = False + for api_check in api_checks: + try: + url = f"{self.base_url}/entry.cgi" + params = { + "api": api_check["api"], + "version": api_check["version"], + "method": api_check["method"], + "sid": self.sid + } + + logger.debug(f"Trying online status check with {api_check['api']}") + response = self.session.get( + url, + params=params, + timeout=self.default_timeout, + verify=False + ) + + if response.status_code == 200: + data = response.json() + if data.get("success"): + logger.info(f"API request successful for {api_check['api']}") + logger.info("Synology NAS is online with API access") + api_success = True + break + else: + error_code = data.get("error", {}).get("code", -1) + logger.warning(f"API response indicates an error: {error_code}, but NAS is reachable") + else: + logger.warning(f"API returned status code {response.status_code}") + except Exception as e: + logger.warning(f"API check failed for {api_check['api']}, but TCP connection succeeded: {str(e)}") + + if not api_success: + logger.warning("All API checks failed, but TCP connection is successful") + + # Обновляем кэшированное значение + self._last_online_check = current_time + self._last_online_status = online_status + + return online_status + + def wake_on_lan(self) -> bool: + """Отправка Wake-on-LAN пакета для включения Synology NAS""" + if not SYNOLOGY_MAC: + logger.error("MAC address not configured") + return False + + try: + # Преобразование MAC-адреса в байты + mac_address = SYNOLOGY_MAC.replace(':', '').replace('-', '') + if len(mac_address) != 12: + logger.error(f"Invalid MAC address format: {SYNOLOGY_MAC}") + return False + + try: + mac_bytes = bytes.fromhex(mac_address) + except ValueError as e: + logger.error(f"Failed to parse MAC address: {str(e)}") + logger.error(f"MAC address must contain only hex characters: {SYNOLOGY_MAC}") + return False + + # Создание Magic Packet + magic_packet = b'\xff' * 6 + mac_bytes * 16 + + # Отправка пакета на конкретный адрес + try: + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) + sock.sendto(magic_packet, (SYNOLOGY_HOST, WOL_PORT)) + sock.close() + logger.info(f"Wake-on-LAN packet sent to {SYNOLOGY_MAC} at {SYNOLOGY_HOST}:{WOL_PORT}") + except Exception as e: + logger.error(f"Error sending directed WoL packet: {str(e)}") + return False + + # Для надежности отправляем также широковещательный пакет + try: + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) + + # Используем стандартный широковещательный адрес + broadcast_addr = "255.255.255.255" + sock.sendto(magic_packet, (broadcast_addr, WOL_PORT)) + sock.close() + logger.info(f"Wake-on-LAN broadcast packet sent to {broadcast_addr}:{WOL_PORT}") + except Exception as e: + logger.warning(f"Error sending broadcast WoL packet (non-critical): {str(e)}") + # Не считаем ошибкой, т.к. основной пакет уже отправлен + + return True + + except Exception as e: + logger.error(f"Unexpected error in wake_on_lan: {str(e)}") + return False + + def wait_for_boot(self, max_attempts: int = 30, delay: int = 10) -> bool: + """Ожидание загрузки Synology NAS""" + logger.info(f"Waiting for Synology NAS to boot (timeout: {max_attempts * delay}s)...") + + for attempt in range(max_attempts): + # Принудительно проверяем статус без использования кэша + if self.is_online(force_check=True): + logger.info(f"Synology NAS is online after {attempt + 1} attempts ({(attempt + 1) * delay}s)") + + # Проверяем, что не только сеть доступна, но и API загрузился + api_ready = False + logger.info("Waiting for API services to initialize...") + + for api_check in range(5): # Даем еще до 50 секунд для загрузки API + if self.sid or self.login(): + api_ready = True + logger.info(f"API services are ready after {api_check + 1} attempts") + break + logger.debug(f"API not ready yet, waiting... ({api_check + 1}/5)") + sleep(10) + + if not api_ready: + logger.warning("System is online but API services may not be fully initialized") + + # Дадим дополнительное время для полной загрузки всех сервисов + sleep(delay) + return True + + sleep(delay) + logger.info(f"Waiting for boot... Attempt {attempt + 1}/{max_attempts}") + + logger.error(f"Synology NAS didn't come online after {max_attempts * delay} seconds") + return False + + def power_on(self) -> bool: + """Включение Synology NAS""" + # Принудительная проверка статуса + if self.is_online(force_check=True): + logger.info("Synology NAS is already online") + return True + + logger.info("Powering on Synology NAS via Wake-on-LAN...") + + # Проверяем, настроен ли MAC-адрес + if not SYNOLOGY_MAC: + logger.error("Cannot power on: MAC address not configured in settings") + return False + + # Пробуем отправить несколько WoL пакетов для надежности + success = False + for attempt in range(3): + logger.debug(f"Sending WoL packet, attempt {attempt + 1}/3") + if self.wake_on_lan(): + success = True + break + sleep(1) + + if not success: + logger.error("Failed to send Wake-on-LAN packets") + return False + + # Ожидание загрузки + logger.info("WoL packets sent successfully, waiting for system to boot...") + boot_result = self.wait_for_boot(max_attempts=30, delay=10) + + if boot_result: + # Проверяем доступность API после загрузки + system_status = self.get_system_status() + if system_status.get("status") == "online": + logger.info("System booted successfully with API access") + return True + else: + logger.warning("System appears to be online but API may not be fully ready") + return True + else: + logger.error("System did not come online after WoL") + return False + + def power_off(self) -> bool: + """Выключение Synology NAS""" + if not self.is_online(force_check=True): + logger.info("Synology NAS is already offline") + return True + + logger.info("Powering off Synology NAS...") + result = self.shutdown_system() + + if result: + # Дополнительная проверка, что система действительно выключилась + logger.info("Verifying system is offline...") + for attempt in range(12): # Проверяем в течение 2 минут (12 * 10 сек) + if not self.is_online(force_check=True): + logger.info(f"System confirmed offline after {attempt * 10} seconds") + return True + logger.debug(f"System still shutting down, checking again... ({attempt + 1}/12)") + sleep(10) + + logger.warning("System still appears to be online after shutdown command") + return False + else: + logger.error("Failed to initiate shutdown") + return False + + # Заглушки для расширенных методов + def get_shared_folders(self) -> List[Dict[str, Any]]: + """Получение списка общих папок (заглушка)""" + logger.warning("Function get_shared_folders() is not implemented yet") + return [] + + def get_system_load(self) -> Dict[str, Any]: + """Получение информации о загрузке системы (заглушка)""" + logger.warning("Function get_system_load() is not implemented yet") + return {} + + def get_storage_status(self) -> Dict[str, Any]: + """Получение подробной информации о хранилище (заглушка)""" + logger.warning("Function get_storage_status() is not implemented yet") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for storage status request") + return {"error": "authentication_failed"} + + try: + # Здесь будет реализация после добавления соответствующего API + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.Storage.CGI.Storage", + "version": "1", + "method": "load_info", + "sid": self.sid + } + + # В текущей версии просто возвращаем заглушку + return { + "volumes": [], + "disks": [], + "total_size": 0, + "total_used": 0, + "error": "not_implemented" + } + except Exception as e: + logger.error(f"Error in get_storage_status: {str(e)}") + return {"error": str(e)} + + def get_security_status(self) -> Dict[str, Any]: + """Получение информации о состоянии безопасности (заглушка)""" + logger.warning("Function get_security_status() is not implemented yet") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for security status request") + return {"success": False, "error": "authentication_failed"} + + try: + # Здесь будет реализация после добавления соответствующего API + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.Core.SecurityScan.Status", + "version": "1", + "method": "get", + "sid": self.sid + } + + # В текущей версии просто возвращаем заглушку + return { + "success": False, + "status": "not_implemented", + "last_check": None, + "is_secure": False, + "error": "not_implemented" + } + except Exception as e: + logger.error(f"Error in get_security_status: {str(e)}") + return {"success": False, "error": str(e)} diff --git a/.history/src/api/synology_20250830083115.py b/.history/src/api/synology_20250830083115.py new file mode 100644 index 0000000..f1b3e29 --- /dev/null +++ b/.history/src/api/synology_20250830083115.py @@ -0,0 +1,1189 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Модуль для взаимодействия с API Synology NAS +""" + +import requests +from requests.adapters import HTTPAdapter +import json +import logging +import time +import urllib3 +from urllib3.util import Retry +from typing import Dict, Any, Optional, List +import socket +import struct +from time import sleep + +from src.config.config import ( + SYNOLOGY_HOST, + SYNOLOGY_PORT, + SYNOLOGY_USERNAME, + SYNOLOGY_PASSWORD, + SYNOLOGY_SECURE, + SYNOLOGY_TIMEOUT, + SYNOLOGY_MAC, + WOL_PORT, + SYNOLOGY_API_VERSION, + SYNOLOGY_POWER_API, + SYNOLOGY_INFO_API +) +from src.api.api_discovery import discover_available_apis, find_compatible_api + +# Отключение предупреждений о небезопасных SSL-соединениях +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + +logger = logging.getLogger(__name__) + +class SynologyAPI: + """Класс для взаимодействия с API Synology NAS""" + + def __init__(self): + """Инициализация класса SynologyAPI""" + logger.info("Creating API with auto-retry and connection pool") + logger.debug(f"Connection details: Host={SYNOLOGY_HOST}, Port={SYNOLOGY_PORT}, Secure={SYNOLOGY_SECURE}") + + self.protocol = "https" if SYNOLOGY_SECURE else "http" + self.base_url = f"{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi" + self.sid = None + self.session = requests.Session() + + # Настройка SSL + if self.protocol == "https": + logger.debug("SSL enabled, disabling certificate verification for internal network") + self.session.verify = False # Отключаем проверку SSL для внутренней сети + + # Добавляем повторные попытки для HTTP-запросов + retry_strategy = Retry( + total=5, # Увеличиваем количество попыток + status_forcelist=[429, 500, 502, 503, 504, 404], + allowed_methods=["GET", "POST"], + backoff_factor=1.5, # Увеличиваем задержку между попытками + respect_retry_after_header=True + ) + adapter = HTTPAdapter( + max_retries=retry_strategy, + pool_connections=3, + pool_maxsize=10 + ) + self.session.mount("http://", adapter) + self.session.mount("https://", adapter) + + # Таймауты будут указаны в запросах + self.default_timeout = (SYNOLOGY_TIMEOUT, SYNOLOGY_TIMEOUT*2) + logger.debug(f"Setting default request timeout: {self.default_timeout}") + + # Кэш для хранения результатов запросов + self._cache = {} + self._cache_ttl = {} + self._last_online_check = 0 + self._last_online_status = False + self._online_check_interval = 30 # Интервал для кэширования проверок онлайн-статуса + + # Время последней успешной аутентификации и срок действия сессии + self._last_auth_time = 0 + self._auth_expiry = 3600 # По умолчанию 1 час + + # Информация о доступных API + self._available_apis = {} + self._api_info_ttl = 0 + + def login(self) -> bool: + """Авторизация в API Synology NAS""" + # Сбрасываем SID для новой сессии + self.sid = None + + logger.info("Attempting to authenticate with Synology NAS...") + logger.debug(f"Base URL: {self.base_url}") + + # Проверяем доступность NAS перед запросом с помощью прямой TCP-проверки + # Избегаем вызова is_online(), чтобы не создавать рекурсию + online_status = self._check_tcp_connection() + if not online_status: + logger.error("Cannot login: Synology NAS is not reachable") + return False + + # Пробуем различные версии API для аутентификации + auth_versions_to_try = [6, 3, 2, 1] # Пробуем от более новых к более старым версиям + + for auth_version in auth_versions_to_try: + try: + # Определяем путь к API аутентификации + auth_path = "auth.cgi" # Значение по умолчанию + + # Проверка информации API для определения доступных версий API + api_info_url = f"{self.base_url}/entry.cgi" + api_info_params = { + "api": "SYNO.API.Info", + "version": "1", + "method": "query", + "query": "SYNO.API.Auth" + } + + logger.debug(f"Querying API info for auth version {auth_version}") + try: + api_info_response = self.session.get( + api_info_url, + params=api_info_params, + timeout=self.default_timeout, + verify=False + ) + + if api_info_response.status_code == 200: + api_info_data = api_info_response.json() + if api_info_data.get("success"): + auth_info = api_info_data.get("data", {}).get("SYNO.API.Auth", {}) + max_version = auth_info.get("maxVersion", 6) + min_version = auth_info.get("minVersion", 1) + auth_path = auth_info.get("path", "auth.cgi") + logger.debug(f"Auth API versions: min={min_version}, max={max_version}, path={auth_path}") + + # Проверяем поддержку текущей версии + if auth_version < min_version or auth_version > max_version: + logger.warning(f"Auth API version {auth_version} not supported (min={min_version}, max={max_version}), trying next version") + continue + else: + logger.warning("Failed to query API info, using default auth path") + else: + logger.warning(f"API info request failed with status {api_info_response.status_code}, using default auth path") + except Exception as e: + logger.warning(f"Error querying API info: {str(e)}, using default auth path") + + # Основной запрос авторизации + url = f"{self.base_url}/{auth_path}" + params = { + "api": "SYNO.API.Auth", + "version": str(auth_version), + "method": "login", + "account": SYNOLOGY_USERNAME, + "passwd": SYNOLOGY_PASSWORD, + "session": "SynologyPowerControlBot", + "format": "cookie" + } + + # Для версии 6+ используем немного другой формат + if auth_version >= 6: + params["enable_syno_token"] = "yes" + + logger.debug(f"Sending auth request to {url} with API version {auth_version}") + start_time = time.time() + response = self.session.get( + url, + params=params, + timeout=self.default_timeout, + verify=False + ) + elapsed_time = time.time() - start_time + logger.debug(f"Auth request completed in {elapsed_time:.2f}s with status code {response.status_code}") + + # Проверка статуса HTTP + if response.status_code != 200: + logger.error(f"HTTP error: {response.status_code}") + logger.debug(f"Response content: {response.text[:200]}") + continue # Пробуем следующую версию + + try: + data = response.json() + except json.JSONDecodeError: + logger.error("Failed to decode JSON response") + logger.debug(f"Response content: {response.text[:200]}") + continue # Пробуем следующую версию + + if data.get("success"): + self.sid = data.get("data", {}).get("sid") + self._last_auth_time = time.time() + logger.info(f"Successfully logged in to Synology NAS using API version {auth_version}") + logger.debug(f"Session ID obtained: {self.sid[:5] if self.sid else 'None'}...") + + # Проверка валидности полученной сессии + if not self._validate_session(): + logger.warning("Session validation failed, trying next auth version") + self.sid = None + continue + + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to log in with version {auth_version}: Error code {error_code}") + + # Если ошибка связана с версией API, пробуем следующую версию + if error_code in [104, 105]: + logger.warning(f"Auth version {auth_version} not supported, trying next version") + continue + + # Дополнительная диагностика + if error_code == 400: + logger.error("Authentication error: Invalid credentials") + elif error_code == 401: + logger.error("Authentication error: Account disabled") + elif error_code == 402: + logger.error("Authentication error: Permission denied") + elif error_code == 403: + logger.error("Authentication error: 2-factor authentication required") + elif error_code == 404: + logger.error("Authentication error: Failed to authenticate with 2-factor authentication") + + # Если ошибка связана с неверными учетными данными, нет смысла пробовать другие версии API + if error_code in [400, 401, 402, 403, 404]: + return False + + except requests.exceptions.Timeout: + logger.error(f"Connection timeout during auth with version {auth_version}") + continue # Пробуем следующую версию + except requests.exceptions.ConnectionError as e: + logger.error(f"Connection error during auth with version {auth_version}: {str(e)}") + continue # Пробуем следующую версию + except requests.RequestException as e: + logger.error(f"Request error during auth with version {auth_version}: {str(e)}") + continue # Пробуем следующую версию + except Exception as e: + logger.error(f"Unexpected error during login with version {auth_version}: {str(e)}", exc_info=True) + continue # Пробуем следующую версию + + # Если все версии не сработали + logger.error("Failed to authenticate with any API version") + return False + + def _validate_session(self) -> bool: + """Проверяет валидность сессии после авторизации""" + if not self.sid: + return False + + # Попробуем сделать простой запрос для проверки сессии + test_apis = [ + {"api": "SYNO.Core.System", "method": "info", "version": 1}, + {"api": "SYNO.DSM.Info", "method": "getinfo", "version": 1} + ] + + for test_api in test_apis: + try: + url = f"{self.base_url}/entry.cgi" + params = { + "api": test_api["api"], + "version": str(test_api["version"]), + "method": test_api["method"], + "_sid": self.sid + } + + response = self.session.get(url, params=params, timeout=self.default_timeout, verify=False) + + if response.status_code == 200: + data = response.json() + if data.get("success"): + logger.debug(f"Session validation successful using {test_api['api']}") + return True + else: + error_code = data.get("error", {}).get("code", -1) + if error_code != 119: # Не сессия истекла + logger.debug(f"Session validation with {test_api['api']} returned error code {error_code}") + return True # Считаем сессию валидной, если ошибка не связана с истечением сессии + except Exception as e: + logger.warning(f"Error during session validation with {test_api['api']}: {str(e)}") + + logger.warning("Session validation failed with all test APIs") + return False + + def logout(self) -> bool: + """Выход из API Synology NAS""" + if not self.sid: + return True + + try: + url = f"{self.base_url}/auth.cgi" + params = { + "api": "SYNO.API.Auth", + "version": "1", + "method": "logout", + "session": "SynologyPowerControlBot", + "_sid": self.sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + self.sid = None + logger.info("Successfully logged out from Synology NAS") + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to log out from Synology NAS: Error code {error_code}") + return False + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + return False + + def _make_api_request(self, api_name: str, method: str, version: int = 1, + params: Optional[Dict[str, Any]] = None, retry_auth: bool = True, retry_count: int = 0) -> Optional[Dict[str, Any]]: + """Обобщенный метод для выполнения API запросов с обработкой ошибок""" + # Ограничение на количество повторных попыток + if retry_count >= 3: + logger.error(f"Too many retries for {api_name}.{method}, giving up") + return None + + # Проверка наличия авторизации + if not self.sid and not self.login(): + logger.error(f"Not authenticated for API request: {api_name}.{method}") + return None + + # Проверка информации API для определения пути и поддерживаемой версии + api_info_url = f"{self.base_url}/entry.cgi" + api_info_params = { + "api": "SYNO.API.Info", + "version": "1", + "method": "query", + "query": api_name + } + + api_path = "entry.cgi" + try: + logger.debug(f"Querying API info for {api_name}") + api_info_response = self.session.get( + api_info_url, + params=api_info_params, + timeout=self.default_timeout, + verify=False + ) + + if api_info_response.status_code == 200: + api_info_data = api_info_response.json() + if api_info_data.get("success"): + api_info = api_info_data.get("data", {}).get(api_name, {}) + if api_info: + max_version = api_info.get("maxVersion", version) + min_version = api_info.get("minVersion", version) + api_path = api_info.get("path", "entry.cgi") + + # Проверка, поддерживается ли запрошенная версия + if version < min_version: + logger.warning(f"API version {version} for {api_name} is below minimum {min_version}, using {min_version}") + version = min_version + elif version > max_version: + logger.warning(f"API version {version} for {api_name} exceeds maximum {max_version}, using {max_version}") + version = max_version + + logger.debug(f"Using API path: {api_path}, version: {version}") + else: + logger.warning(f"API {api_name} not found in API info, using defaults") + except Exception as e: + logger.warning(f"Error querying API info for {api_name}: {str(e)}, using defaults") + + # Подготовка базовых параметров запроса + base_params = { + "api": api_name, + "version": str(version), + "method": method, + "_sid": self.sid # Используем _sid вместо sid для лучшей совместимости + } + + # Добавление дополнительных параметров, если они заданы + if params: + base_params.update(params) + + url = f"{self.base_url}/{api_path}" + logger.debug(f"API request: {api_name}.{method} v{version} to {url}") + logger.debug(f"Full request params: {base_params}") + + try: + start_time = time.time() + response = self.session.get( + url, + params=base_params, + timeout=self.default_timeout, + verify=False + ) + elapsed_time = time.time() - start_time + logger.debug(f"API request completed in {elapsed_time:.2f}s with status {response.status_code}") + + # Проверка статуса HTTP + if response.status_code != 200: + logger.error(f"HTTP error: {response.status_code} for {api_name}.{method}") + + # Повторная попытка при ошибках соединения + if response.status_code in [500, 502, 503, 504]: + logger.info(f"Server error, retrying request for {api_name}.{method}") + sleep(2) + return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1) + + return None + + try: + data = response.json() + except json.JSONDecodeError: + logger.error(f"Failed to decode JSON response for {api_name}.{method}") + logger.debug(f"Response content: {response.text[:200]}") + + # Повторная попытка при ошибках декодирования + logger.info(f"JSON decode error, retrying request for {api_name}.{method}") + sleep(2) + return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1) + + if data.get("success"): + logger.info(f"API request successful for {api_name}.{method}") + return data.get("data", {}) + else: + error_code = data.get("error", {}).get("code", -1) + error_desc = self._get_error_description(error_code) + logger.error(f"API error for {api_name}.{method}: {error_code} - {error_desc}") + + # Ошибки доступа или прав часто встречаются, но они не критичные + # Например, ошибка 102 означает, что нет прав, но NAS доступен + if error_code in [102, 103, 104, 105]: + logger.warning(f"Permission-related error for {api_name}.{method}: {error_code}") + # Возвращаем пустой словарь вместо None, + # чтобы вызывающий код мог понять, что запрос выполнен + return {} + + # Если ошибка связана с авторизацией и нам разрешено повторить попытку + if error_code in [106, 107, 119] and retry_auth: + logger.info(f"Session error (code {error_code}), creating fresh session...") + self.sid = None # Сбрасываем SID + + # Для ошибки 119 (Session timeout) дадим системе немного времени + if error_code == 119: + logger.info("Session timeout detected, waiting before retry...") + sleep(3) + + if self.login(): + logger.info("Re-authenticated with fresh session, retrying API request...") + # Рекурсивный вызов, но со счетчиком повторов + return self._make_api_request(api_name, method, version, params, False, retry_count + 1) + + # Для некоторых ошибок можно автоматически повторить запрос + if error_code in [408, 429, 500, 502, 503, 504]: + logger.info(f"Temporary error {error_code}, retrying request for {api_name}.{method}") + sleep(2) + return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1) + + return None + + except requests.exceptions.Timeout: + logger.error(f"Request timeout for {api_name}.{method}") + + # Повторная попытка при таймауте + if retry_count < 2: + logger.info(f"Timeout, retrying request for {api_name}.{method}") + sleep(2) + return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1) + + return None + except requests.exceptions.ConnectionError as e: + logger.error(f"Connection error for {api_name}.{method}: {str(e)}") + + # Повторная попытка при ошибке соединения + if retry_count < 2: + logger.info(f"Connection error, retrying request for {api_name}.{method}") + sleep(2) + return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1) + + return None + except Exception as e: + logger.error(f"Unexpected error during {api_name}.{method}: {str(e)}") + return None + + def get_system_status(self) -> Dict[str, Any]: + """Получение статуса системы""" + # Проверяем доступность системы + if not self.is_online(): + logger.info("Device is offline, skipping API request") + return {"status": "offline"} + + # Проверяем, есть ли кэшированный результат + cache_key = "system_status" + current_time = time.time() + if cache_key in self._cache and current_time - self._cache_ttl.get(cache_key, 0) < 60: + logger.debug("Using cached system status") + return self._cache[cache_key] + + # Используем рекомендованный API для получения информации о системе + logger.info(f"Getting system info using {SYNOLOGY_INFO_API} v{SYNOLOGY_API_VERSION}") + + # Для SYNO.DSM.Info метод getinfo, для других обычно используется get или info + if SYNOLOGY_INFO_API == "SYNO.DSM.Info": + method = "getinfo" + else: + method = "get" + + result = self._make_api_request(SYNOLOGY_INFO_API, method, version=SYNOLOGY_API_VERSION) + + if result: + logger.info(f"Successfully retrieved system info using {SYNOLOGY_INFO_API}") + + # Формируем расширенный ответ с дополнительной информацией + system_info = { + "status": "online", + "hostname": result.get("hostname", "unknown"), + "model": result.get("model", "unknown"), + "version": result.get("version", "unknown"), + "uptime": result.get("uptime", 0), + "time": current_time, + "is_online": True, + "api_used": SYNOLOGY_INFO_API + } + + # Сохраняем в кэше + self._cache[cache_key] = system_info + self._cache_ttl[cache_key] = current_time + + return system_info + + # Если основной API не сработал, пробуем резервные варианты + logger.warning(f"Failed to retrieve system info with {SYNOLOGY_INFO_API}, trying fallback APIs") + + # Пробуем резервные API + apis_to_try = [ + {"name": "SYNO.DSM.Info", "method": "getinfo", "version": 2}, + {"name": "SYNO.Core.System", "method": "info", "version": 1}, + {"name": "SYNO.Core.System.Status", "method": "get", "version": 1}, + {"name": "SYNO.Core.System.Info", "method": "get", "version": 1}, + ] + + for api in apis_to_try: + if api["name"] == SYNOLOGY_INFO_API: + continue # Пропускаем уже проверенный API + + logger.info(f"Trying to get system info using {api['name']}.{api['method']} v{api['version']}") + result = self._make_api_request(api["name"], api["method"], version=api["version"]) + + if result: + logger.info(f"Successfully retrieved system info using {api['name']}") + + # Формируем расширенный ответ с дополнительной информацией + system_info = { + "status": "online", + "hostname": result.get("hostname", "unknown"), + "model": result.get("model", "unknown"), + "version": result.get("version", "unknown"), + "uptime": result.get("uptime", 0), + "time": current_time, + "is_online": True, + "api_used": api["name"] + } + + # Сохраняем в кэше + self._cache[cache_key] = system_info + self._cache_ttl[cache_key] = current_time + + return system_info + + # Если все запросы не удались, но система онлайн, возвращаем базовую информацию + logger.warning("Failed to retrieve system info with all API methods") + return { + "status": "error", + "error": "Failed to fetch system information", + "is_online": True, + "time": current_time + } + + def shutdown_system(self) -> bool: + """Выключение системы""" + # Проверяем, включено ли устройство перед попыткой его выключить + if not self.is_online(force_check=True): + logger.info("Synology NAS is already offline, no need to shut down") + return True + + logger.info("Attempting to shutdown Synology NAS...") + + # Попробуем сначала использовать предпочтительный API для управления питанием + logger.info(f"Trying shutdown with {SYNOLOGY_POWER_API} v{SYNOLOGY_API_VERSION}") + + # Для SYNO.Core.Hardware.PowerRecovery используется метод setPowerOnState + # Для других API обычно используется метод shutdown или reboot + if SYNOLOGY_POWER_API == "SYNO.Core.Hardware.PowerRecovery": + # Для этого API нужны специальные параметры + params = {"state": "powerbtn"} # powerbtn имитирует нажатие кнопки питания + result = self._make_api_request(SYNOLOGY_POWER_API, "setPowerOnState", version=SYNOLOGY_API_VERSION, params=params) + else: + # Пробуем стандартный метод + result = self._make_api_request(SYNOLOGY_POWER_API, "shutdown", version=SYNOLOGY_API_VERSION) + + if result is not None: + logger.info(f"Successfully initiated system shutdown using {SYNOLOGY_POWER_API}") + + # Даем системе время начать процесс выключения + logger.info("Waiting for shutdown to initialize...") + sleep(5) + + # Проверяем статус + if not self.is_online(force_check=True): + logger.info("System is now offline. Shutdown confirmed successful.") + return True + else: + logger.info("System still appears to be online, but shutdown may be in progress.") + return True + else: + logger.warning(f"Failed to shutdown using {SYNOLOGY_POWER_API}, trying alternative methods") + + # Если не сработал основной метод, пробуем резервные варианты + # Проверка всех доступных методов API для выключения + apis_to_try = [ + {"name": "SYNO.Core.System", "method": "shutdown", "version": 3}, + {"name": "SYNO.Core.System.Power", "method": "shutdown", "version": 1}, + {"name": "SYNO.DSM.Power", "method": "shutdown", "version": 1}, + {"name": "SYNO.System.Power", "method": "shutdown", "version": 1}, + {"name": "SYNO.DSM.System", "method": "shutdown", "version": 1} + ] + + # Проверяем доступные API + try: + api_info_url = f"{self.base_url}/entry.cgi" + api_info_params = { + "api": "SYNO.API.Info", + "version": "1", + "method": "query", + "query": "SYNO.Core.System.Power,SYNO.DSM.Power,SYNO.Core.System,SYNO.System.Power,SYNO.DSM.System" + } + + logger.debug("Checking available shutdown APIs") + api_info_response = self.session.get( + api_info_url, + params=api_info_params, + timeout=self.default_timeout, + verify=False + ) + + if api_info_response.status_code == 200: + api_info_data = api_info_response.json() + if api_info_data.get("success"): + available_apis = api_info_data.get("data", {}) + logger.debug(f"Available APIs: {list(available_apis.keys())}") + + # Фильтруем только доступные API + filtered_apis = [] + for api in apis_to_try: + if api["name"] in available_apis: + api_info = available_apis[api["name"]] + # Проверка версии API + if api_info.get("minVersion", 1) <= api["version"] <= api_info.get("maxVersion", 1): + filtered_apis.append(api) + logger.debug(f"Adding {api['name']} to available shutdown APIs") + else: + logger.debug(f"API {api['name']} version {api['version']} not supported. Supported: {api_info.get('minVersion')}-{api_info.get('maxVersion')}") + + if filtered_apis: + apis_to_try = filtered_apis + else: + logger.warning("No compatible APIs found, trying all methods as fallback") + else: + logger.warning("Failed to query API info, using default methods") + else: + logger.warning(f"API info request failed with status {api_info_response.status_code}, using default methods") + except Exception as e: + logger.warning(f"Error querying API info: {str(e)}, using default methods") + + # Пробуем все доступные методы по порядку + for api in apis_to_try: + logger.info(f"Trying shutdown with {api['name']}.{api['method']} v{api['version']}") + result = self._make_api_request(api["name"], api["method"], version=api["version"]) + + if result is not None: + logger.info(f"Successfully initiated system shutdown using {api['name']}") + + # Даем системе время начать процесс выключения + logger.info("Waiting for shutdown to initialize...") + sleep(5) + + # Проверяем статус + if not self.is_online(force_check=True): + logger.info("System is now offline. Shutdown confirmed successful.") + return True + else: + logger.info("System still appears to be online, but shutdown may be in progress.") + return True + else: + logger.warning(f"Failed to shutdown using {api['name']}.{api['method']} v{api['version']}") + + # Если ни один метод не сработал, но система стала недоступна + if not self.is_online(force_check=True): + logger.info("System appears to be shutting down despite API errors") + return True + + logger.error("Failed to shutdown system after trying multiple APIs") + return False + + def reboot_system(self) -> bool: + """Перезагрузка системы""" + # Проверяем, включена ли система + if not self.is_online(force_check=True): + logger.error("Cannot reboot: System is offline") + return False + + logger.info("Attempting to reboot Synology NAS...") + + # Попробуем сначала использовать предпочтительный API для управления питанием + logger.info(f"Trying reboot with {SYNOLOGY_POWER_API} v{SYNOLOGY_API_VERSION}") + + # Для SYNO.Core.Hardware.PowerRecovery используется метод setPowerOnState или powerButton + # Для других API обычно используется метод restart или reboot + if SYNOLOGY_POWER_API == "SYNO.Core.Hardware.PowerRecovery": + # Для этого API нужны специальные параметры + params = {"reboot": "true"} # Передаем флаг для перезагрузки + result = self._make_api_request(SYNOLOGY_POWER_API, "setPowerOnState", version=SYNOLOGY_API_VERSION, params=params) + else: + # Пробуем стандартный метод + methods_to_try = ["restart", "reboot"] + result = None + for method in methods_to_try: + result = self._make_api_request(SYNOLOGY_POWER_API, method, version=SYNOLOGY_API_VERSION) + if result is not None: + logger.info(f"Successfully used method {method} for {SYNOLOGY_POWER_API}") + break + + if result is not None: + logger.info(f"Successfully initiated system reboot using {SYNOLOGY_POWER_API}") + + # Даем системе время начать процесс перезагрузки + logger.info("Waiting for reboot to initialize...") + sleep(5) + + # Ждем, пока система станет недоступна (признак перезагрузки) + reboot_started = False + for i in range(12): # Проверяем в течение 60 секунд (12 * 5сек) + if not self.is_online(force_check=True): + logger.info(f"System went offline after {i*5} seconds, reboot in progress") + reboot_started = True + break + logger.debug(f"System still online, waiting... ({i+1}/12)") + sleep(5) + + if reboot_started: + # Ждем, пока система снова станет доступна + logger.info("Waiting for system to come back online...") + return self.wait_for_boot(max_attempts=30, delay=10) + else: + logger.warning("System did not go offline after reboot command, but command was accepted") + # Даже если система не ушла оффлайн, команда могла быть принята + return True + else: + logger.warning(f"Failed to reboot using {SYNOLOGY_POWER_API}, trying alternative methods") + + # Если не сработал основной метод, пробуем резервные варианты + # Проверка всех доступных методов API для перезагрузки + apis_to_try = [ + {"name": "SYNO.Core.System.Power", "method": "restart", "version": 1}, + {"name": "SYNO.DSM.Power", "method": "reboot", "version": 1}, + {"name": "SYNO.Core.System", "method": "reboot", "version": 3}, + {"name": "SYNO.System.Power", "method": "reboot", "version": 1}, + {"name": "SYNO.DSM.System", "method": "reboot", "version": 1}, + {"name": "SYNO.Core.Hardware.NeedReboot", "method": "reboot", "version": 1} + ] + + # Получаем список доступных API + try: + api_info_url = f"{self.base_url}/entry.cgi" + api_info_params = { + "api": "SYNO.API.Info", + "version": "1", + "method": "query", + "query": "SYNO.Core.System.Power,SYNO.DSM.Power,SYNO.Core.System,SYNO.System.Power,SYNO.DSM.System,SYNO.Core.Hardware.NeedReboot" + } + + logger.debug("Checking available reboot APIs") + api_info_response = self.session.get( + api_info_url, + params=api_info_params, + timeout=self.default_timeout, + verify=False + ) + + if api_info_response.status_code == 200: + api_info_data = api_info_response.json() + if api_info_data.get("success"): + available_apis = api_info_data.get("data", {}) + logger.debug(f"Available APIs: {list(available_apis.keys())}") + + # Фильтруем только доступные API + filtered_apis = [] + for api in apis_to_try: + if api["name"] in available_apis: + api_info = available_apis[api["name"]] + # Проверка версии API + if api_info.get("minVersion", 1) <= api["version"] <= api_info.get("maxVersion", 1): + filtered_apis.append(api) + logger.debug(f"Adding {api['name']} to available reboot APIs") + else: + logger.debug(f"API {api['name']} version {api['version']} not supported. Supported: {api_info.get('minVersion')}-{api_info.get('maxVersion')}") + + if filtered_apis: + apis_to_try = filtered_apis + else: + logger.warning("No compatible APIs found, trying all methods as fallback") + else: + logger.warning("Failed to query API info, using default methods") + else: + logger.warning(f"API info request failed with status {api_info_response.status_code}, using default methods") + except Exception as e: + logger.warning(f"Error querying API info: {str(e)}, using default methods") + + # Пробуем все доступные методы по порядку + for api in apis_to_try: + logger.info(f"Trying reboot with {api['name']}.{api['method']} v{api['version']}") + result = self._make_api_request(api["name"], api["method"], version=api["version"]) + + if result is not None: + logger.info(f"Successfully initiated system reboot using {api['name']}") + + # Даем системе время начать процесс перезагрузки + logger.info("Waiting for reboot to initialize...") + sleep(5) + + # Ждем, пока система станет недоступна (признак перезагрузки) + reboot_started = False + for i in range(12): # Проверяем в течение 60 секунд (12 * 5сек) + if not self.is_online(force_check=True): + logger.info(f"System went offline after {i*5} seconds, reboot in progress") + reboot_started = True + break + logger.debug(f"System still online, waiting... ({i+1}/12)") + sleep(5) + + if reboot_started: + # Ждем, пока система снова станет доступна + logger.info("Waiting for system to come back online...") + return self.wait_for_boot(max_attempts=30, delay=10) + else: + logger.warning("System did not go offline after reboot command, but command was accepted") + # Даже если система не ушла оффлайн, команда могла быть принята + return True + else: + logger.warning(f"Failed to reboot using {api['name']}.{api['method']} v{api['version']}") + + logger.error("Failed to reboot system after trying multiple APIs") + return False + + def _get_error_description(self, error_code: int) -> str: + """Получение описания ошибки по коду""" + error_descriptions = { + 100: "Unknown error", + 101: "Invalid parameter", + 102: "API does not exist", + 103: "Method does not exist", + 104: "Version does not support", + 105: "Permission denied", + 106: "Session timeout", + 107: "Session interrupted by duplicate login", + 400: "Invalid credentials", + 401: "Account disabled", + 402: "Permission denied", + 403: "2FA required", + 404: "Failed to authenticate with 2FA" + } + return error_descriptions.get(error_code, "Unknown error code") + + def _check_tcp_connection(self) -> bool: + """Проверка базового TCP-соединения с Synology NAS""" + try: + socket_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + socket_obj.settimeout(SYNOLOGY_TIMEOUT) + result = socket_obj.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT)) + socket_obj.close() + + return result == 0 + except socket.error as e: + logger.error(f"Socket error during connection check: {str(e)}") + return False + except Exception as e: + logger.error(f"Unexpected error during connection check: {str(e)}") + return False + + def is_online(self, force_check=False) -> bool: + """Проверка онлайн-статуса Synology NAS""" + # Используем кэшированное значение, если доступно и не устарело + current_time = time.time() + if not force_check and (current_time - self._last_online_check) < self._online_check_interval: + logger.debug(f"Using cached online status: {self._last_online_status}") + return self._last_online_status + + logger.info("Checking if NAS is online...") + + # Проверяем TCP-соединение + online_status = self._check_tcp_connection() + logger.info(f"Detected Synology NAS online status: {online_status}") + + # Если TCP-соединение успешно и у нас есть действующий SID, + # попробуем более детальную проверку через API + if online_status and self.sid: + logger.info("Trying to fetch more detailed online status through API...") + + # Пробуем разные API для проверки онлайн-статуса + api_checks = [ + {"api": "SYNO.DSM.Info", "version": "2", "method": "getinfo"}, + {"api": "SYNO.Core.System", "version": "1", "method": "info"}, + {"api": "SYNO.Core.System.Status", "version": "1", "method": "get"} + ] + + api_success = False + for api_check in api_checks: + try: + url = f"{self.base_url}/entry.cgi" + params = { + "api": api_check["api"], + "version": api_check["version"], + "method": api_check["method"], + "sid": self.sid + } + + logger.debug(f"Trying online status check with {api_check['api']}") + response = self.session.get( + url, + params=params, + timeout=self.default_timeout, + verify=False + ) + + if response.status_code == 200: + data = response.json() + if data.get("success"): + logger.info(f"API request successful for {api_check['api']}") + logger.info("Synology NAS is online with API access") + api_success = True + break + else: + error_code = data.get("error", {}).get("code", -1) + logger.warning(f"API response indicates an error: {error_code}, but NAS is reachable") + else: + logger.warning(f"API returned status code {response.status_code}") + except Exception as e: + logger.warning(f"API check failed for {api_check['api']}, but TCP connection succeeded: {str(e)}") + + if not api_success: + logger.warning("All API checks failed, but TCP connection is successful") + + # Обновляем кэшированное значение + self._last_online_check = current_time + self._last_online_status = online_status + + return online_status + + def wake_on_lan(self) -> bool: + """Отправка Wake-on-LAN пакета для включения Synology NAS""" + if not SYNOLOGY_MAC: + logger.error("MAC address not configured") + return False + + try: + # Преобразование MAC-адреса в байты + mac_address = SYNOLOGY_MAC.replace(':', '').replace('-', '') + if len(mac_address) != 12: + logger.error(f"Invalid MAC address format: {SYNOLOGY_MAC}") + return False + + try: + mac_bytes = bytes.fromhex(mac_address) + except ValueError as e: + logger.error(f"Failed to parse MAC address: {str(e)}") + logger.error(f"MAC address must contain only hex characters: {SYNOLOGY_MAC}") + return False + + # Создание Magic Packet + magic_packet = b'\xff' * 6 + mac_bytes * 16 + + # Отправка пакета на конкретный адрес + try: + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) + sock.sendto(magic_packet, (SYNOLOGY_HOST, WOL_PORT)) + sock.close() + logger.info(f"Wake-on-LAN packet sent to {SYNOLOGY_MAC} at {SYNOLOGY_HOST}:{WOL_PORT}") + except Exception as e: + logger.error(f"Error sending directed WoL packet: {str(e)}") + return False + + # Для надежности отправляем также широковещательный пакет + try: + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) + + # Используем стандартный широковещательный адрес + broadcast_addr = "255.255.255.255" + sock.sendto(magic_packet, (broadcast_addr, WOL_PORT)) + sock.close() + logger.info(f"Wake-on-LAN broadcast packet sent to {broadcast_addr}:{WOL_PORT}") + except Exception as e: + logger.warning(f"Error sending broadcast WoL packet (non-critical): {str(e)}") + # Не считаем ошибкой, т.к. основной пакет уже отправлен + + return True + + except Exception as e: + logger.error(f"Unexpected error in wake_on_lan: {str(e)}") + return False + + def wait_for_boot(self, max_attempts: int = 30, delay: int = 10) -> bool: + """Ожидание загрузки Synology NAS""" + logger.info(f"Waiting for Synology NAS to boot (timeout: {max_attempts * delay}s)...") + + for attempt in range(max_attempts): + # Принудительно проверяем статус без использования кэша + if self.is_online(force_check=True): + logger.info(f"Synology NAS is online after {attempt + 1} attempts ({(attempt + 1) * delay}s)") + + # Проверяем, что не только сеть доступна, но и API загрузился + api_ready = False + logger.info("Waiting for API services to initialize...") + + for api_check in range(5): # Даем еще до 50 секунд для загрузки API + if self.sid or self.login(): + api_ready = True + logger.info(f"API services are ready after {api_check + 1} attempts") + break + logger.debug(f"API not ready yet, waiting... ({api_check + 1}/5)") + sleep(10) + + if not api_ready: + logger.warning("System is online but API services may not be fully initialized") + + # Дадим дополнительное время для полной загрузки всех сервисов + sleep(delay) + return True + + sleep(delay) + logger.info(f"Waiting for boot... Attempt {attempt + 1}/{max_attempts}") + + logger.error(f"Synology NAS didn't come online after {max_attempts * delay} seconds") + return False + + def power_on(self) -> bool: + """Включение Synology NAS""" + # Принудительная проверка статуса + if self.is_online(force_check=True): + logger.info("Synology NAS is already online") + return True + + logger.info("Powering on Synology NAS via Wake-on-LAN...") + + # Проверяем, настроен ли MAC-адрес + if not SYNOLOGY_MAC: + logger.error("Cannot power on: MAC address not configured in settings") + return False + + # Пробуем отправить несколько WoL пакетов для надежности + success = False + for attempt in range(3): + logger.debug(f"Sending WoL packet, attempt {attempt + 1}/3") + if self.wake_on_lan(): + success = True + break + sleep(1) + + if not success: + logger.error("Failed to send Wake-on-LAN packets") + return False + + # Ожидание загрузки + logger.info("WoL packets sent successfully, waiting for system to boot...") + boot_result = self.wait_for_boot(max_attempts=30, delay=10) + + if boot_result: + # Проверяем доступность API после загрузки + system_status = self.get_system_status() + if system_status.get("status") == "online": + logger.info("System booted successfully with API access") + return True + else: + logger.warning("System appears to be online but API may not be fully ready") + return True + else: + logger.error("System did not come online after WoL") + return False + + def power_off(self) -> bool: + """Выключение Synology NAS""" + if not self.is_online(force_check=True): + logger.info("Synology NAS is already offline") + return True + + logger.info("Powering off Synology NAS...") + result = self.shutdown_system() + + if result: + # Дополнительная проверка, что система действительно выключилась + logger.info("Verifying system is offline...") + for attempt in range(12): # Проверяем в течение 2 минут (12 * 10 сек) + if not self.is_online(force_check=True): + logger.info(f"System confirmed offline after {attempt * 10} seconds") + return True + logger.debug(f"System still shutting down, checking again... ({attempt + 1}/12)") + sleep(10) + + logger.warning("System still appears to be online after shutdown command") + return False + else: + logger.error("Failed to initiate shutdown") + return False + + # Заглушки для расширенных методов + def get_shared_folders(self) -> List[Dict[str, Any]]: + """Получение списка общих папок (заглушка)""" + logger.warning("Function get_shared_folders() is not implemented yet") + return [] + + def get_system_load(self) -> Dict[str, Any]: + """Получение информации о загрузке системы (заглушка)""" + logger.warning("Function get_system_load() is not implemented yet") + return {} + + def get_storage_status(self) -> Dict[str, Any]: + """Получение подробной информации о хранилище (заглушка)""" + logger.warning("Function get_storage_status() is not implemented yet") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for storage status request") + return {"error": "authentication_failed"} + + try: + # Здесь будет реализация после добавления соответствующего API + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.Storage.CGI.Storage", + "version": "1", + "method": "load_info", + "sid": self.sid + } + + # В текущей версии просто возвращаем заглушку + return { + "volumes": [], + "disks": [], + "total_size": 0, + "total_used": 0, + "error": "not_implemented" + } + except Exception as e: + logger.error(f"Error in get_storage_status: {str(e)}") + return {"error": str(e)} + + def get_security_status(self) -> Dict[str, Any]: + """Получение информации о состоянии безопасности (заглушка)""" + logger.warning("Function get_security_status() is not implemented yet") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for security status request") + return {"success": False, "error": "authentication_failed"} + + try: + # Здесь будет реализация после добавления соответствующего API + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.Core.SecurityScan.Status", + "version": "1", + "method": "get", + "sid": self.sid + } + + # В текущей версии просто возвращаем заглушку + return { + "success": False, + "status": "not_implemented", + "last_check": None, + "is_secure": False, + "error": "not_implemented" + } + except Exception as e: + logger.error(f"Error in get_security_status: {str(e)}") + return {"success": False, "error": str(e)} diff --git a/.history/src/api/synology_20250830084539.py b/.history/src/api/synology_20250830084539.py new file mode 100644 index 0000000..a226ac0 --- /dev/null +++ b/.history/src/api/synology_20250830084539.py @@ -0,0 +1,1204 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Модуль для взаимодействия с API Synology NAS +""" + +import requests +from requests.adapters import HTTPAdapter +import json +import logging +import time +import urllib3 +from urllib3.util import Retry +from typing import Dict, Any, Optional, List +import socket +import struct +from time import sleep + +from src.config.config import ( + SYNOLOGY_HOST, + SYNOLOGY_PORT, + SYNOLOGY_USERNAME, + SYNOLOGY_PASSWORD, + SYNOLOGY_SECURE, + SYNOLOGY_TIMEOUT, + SYNOLOGY_MAC, + WOL_PORT, + SYNOLOGY_API_VERSION, + SYNOLOGY_POWER_API, + SYNOLOGY_INFO_API +) +from src.api.api_discovery import discover_available_apis, find_compatible_api + +# Отключение предупреждений о небезопасных SSL-соединениях +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + +logger = logging.getLogger(__name__) + +class SynologyAPI: + """Класс для взаимодействия с API Synology NAS""" + + def __init__(self): + """Инициализация класса SynologyAPI""" + logger.info("Creating API with auto-retry and connection pool") + logger.debug(f"Connection details: Host={SYNOLOGY_HOST}, Port={SYNOLOGY_PORT}, Secure={SYNOLOGY_SECURE}") + + self.protocol = "https" if SYNOLOGY_SECURE else "http" + self.base_url = f"{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi" + self.sid = None + self.session = requests.Session() + + # Настройка SSL + if self.protocol == "https": + logger.debug("SSL enabled, disabling certificate verification for internal network") + self.session.verify = False # Отключаем проверку SSL для внутренней сети + + # Добавляем пользовательские заголовки для улучшения совместимости с API + custom_headers = { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36', + 'Accept': 'application/json, text/javascript, */*; q=0.01', + 'Accept-Language': 'ru-RU,ru;q=0.9,en-US;q=0.8,en;q=0.7', + 'X-Requested-With': 'XMLHttpRequest', + 'Connection': 'keep-alive', + 'Referer': f'{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/' + } + self.session.headers.update(custom_headers) + logger.debug("Added browser-like headers for API compatibility") + + # Добавляем повторные попытки для HTTP-запросов + retry_strategy = Retry( + total=5, # Увеличиваем количество попыток + status_forcelist=[429, 500, 502, 503, 504, 404], + allowed_methods=["GET", "POST"], + backoff_factor=1.5, # Увеличиваем задержку между попытками + respect_retry_after_header=True + ) + adapter = HTTPAdapter( + max_retries=retry_strategy, + pool_connections=3, + pool_maxsize=10 + ) + self.session.mount("http://", adapter) + self.session.mount("https://", adapter) + + # Таймауты будут указаны в запросах + self.default_timeout = (SYNOLOGY_TIMEOUT, SYNOLOGY_TIMEOUT*2) + logger.debug(f"Setting default request timeout: {self.default_timeout}") + + # Кэш для хранения результатов запросов + self._cache = {} + self._cache_ttl = {} + self._last_online_check = 0 + self._last_online_status = False + self._online_check_interval = 30 # Интервал для кэширования проверок онлайн-статуса + + # Время последней успешной аутентификации и срок действия сессии + self._last_auth_time = 0 + self._auth_expiry = 3600 # По умолчанию 1 час + + # Информация о доступных API + self._available_apis = {} + self._api_info_ttl = 0 + + # Инициализируем API version resolver для автоматического определения совместимых API + self.api_resolver = None # Будет создан при необходимости + + def login(self) -> bool: + """Авторизация в API Synology NAS""" + # Сбрасываем SID для новой сессии + self.sid = None + + logger.info("Attempting to authenticate with Synology NAS...") + logger.debug(f"Base URL: {self.base_url}") + + # Проверяем доступность NAS перед запросом с помощью прямой TCP-проверки + # Избегаем вызова is_online(), чтобы не создавать рекурсию + online_status = self._check_tcp_connection() + if not online_status: + logger.error("Cannot login: Synology NAS is not reachable") + return False + + # Пробуем различные версии API для аутентификации + auth_versions_to_try = [6, 3, 2, 1] # Пробуем от более новых к более старым версиям + + for auth_version in auth_versions_to_try: + try: + # Определяем путь к API аутентификации + auth_path = "auth.cgi" # Значение по умолчанию + + # Проверка информации API для определения доступных версий API + api_info_url = f"{self.base_url}/entry.cgi" + api_info_params = { + "api": "SYNO.API.Info", + "version": "1", + "method": "query", + "query": "SYNO.API.Auth" + } + + logger.debug(f"Querying API info for auth version {auth_version}") + try: + api_info_response = self.session.get( + api_info_url, + params=api_info_params, + timeout=self.default_timeout, + verify=False + ) + + if api_info_response.status_code == 200: + api_info_data = api_info_response.json() + if api_info_data.get("success"): + auth_info = api_info_data.get("data", {}).get("SYNO.API.Auth", {}) + max_version = auth_info.get("maxVersion", 6) + min_version = auth_info.get("minVersion", 1) + auth_path = auth_info.get("path", "auth.cgi") + logger.debug(f"Auth API versions: min={min_version}, max={max_version}, path={auth_path}") + + # Проверяем поддержку текущей версии + if auth_version < min_version or auth_version > max_version: + logger.warning(f"Auth API version {auth_version} not supported (min={min_version}, max={max_version}), trying next version") + continue + else: + logger.warning("Failed to query API info, using default auth path") + else: + logger.warning(f"API info request failed with status {api_info_response.status_code}, using default auth path") + except Exception as e: + logger.warning(f"Error querying API info: {str(e)}, using default auth path") + + # Основной запрос авторизации + url = f"{self.base_url}/{auth_path}" + params = { + "api": "SYNO.API.Auth", + "version": str(auth_version), + "method": "login", + "account": SYNOLOGY_USERNAME, + "passwd": SYNOLOGY_PASSWORD, + "session": "SynologyPowerControlBot", + "format": "cookie" + } + + # Для версии 6+ используем немного другой формат + if auth_version >= 6: + params["enable_syno_token"] = "yes" + + logger.debug(f"Sending auth request to {url} with API version {auth_version}") + start_time = time.time() + response = self.session.get( + url, + params=params, + timeout=self.default_timeout, + verify=False + ) + elapsed_time = time.time() - start_time + logger.debug(f"Auth request completed in {elapsed_time:.2f}s with status code {response.status_code}") + + # Проверка статуса HTTP + if response.status_code != 200: + logger.error(f"HTTP error: {response.status_code}") + logger.debug(f"Response content: {response.text[:200]}") + continue # Пробуем следующую версию + + try: + data = response.json() + except json.JSONDecodeError: + logger.error("Failed to decode JSON response") + logger.debug(f"Response content: {response.text[:200]}") + continue # Пробуем следующую версию + + if data.get("success"): + self.sid = data.get("data", {}).get("sid") + self._last_auth_time = time.time() + logger.info(f"Successfully logged in to Synology NAS using API version {auth_version}") + logger.debug(f"Session ID obtained: {self.sid[:5] if self.sid else 'None'}...") + + # Проверка валидности полученной сессии + if not self._validate_session(): + logger.warning("Session validation failed, trying next auth version") + self.sid = None + continue + + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to log in with version {auth_version}: Error code {error_code}") + + # Если ошибка связана с версией API, пробуем следующую версию + if error_code in [104, 105]: + logger.warning(f"Auth version {auth_version} not supported, trying next version") + continue + + # Дополнительная диагностика + if error_code == 400: + logger.error("Authentication error: Invalid credentials") + elif error_code == 401: + logger.error("Authentication error: Account disabled") + elif error_code == 402: + logger.error("Authentication error: Permission denied") + elif error_code == 403: + logger.error("Authentication error: 2-factor authentication required") + elif error_code == 404: + logger.error("Authentication error: Failed to authenticate with 2-factor authentication") + + # Если ошибка связана с неверными учетными данными, нет смысла пробовать другие версии API + if error_code in [400, 401, 402, 403, 404]: + return False + + except requests.exceptions.Timeout: + logger.error(f"Connection timeout during auth with version {auth_version}") + continue # Пробуем следующую версию + except requests.exceptions.ConnectionError as e: + logger.error(f"Connection error during auth with version {auth_version}: {str(e)}") + continue # Пробуем следующую версию + except requests.RequestException as e: + logger.error(f"Request error during auth with version {auth_version}: {str(e)}") + continue # Пробуем следующую версию + except Exception as e: + logger.error(f"Unexpected error during login with version {auth_version}: {str(e)}", exc_info=True) + continue # Пробуем следующую версию + + # Если все версии не сработали + logger.error("Failed to authenticate with any API version") + return False + + def _validate_session(self) -> bool: + """Проверяет валидность сессии после авторизации""" + if not self.sid: + return False + + # Попробуем сделать простой запрос для проверки сессии + test_apis = [ + {"api": "SYNO.Core.System", "method": "info", "version": 1}, + {"api": "SYNO.DSM.Info", "method": "getinfo", "version": 1} + ] + + for test_api in test_apis: + try: + url = f"{self.base_url}/entry.cgi" + params = { + "api": test_api["api"], + "version": str(test_api["version"]), + "method": test_api["method"], + "_sid": self.sid + } + + response = self.session.get(url, params=params, timeout=self.default_timeout, verify=False) + + if response.status_code == 200: + data = response.json() + if data.get("success"): + logger.debug(f"Session validation successful using {test_api['api']}") + return True + else: + error_code = data.get("error", {}).get("code", -1) + if error_code != 119: # Не сессия истекла + logger.debug(f"Session validation with {test_api['api']} returned error code {error_code}") + return True # Считаем сессию валидной, если ошибка не связана с истечением сессии + except Exception as e: + logger.warning(f"Error during session validation with {test_api['api']}: {str(e)}") + + logger.warning("Session validation failed with all test APIs") + return False + + def logout(self) -> bool: + """Выход из API Synology NAS""" + if not self.sid: + return True + + try: + url = f"{self.base_url}/auth.cgi" + params = { + "api": "SYNO.API.Auth", + "version": "1", + "method": "logout", + "session": "SynologyPowerControlBot", + "_sid": self.sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + self.sid = None + logger.info("Successfully logged out from Synology NAS") + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to log out from Synology NAS: Error code {error_code}") + return False + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + return False + + def _make_api_request(self, api_name: str, method: str, version: int = 1, + params: Optional[Dict[str, Any]] = None, retry_auth: bool = True, retry_count: int = 0) -> Optional[Dict[str, Any]]: + """Обобщенный метод для выполнения API запросов с обработкой ошибок""" + # Ограничение на количество повторных попыток + if retry_count >= 3: + logger.error(f"Too many retries for {api_name}.{method}, giving up") + return None + + # Проверка наличия авторизации + if not self.sid and not self.login(): + logger.error(f"Not authenticated for API request: {api_name}.{method}") + return None + + # Проверка информации API для определения пути и поддерживаемой версии + api_info_url = f"{self.base_url}/entry.cgi" + api_info_params = { + "api": "SYNO.API.Info", + "version": "1", + "method": "query", + "query": api_name + } + + api_path = "entry.cgi" + try: + logger.debug(f"Querying API info for {api_name}") + api_info_response = self.session.get( + api_info_url, + params=api_info_params, + timeout=self.default_timeout, + verify=False + ) + + if api_info_response.status_code == 200: + api_info_data = api_info_response.json() + if api_info_data.get("success"): + api_info = api_info_data.get("data", {}).get(api_name, {}) + if api_info: + max_version = api_info.get("maxVersion", version) + min_version = api_info.get("minVersion", version) + api_path = api_info.get("path", "entry.cgi") + + # Проверка, поддерживается ли запрошенная версия + if version < min_version: + logger.warning(f"API version {version} for {api_name} is below minimum {min_version}, using {min_version}") + version = min_version + elif version > max_version: + logger.warning(f"API version {version} for {api_name} exceeds maximum {max_version}, using {max_version}") + version = max_version + + logger.debug(f"Using API path: {api_path}, version: {version}") + else: + logger.warning(f"API {api_name} not found in API info, using defaults") + except Exception as e: + logger.warning(f"Error querying API info for {api_name}: {str(e)}, using defaults") + + # Подготовка базовых параметров запроса + base_params = { + "api": api_name, + "version": str(version), + "method": method, + "_sid": self.sid # Используем _sid вместо sid для лучшей совместимости + } + + # Добавление дополнительных параметров, если они заданы + if params: + base_params.update(params) + + url = f"{self.base_url}/{api_path}" + logger.debug(f"API request: {api_name}.{method} v{version} to {url}") + logger.debug(f"Full request params: {base_params}") + + try: + start_time = time.time() + response = self.session.get( + url, + params=base_params, + timeout=self.default_timeout, + verify=False + ) + elapsed_time = time.time() - start_time + logger.debug(f"API request completed in {elapsed_time:.2f}s with status {response.status_code}") + + # Проверка статуса HTTP + if response.status_code != 200: + logger.error(f"HTTP error: {response.status_code} for {api_name}.{method}") + + # Повторная попытка при ошибках соединения + if response.status_code in [500, 502, 503, 504]: + logger.info(f"Server error, retrying request for {api_name}.{method}") + sleep(2) + return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1) + + return None + + try: + data = response.json() + except json.JSONDecodeError: + logger.error(f"Failed to decode JSON response for {api_name}.{method}") + logger.debug(f"Response content: {response.text[:200]}") + + # Повторная попытка при ошибках декодирования + logger.info(f"JSON decode error, retrying request for {api_name}.{method}") + sleep(2) + return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1) + + if data.get("success"): + logger.info(f"API request successful for {api_name}.{method}") + return data.get("data", {}) + else: + error_code = data.get("error", {}).get("code", -1) + error_desc = self._get_error_description(error_code) + logger.error(f"API error for {api_name}.{method}: {error_code} - {error_desc}") + + # Ошибки доступа или прав часто встречаются, но они не критичные + # Например, ошибка 102 означает, что нет прав, но NAS доступен + if error_code in [102, 103, 104, 105]: + logger.warning(f"Permission-related error for {api_name}.{method}: {error_code}") + # Возвращаем пустой словарь вместо None, + # чтобы вызывающий код мог понять, что запрос выполнен + return {} + + # Если ошибка связана с авторизацией и нам разрешено повторить попытку + if error_code in [106, 107, 119] and retry_auth: + logger.info(f"Session error (code {error_code}), creating fresh session...") + self.sid = None # Сбрасываем SID + + # Для ошибки 119 (Session timeout) дадим системе немного времени + if error_code == 119: + logger.info("Session timeout detected, waiting before retry...") + sleep(3) + + if self.login(): + logger.info("Re-authenticated with fresh session, retrying API request...") + # Рекурсивный вызов, но со счетчиком повторов + return self._make_api_request(api_name, method, version, params, False, retry_count + 1) + + # Для некоторых ошибок можно автоматически повторить запрос + if error_code in [408, 429, 500, 502, 503, 504]: + logger.info(f"Temporary error {error_code}, retrying request for {api_name}.{method}") + sleep(2) + return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1) + + return None + + except requests.exceptions.Timeout: + logger.error(f"Request timeout for {api_name}.{method}") + + # Повторная попытка при таймауте + if retry_count < 2: + logger.info(f"Timeout, retrying request for {api_name}.{method}") + sleep(2) + return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1) + + return None + except requests.exceptions.ConnectionError as e: + logger.error(f"Connection error for {api_name}.{method}: {str(e)}") + + # Повторная попытка при ошибке соединения + if retry_count < 2: + logger.info(f"Connection error, retrying request for {api_name}.{method}") + sleep(2) + return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1) + + return None + except Exception as e: + logger.error(f"Unexpected error during {api_name}.{method}: {str(e)}") + return None + + def get_system_status(self) -> Dict[str, Any]: + """Получение статуса системы""" + # Проверяем доступность системы + if not self.is_online(): + logger.info("Device is offline, skipping API request") + return {"status": "offline"} + + # Проверяем, есть ли кэшированный результат + cache_key = "system_status" + current_time = time.time() + if cache_key in self._cache and current_time - self._cache_ttl.get(cache_key, 0) < 60: + logger.debug("Using cached system status") + return self._cache[cache_key] + + # Используем рекомендованный API для получения информации о системе + logger.info(f"Getting system info using {SYNOLOGY_INFO_API} v{SYNOLOGY_API_VERSION}") + + # Для SYNO.DSM.Info метод getinfo, для других обычно используется get или info + if SYNOLOGY_INFO_API == "SYNO.DSM.Info": + method = "getinfo" + else: + method = "get" + + result = self._make_api_request(SYNOLOGY_INFO_API, method, version=SYNOLOGY_API_VERSION) + + if result: + logger.info(f"Successfully retrieved system info using {SYNOLOGY_INFO_API}") + + # Формируем расширенный ответ с дополнительной информацией + system_info = { + "status": "online", + "hostname": result.get("hostname", "unknown"), + "model": result.get("model", "unknown"), + "version": result.get("version", "unknown"), + "uptime": result.get("uptime", 0), + "time": current_time, + "is_online": True, + "api_used": SYNOLOGY_INFO_API + } + + # Сохраняем в кэше + self._cache[cache_key] = system_info + self._cache_ttl[cache_key] = current_time + + return system_info + + # Если основной API не сработал, пробуем резервные варианты + logger.warning(f"Failed to retrieve system info with {SYNOLOGY_INFO_API}, trying fallback APIs") + + # Пробуем резервные API + apis_to_try = [ + {"name": "SYNO.DSM.Info", "method": "getinfo", "version": 2}, + {"name": "SYNO.Core.System", "method": "info", "version": 1}, + {"name": "SYNO.Core.System.Status", "method": "get", "version": 1}, + {"name": "SYNO.Core.System.Info", "method": "get", "version": 1}, + ] + + for api in apis_to_try: + if api["name"] == SYNOLOGY_INFO_API: + continue # Пропускаем уже проверенный API + + logger.info(f"Trying to get system info using {api['name']}.{api['method']} v{api['version']}") + result = self._make_api_request(api["name"], api["method"], version=api["version"]) + + if result: + logger.info(f"Successfully retrieved system info using {api['name']}") + + # Формируем расширенный ответ с дополнительной информацией + system_info = { + "status": "online", + "hostname": result.get("hostname", "unknown"), + "model": result.get("model", "unknown"), + "version": result.get("version", "unknown"), + "uptime": result.get("uptime", 0), + "time": current_time, + "is_online": True, + "api_used": api["name"] + } + + # Сохраняем в кэше + self._cache[cache_key] = system_info + self._cache_ttl[cache_key] = current_time + + return system_info + + # Если все запросы не удались, но система онлайн, возвращаем базовую информацию + logger.warning("Failed to retrieve system info with all API methods") + return { + "status": "error", + "error": "Failed to fetch system information", + "is_online": True, + "time": current_time + } + + def shutdown_system(self) -> bool: + """Выключение системы""" + # Проверяем, включено ли устройство перед попыткой его выключить + if not self.is_online(force_check=True): + logger.info("Synology NAS is already offline, no need to shut down") + return True + + logger.info("Attempting to shutdown Synology NAS...") + + # Попробуем сначала использовать предпочтительный API для управления питанием + logger.info(f"Trying shutdown with {SYNOLOGY_POWER_API} v{SYNOLOGY_API_VERSION}") + + # Для SYNO.Core.Hardware.PowerRecovery используется метод setPowerOnState + # Для других API обычно используется метод shutdown или reboot + if SYNOLOGY_POWER_API == "SYNO.Core.Hardware.PowerRecovery": + # Для этого API нужны специальные параметры + params = {"state": "powerbtn"} # powerbtn имитирует нажатие кнопки питания + result = self._make_api_request(SYNOLOGY_POWER_API, "setPowerOnState", version=SYNOLOGY_API_VERSION, params=params) + else: + # Пробуем стандартный метод + result = self._make_api_request(SYNOLOGY_POWER_API, "shutdown", version=SYNOLOGY_API_VERSION) + + if result is not None: + logger.info(f"Successfully initiated system shutdown using {SYNOLOGY_POWER_API}") + + # Даем системе время начать процесс выключения + logger.info("Waiting for shutdown to initialize...") + sleep(5) + + # Проверяем статус + if not self.is_online(force_check=True): + logger.info("System is now offline. Shutdown confirmed successful.") + return True + else: + logger.info("System still appears to be online, but shutdown may be in progress.") + return True + else: + logger.warning(f"Failed to shutdown using {SYNOLOGY_POWER_API}, trying alternative methods") + + # Если не сработал основной метод, пробуем резервные варианты + # Проверка всех доступных методов API для выключения + apis_to_try = [ + {"name": "SYNO.Core.System", "method": "shutdown", "version": 3}, + {"name": "SYNO.Core.System.Power", "method": "shutdown", "version": 1}, + {"name": "SYNO.DSM.Power", "method": "shutdown", "version": 1}, + {"name": "SYNO.System.Power", "method": "shutdown", "version": 1}, + {"name": "SYNO.DSM.System", "method": "shutdown", "version": 1} + ] + + # Проверяем доступные API + try: + api_info_url = f"{self.base_url}/entry.cgi" + api_info_params = { + "api": "SYNO.API.Info", + "version": "1", + "method": "query", + "query": "SYNO.Core.System.Power,SYNO.DSM.Power,SYNO.Core.System,SYNO.System.Power,SYNO.DSM.System" + } + + logger.debug("Checking available shutdown APIs") + api_info_response = self.session.get( + api_info_url, + params=api_info_params, + timeout=self.default_timeout, + verify=False + ) + + if api_info_response.status_code == 200: + api_info_data = api_info_response.json() + if api_info_data.get("success"): + available_apis = api_info_data.get("data", {}) + logger.debug(f"Available APIs: {list(available_apis.keys())}") + + # Фильтруем только доступные API + filtered_apis = [] + for api in apis_to_try: + if api["name"] in available_apis: + api_info = available_apis[api["name"]] + # Проверка версии API + if api_info.get("minVersion", 1) <= api["version"] <= api_info.get("maxVersion", 1): + filtered_apis.append(api) + logger.debug(f"Adding {api['name']} to available shutdown APIs") + else: + logger.debug(f"API {api['name']} version {api['version']} not supported. Supported: {api_info.get('minVersion')}-{api_info.get('maxVersion')}") + + if filtered_apis: + apis_to_try = filtered_apis + else: + logger.warning("No compatible APIs found, trying all methods as fallback") + else: + logger.warning("Failed to query API info, using default methods") + else: + logger.warning(f"API info request failed with status {api_info_response.status_code}, using default methods") + except Exception as e: + logger.warning(f"Error querying API info: {str(e)}, using default methods") + + # Пробуем все доступные методы по порядку + for api in apis_to_try: + logger.info(f"Trying shutdown with {api['name']}.{api['method']} v{api['version']}") + result = self._make_api_request(api["name"], api["method"], version=api["version"]) + + if result is not None: + logger.info(f"Successfully initiated system shutdown using {api['name']}") + + # Даем системе время начать процесс выключения + logger.info("Waiting for shutdown to initialize...") + sleep(5) + + # Проверяем статус + if not self.is_online(force_check=True): + logger.info("System is now offline. Shutdown confirmed successful.") + return True + else: + logger.info("System still appears to be online, but shutdown may be in progress.") + return True + else: + logger.warning(f"Failed to shutdown using {api['name']}.{api['method']} v{api['version']}") + + # Если ни один метод не сработал, но система стала недоступна + if not self.is_online(force_check=True): + logger.info("System appears to be shutting down despite API errors") + return True + + logger.error("Failed to shutdown system after trying multiple APIs") + return False + + def reboot_system(self) -> bool: + """Перезагрузка системы""" + # Проверяем, включена ли система + if not self.is_online(force_check=True): + logger.error("Cannot reboot: System is offline") + return False + + logger.info("Attempting to reboot Synology NAS...") + + # Попробуем сначала использовать предпочтительный API для управления питанием + logger.info(f"Trying reboot with {SYNOLOGY_POWER_API} v{SYNOLOGY_API_VERSION}") + + # Для SYNO.Core.Hardware.PowerRecovery используется метод setPowerOnState или powerButton + # Для других API обычно используется метод restart или reboot + if SYNOLOGY_POWER_API == "SYNO.Core.Hardware.PowerRecovery": + # Для этого API нужны специальные параметры + params = {"reboot": "true"} # Передаем флаг для перезагрузки + result = self._make_api_request(SYNOLOGY_POWER_API, "setPowerOnState", version=SYNOLOGY_API_VERSION, params=params) + else: + # Пробуем стандартный метод + methods_to_try = ["restart", "reboot"] + result = None + for method in methods_to_try: + result = self._make_api_request(SYNOLOGY_POWER_API, method, version=SYNOLOGY_API_VERSION) + if result is not None: + logger.info(f"Successfully used method {method} for {SYNOLOGY_POWER_API}") + break + + if result is not None: + logger.info(f"Successfully initiated system reboot using {SYNOLOGY_POWER_API}") + + # Даем системе время начать процесс перезагрузки + logger.info("Waiting for reboot to initialize...") + sleep(5) + + # Ждем, пока система станет недоступна (признак перезагрузки) + reboot_started = False + for i in range(12): # Проверяем в течение 60 секунд (12 * 5сек) + if not self.is_online(force_check=True): + logger.info(f"System went offline after {i*5} seconds, reboot in progress") + reboot_started = True + break + logger.debug(f"System still online, waiting... ({i+1}/12)") + sleep(5) + + if reboot_started: + # Ждем, пока система снова станет доступна + logger.info("Waiting for system to come back online...") + return self.wait_for_boot(max_attempts=30, delay=10) + else: + logger.warning("System did not go offline after reboot command, but command was accepted") + # Даже если система не ушла оффлайн, команда могла быть принята + return True + else: + logger.warning(f"Failed to reboot using {SYNOLOGY_POWER_API}, trying alternative methods") + + # Если не сработал основной метод, пробуем резервные варианты + # Проверка всех доступных методов API для перезагрузки + apis_to_try = [ + {"name": "SYNO.Core.System.Power", "method": "restart", "version": 1}, + {"name": "SYNO.DSM.Power", "method": "reboot", "version": 1}, + {"name": "SYNO.Core.System", "method": "reboot", "version": 3}, + {"name": "SYNO.System.Power", "method": "reboot", "version": 1}, + {"name": "SYNO.DSM.System", "method": "reboot", "version": 1}, + {"name": "SYNO.Core.Hardware.NeedReboot", "method": "reboot", "version": 1} + ] + + # Получаем список доступных API + try: + api_info_url = f"{self.base_url}/entry.cgi" + api_info_params = { + "api": "SYNO.API.Info", + "version": "1", + "method": "query", + "query": "SYNO.Core.System.Power,SYNO.DSM.Power,SYNO.Core.System,SYNO.System.Power,SYNO.DSM.System,SYNO.Core.Hardware.NeedReboot" + } + + logger.debug("Checking available reboot APIs") + api_info_response = self.session.get( + api_info_url, + params=api_info_params, + timeout=self.default_timeout, + verify=False + ) + + if api_info_response.status_code == 200: + api_info_data = api_info_response.json() + if api_info_data.get("success"): + available_apis = api_info_data.get("data", {}) + logger.debug(f"Available APIs: {list(available_apis.keys())}") + + # Фильтруем только доступные API + filtered_apis = [] + for api in apis_to_try: + if api["name"] in available_apis: + api_info = available_apis[api["name"]] + # Проверка версии API + if api_info.get("minVersion", 1) <= api["version"] <= api_info.get("maxVersion", 1): + filtered_apis.append(api) + logger.debug(f"Adding {api['name']} to available reboot APIs") + else: + logger.debug(f"API {api['name']} version {api['version']} not supported. Supported: {api_info.get('minVersion')}-{api_info.get('maxVersion')}") + + if filtered_apis: + apis_to_try = filtered_apis + else: + logger.warning("No compatible APIs found, trying all methods as fallback") + else: + logger.warning("Failed to query API info, using default methods") + else: + logger.warning(f"API info request failed with status {api_info_response.status_code}, using default methods") + except Exception as e: + logger.warning(f"Error querying API info: {str(e)}, using default methods") + + # Пробуем все доступные методы по порядку + for api in apis_to_try: + logger.info(f"Trying reboot with {api['name']}.{api['method']} v{api['version']}") + result = self._make_api_request(api["name"], api["method"], version=api["version"]) + + if result is not None: + logger.info(f"Successfully initiated system reboot using {api['name']}") + + # Даем системе время начать процесс перезагрузки + logger.info("Waiting for reboot to initialize...") + sleep(5) + + # Ждем, пока система станет недоступна (признак перезагрузки) + reboot_started = False + for i in range(12): # Проверяем в течение 60 секунд (12 * 5сек) + if not self.is_online(force_check=True): + logger.info(f"System went offline after {i*5} seconds, reboot in progress") + reboot_started = True + break + logger.debug(f"System still online, waiting... ({i+1}/12)") + sleep(5) + + if reboot_started: + # Ждем, пока система снова станет доступна + logger.info("Waiting for system to come back online...") + return self.wait_for_boot(max_attempts=30, delay=10) + else: + logger.warning("System did not go offline after reboot command, but command was accepted") + # Даже если система не ушла оффлайн, команда могла быть принята + return True + else: + logger.warning(f"Failed to reboot using {api['name']}.{api['method']} v{api['version']}") + + logger.error("Failed to reboot system after trying multiple APIs") + return False + + def _get_error_description(self, error_code: int) -> str: + """Получение описания ошибки по коду""" + error_descriptions = { + 100: "Unknown error", + 101: "Invalid parameter", + 102: "API does not exist", + 103: "Method does not exist", + 104: "Version does not support", + 105: "Permission denied", + 106: "Session timeout", + 107: "Session interrupted by duplicate login", + 400: "Invalid credentials", + 401: "Account disabled", + 402: "Permission denied", + 403: "2FA required", + 404: "Failed to authenticate with 2FA" + } + return error_descriptions.get(error_code, "Unknown error code") + + def _check_tcp_connection(self) -> bool: + """Проверка базового TCP-соединения с Synology NAS""" + try: + socket_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + socket_obj.settimeout(SYNOLOGY_TIMEOUT) + result = socket_obj.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT)) + socket_obj.close() + + return result == 0 + except socket.error as e: + logger.error(f"Socket error during connection check: {str(e)}") + return False + except Exception as e: + logger.error(f"Unexpected error during connection check: {str(e)}") + return False + + def is_online(self, force_check=False) -> bool: + """Проверка онлайн-статуса Synology NAS""" + # Используем кэшированное значение, если доступно и не устарело + current_time = time.time() + if not force_check and (current_time - self._last_online_check) < self._online_check_interval: + logger.debug(f"Using cached online status: {self._last_online_status}") + return self._last_online_status + + logger.info("Checking if NAS is online...") + + # Проверяем TCP-соединение + online_status = self._check_tcp_connection() + logger.info(f"Detected Synology NAS online status: {online_status}") + + # Если TCP-соединение успешно и у нас есть действующий SID, + # попробуем более детальную проверку через API + if online_status and self.sid: + logger.info("Trying to fetch more detailed online status through API...") + + # Пробуем разные API для проверки онлайн-статуса + api_checks = [ + {"api": "SYNO.DSM.Info", "version": "2", "method": "getinfo"}, + {"api": "SYNO.Core.System", "version": "1", "method": "info"}, + {"api": "SYNO.Core.System.Status", "version": "1", "method": "get"} + ] + + api_success = False + for api_check in api_checks: + try: + url = f"{self.base_url}/entry.cgi" + params = { + "api": api_check["api"], + "version": api_check["version"], + "method": api_check["method"], + "sid": self.sid + } + + logger.debug(f"Trying online status check with {api_check['api']}") + response = self.session.get( + url, + params=params, + timeout=self.default_timeout, + verify=False + ) + + if response.status_code == 200: + data = response.json() + if data.get("success"): + logger.info(f"API request successful for {api_check['api']}") + logger.info("Synology NAS is online with API access") + api_success = True + break + else: + error_code = data.get("error", {}).get("code", -1) + logger.warning(f"API response indicates an error: {error_code}, but NAS is reachable") + else: + logger.warning(f"API returned status code {response.status_code}") + except Exception as e: + logger.warning(f"API check failed for {api_check['api']}, but TCP connection succeeded: {str(e)}") + + if not api_success: + logger.warning("All API checks failed, but TCP connection is successful") + + # Обновляем кэшированное значение + self._last_online_check = current_time + self._last_online_status = online_status + + return online_status + + def wake_on_lan(self) -> bool: + """Отправка Wake-on-LAN пакета для включения Synology NAS""" + if not SYNOLOGY_MAC: + logger.error("MAC address not configured") + return False + + try: + # Преобразование MAC-адреса в байты + mac_address = SYNOLOGY_MAC.replace(':', '').replace('-', '') + if len(mac_address) != 12: + logger.error(f"Invalid MAC address format: {SYNOLOGY_MAC}") + return False + + try: + mac_bytes = bytes.fromhex(mac_address) + except ValueError as e: + logger.error(f"Failed to parse MAC address: {str(e)}") + logger.error(f"MAC address must contain only hex characters: {SYNOLOGY_MAC}") + return False + + # Создание Magic Packet + magic_packet = b'\xff' * 6 + mac_bytes * 16 + + # Отправка пакета на конкретный адрес + try: + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) + sock.sendto(magic_packet, (SYNOLOGY_HOST, WOL_PORT)) + sock.close() + logger.info(f"Wake-on-LAN packet sent to {SYNOLOGY_MAC} at {SYNOLOGY_HOST}:{WOL_PORT}") + except Exception as e: + logger.error(f"Error sending directed WoL packet: {str(e)}") + return False + + # Для надежности отправляем также широковещательный пакет + try: + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) + + # Используем стандартный широковещательный адрес + broadcast_addr = "255.255.255.255" + sock.sendto(magic_packet, (broadcast_addr, WOL_PORT)) + sock.close() + logger.info(f"Wake-on-LAN broadcast packet sent to {broadcast_addr}:{WOL_PORT}") + except Exception as e: + logger.warning(f"Error sending broadcast WoL packet (non-critical): {str(e)}") + # Не считаем ошибкой, т.к. основной пакет уже отправлен + + return True + + except Exception as e: + logger.error(f"Unexpected error in wake_on_lan: {str(e)}") + return False + + def wait_for_boot(self, max_attempts: int = 30, delay: int = 10) -> bool: + """Ожидание загрузки Synology NAS""" + logger.info(f"Waiting for Synology NAS to boot (timeout: {max_attempts * delay}s)...") + + for attempt in range(max_attempts): + # Принудительно проверяем статус без использования кэша + if self.is_online(force_check=True): + logger.info(f"Synology NAS is online after {attempt + 1} attempts ({(attempt + 1) * delay}s)") + + # Проверяем, что не только сеть доступна, но и API загрузился + api_ready = False + logger.info("Waiting for API services to initialize...") + + for api_check in range(5): # Даем еще до 50 секунд для загрузки API + if self.sid or self.login(): + api_ready = True + logger.info(f"API services are ready after {api_check + 1} attempts") + break + logger.debug(f"API not ready yet, waiting... ({api_check + 1}/5)") + sleep(10) + + if not api_ready: + logger.warning("System is online but API services may not be fully initialized") + + # Дадим дополнительное время для полной загрузки всех сервисов + sleep(delay) + return True + + sleep(delay) + logger.info(f"Waiting for boot... Attempt {attempt + 1}/{max_attempts}") + + logger.error(f"Synology NAS didn't come online after {max_attempts * delay} seconds") + return False + + def power_on(self) -> bool: + """Включение Synology NAS""" + # Принудительная проверка статуса + if self.is_online(force_check=True): + logger.info("Synology NAS is already online") + return True + + logger.info("Powering on Synology NAS via Wake-on-LAN...") + + # Проверяем, настроен ли MAC-адрес + if not SYNOLOGY_MAC: + logger.error("Cannot power on: MAC address not configured in settings") + return False + + # Пробуем отправить несколько WoL пакетов для надежности + success = False + for attempt in range(3): + logger.debug(f"Sending WoL packet, attempt {attempt + 1}/3") + if self.wake_on_lan(): + success = True + break + sleep(1) + + if not success: + logger.error("Failed to send Wake-on-LAN packets") + return False + + # Ожидание загрузки + logger.info("WoL packets sent successfully, waiting for system to boot...") + boot_result = self.wait_for_boot(max_attempts=30, delay=10) + + if boot_result: + # Проверяем доступность API после загрузки + system_status = self.get_system_status() + if system_status.get("status") == "online": + logger.info("System booted successfully with API access") + return True + else: + logger.warning("System appears to be online but API may not be fully ready") + return True + else: + logger.error("System did not come online after WoL") + return False + + def power_off(self) -> bool: + """Выключение Synology NAS""" + if not self.is_online(force_check=True): + logger.info("Synology NAS is already offline") + return True + + logger.info("Powering off Synology NAS...") + result = self.shutdown_system() + + if result: + # Дополнительная проверка, что система действительно выключилась + logger.info("Verifying system is offline...") + for attempt in range(12): # Проверяем в течение 2 минут (12 * 10 сек) + if not self.is_online(force_check=True): + logger.info(f"System confirmed offline after {attempt * 10} seconds") + return True + logger.debug(f"System still shutting down, checking again... ({attempt + 1}/12)") + sleep(10) + + logger.warning("System still appears to be online after shutdown command") + return False + else: + logger.error("Failed to initiate shutdown") + return False + + # Заглушки для расширенных методов + def get_shared_folders(self) -> List[Dict[str, Any]]: + """Получение списка общих папок (заглушка)""" + logger.warning("Function get_shared_folders() is not implemented yet") + return [] + + def get_system_load(self) -> Dict[str, Any]: + """Получение информации о загрузке системы (заглушка)""" + logger.warning("Function get_system_load() is not implemented yet") + return {} + + def get_storage_status(self) -> Dict[str, Any]: + """Получение подробной информации о хранилище (заглушка)""" + logger.warning("Function get_storage_status() is not implemented yet") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for storage status request") + return {"error": "authentication_failed"} + + try: + # Здесь будет реализация после добавления соответствующего API + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.Storage.CGI.Storage", + "version": "1", + "method": "load_info", + "sid": self.sid + } + + # В текущей версии просто возвращаем заглушку + return { + "volumes": [], + "disks": [], + "total_size": 0, + "total_used": 0, + "error": "not_implemented" + } + except Exception as e: + logger.error(f"Error in get_storage_status: {str(e)}") + return {"error": str(e)} + + def get_security_status(self) -> Dict[str, Any]: + """Получение информации о состоянии безопасности (заглушка)""" + logger.warning("Function get_security_status() is not implemented yet") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for security status request") + return {"success": False, "error": "authentication_failed"} + + try: + # Здесь будет реализация после добавления соответствующего API + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.Core.SecurityScan.Status", + "version": "1", + "method": "get", + "sid": self.sid + } + + # В текущей версии просто возвращаем заглушку + return { + "success": False, + "status": "not_implemented", + "last_check": None, + "is_secure": False, + "error": "not_implemented" + } + except Exception as e: + logger.error(f"Error in get_security_status: {str(e)}") + return {"success": False, "error": str(e)} diff --git a/.history/src/api/synology_20250830084644.py b/.history/src/api/synology_20250830084644.py new file mode 100644 index 0000000..b31704e --- /dev/null +++ b/.history/src/api/synology_20250830084644.py @@ -0,0 +1,1218 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Модуль для взаимодействия с API Synology NAS +""" + +import requests +from requests.adapters import HTTPAdapter +import json +import logging +import time +import urllib3 +from urllib3.util import Retry +from typing import Dict, Any, Optional, List +import socket +import struct +from time import sleep + +from src.config.config import ( + SYNOLOGY_HOST, + SYNOLOGY_PORT, + SYNOLOGY_USERNAME, + SYNOLOGY_PASSWORD, + SYNOLOGY_SECURE, + SYNOLOGY_TIMEOUT, + SYNOLOGY_MAC, + WOL_PORT, + SYNOLOGY_API_VERSION, + SYNOLOGY_POWER_API, + SYNOLOGY_INFO_API +) +from src.api.api_discovery import discover_available_apis, find_compatible_api + +# Отключение предупреждений о небезопасных SSL-соединениях +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + +logger = logging.getLogger(__name__) + +class SynologyAPI: + """Класс для взаимодействия с API Synology NAS""" + + def __init__(self): + """Инициализация класса SynologyAPI""" + logger.info("Creating API with auto-retry and connection pool") + logger.debug(f"Connection details: Host={SYNOLOGY_HOST}, Port={SYNOLOGY_PORT}, Secure={SYNOLOGY_SECURE}") + + self.protocol = "https" if SYNOLOGY_SECURE else "http" + self.base_url = f"{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi" + self.sid = None + self.session = requests.Session() + + # Настройка SSL + if self.protocol == "https": + logger.debug("SSL enabled, disabling certificate verification for internal network") + self.session.verify = False # Отключаем проверку SSL для внутренней сети + + # Добавляем пользовательские заголовки для улучшения совместимости с API + custom_headers = { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36', + 'Accept': 'application/json, text/javascript, */*; q=0.01', + 'Accept-Language': 'ru-RU,ru;q=0.9,en-US;q=0.8,en;q=0.7', + 'X-Requested-With': 'XMLHttpRequest', + 'Connection': 'keep-alive', + 'Referer': f'{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/' + } + self.session.headers.update(custom_headers) + logger.debug("Added browser-like headers for API compatibility") + + # Добавляем повторные попытки для HTTP-запросов + retry_strategy = Retry( + total=5, # Увеличиваем количество попыток + status_forcelist=[429, 500, 502, 503, 504, 404], + allowed_methods=["GET", "POST"], + backoff_factor=1.5, # Увеличиваем задержку между попытками + respect_retry_after_header=True + ) + adapter = HTTPAdapter( + max_retries=retry_strategy, + pool_connections=3, + pool_maxsize=10 + ) + self.session.mount("http://", adapter) + self.session.mount("https://", adapter) + + # Таймауты будут указаны в запросах + self.default_timeout = (SYNOLOGY_TIMEOUT, SYNOLOGY_TIMEOUT*2) + logger.debug(f"Setting default request timeout: {self.default_timeout}") + + # Кэш для хранения результатов запросов + self._cache = {} + self._cache_ttl = {} + self._last_online_check = 0 + self._last_online_status = False + self._online_check_interval = 30 # Интервал для кэширования проверок онлайн-статуса + + # Время последней успешной аутентификации и срок действия сессии + self._last_auth_time = 0 + self._auth_expiry = 3600 # По умолчанию 1 час + + # Информация о доступных API + self._available_apis = {} + self._api_info_ttl = 0 + + # Инициализируем API version resolver для автоматического определения совместимых API + self.api_resolver = None # Будет создан при необходимости + + def login(self) -> bool: + """Авторизация в API Synology NAS""" + # Сбрасываем SID для новой сессии + self.sid = None + + logger.info("Attempting to authenticate with Synology NAS...") + logger.debug(f"Base URL: {self.base_url}") + + # Проверяем доступность NAS перед запросом с помощью прямой TCP-проверки + # Избегаем вызова is_online(), чтобы не создавать рекурсию + online_status = self._check_tcp_connection() + if not online_status: + logger.error("Cannot login: Synology NAS is not reachable") + return False + + # Пробуем различные версии API для аутентификации + # Начинаем с версии 3, которая показала лучшую совместимость в тестах + auth_versions_to_try = [3, 2, 1, 6] # Пробуем сначала более стабильные версии + + for auth_version in auth_versions_to_try: + try: + # Определяем путь к API аутентификации + auth_path = "entry.cgi" # Используем entry.cgi вместо auth.cgi по умолчанию + + # Проверка информации API для определения доступных версий API + api_info_url = f"{self.base_url}/entry.cgi" + api_info_params = { + "api": "SYNO.API.Info", + "version": "1", + "method": "query", + "query": "SYNO.API.Auth" + } + + logger.debug(f"Querying API info for auth version {auth_version}") + try: + api_info_response = self.session.get( + api_info_url, + params=api_info_params, + timeout=self.default_timeout, + verify=False + ) + + if api_info_response.status_code == 200: + api_info_data = api_info_response.json() + if api_info_data.get("success"): + auth_info = api_info_data.get("data", {}).get("SYNO.API.Auth", {}) + max_version = auth_info.get("maxVersion", 6) + min_version = auth_info.get("minVersion", 1) + auth_path = auth_info.get("path", "entry.cgi") + logger.debug(f"Auth API versions: min={min_version}, max={max_version}, path={auth_path}") + + # Проверяем поддержку текущей версии + if auth_version < min_version or auth_version > max_version: + logger.warning(f"Auth API version {auth_version} not supported (min={min_version}, max={max_version}), trying next version") + continue + else: + logger.warning("Failed to query API info, using default auth path") + else: + logger.warning(f"API info request failed with status {api_info_response.status_code}, using default auth path") + except Exception as e: + logger.warning(f"Error querying API info: {str(e)}, using default auth path") + + # Основной запрос авторизации + url = f"{self.base_url}/{auth_path}" + params = { + "api": "SYNO.API.Auth", + "version": str(auth_version), + "method": "login", + "account": SYNOLOGY_USERNAME, + "passwd": SYNOLOGY_PASSWORD, + "session": "SynologyPowerControlBot", + "format": "cookie" + } + + # Для версии 6+ используем немного другой формат + if auth_version >= 6: + params["enable_syno_token"] = "yes" + + logger.debug(f"Sending auth request to {url} with API version {auth_version}") + start_time = time.time() + response = self.session.get( + url, + params=params, + timeout=self.default_timeout, + verify=False + ) + elapsed_time = time.time() - start_time + logger.debug(f"Auth request completed in {elapsed_time:.2f}s with status code {response.status_code}") + + # Проверка статуса HTTP + if response.status_code != 200: + logger.error(f"HTTP error: {response.status_code}") + logger.debug(f"Response content: {response.text[:200]}") + continue # Пробуем следующую версию + + try: + data = response.json() + except json.JSONDecodeError: + logger.error("Failed to decode JSON response") + logger.debug(f"Response content: {response.text[:200]}") + continue # Пробуем следующую версию + + if data.get("success"): + self.sid = data.get("data", {}).get("sid") + self._last_auth_time = time.time() + logger.info(f"Successfully logged in to Synology NAS using API version {auth_version}") + logger.debug(f"Session ID obtained: {self.sid[:5] if self.sid else 'None'}...") + + # Получаем и сохраняем токен SYNO, если он есть + syno_token = data.get("data", {}).get("synotoken") + if syno_token: + self.session.headers.update({'X-SYNO-TOKEN': syno_token}) + logger.debug("Added X-SYNO-TOKEN header for improved API compatibility") + + # Также добавляем SID в cookies для улучшения совместимости + self.session.cookies.update({ + 'id': self.sid, + 'sid': self.sid + }) + logger.debug("Added SID to session cookies for improved compatibility") + + # Проверка валидности полученной сессии с помощью простого запроса + # Будем использовать SYNO.API.Info без проверки сложных методов + + # Даем системе немного времени для инициализации сессии + time.sleep(0.5) + + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to log in with version {auth_version}: Error code {error_code}") + + # Если ошибка связана с версией API, пробуем следующую версию + if error_code in [104, 105]: + logger.warning(f"Auth version {auth_version} not supported, trying next version") + continue + + # Дополнительная диагностика + if error_code == 400: + logger.error("Authentication error: Invalid credentials") + elif error_code == 401: + logger.error("Authentication error: Account disabled") + elif error_code == 402: + logger.error("Authentication error: Permission denied") + elif error_code == 403: + logger.error("Authentication error: 2-factor authentication required") + elif error_code == 404: + logger.error("Authentication error: Failed to authenticate with 2-factor authentication") + + # Если ошибка связана с неверными учетными данными, нет смысла пробовать другие версии API + if error_code in [400, 401, 402, 403, 404]: + return False + + except requests.exceptions.Timeout: + logger.error(f"Connection timeout during auth with version {auth_version}") + continue # Пробуем следующую версию + except requests.exceptions.ConnectionError as e: + logger.error(f"Connection error during auth with version {auth_version}: {str(e)}") + continue # Пробуем следующую версию + except requests.RequestException as e: + logger.error(f"Request error during auth with version {auth_version}: {str(e)}") + continue # Пробуем следующую версию + except Exception as e: + logger.error(f"Unexpected error during login with version {auth_version}: {str(e)}", exc_info=True) + continue # Пробуем следующую версию + + # Если все версии не сработали + logger.error("Failed to authenticate with any API version") + return False + + def _validate_session(self) -> bool: + """Проверяет валидность сессии после авторизации""" + if not self.sid: + return False + + # Попробуем сделать простой запрос для проверки сессии + test_apis = [ + {"api": "SYNO.Core.System", "method": "info", "version": 1}, + {"api": "SYNO.DSM.Info", "method": "getinfo", "version": 1} + ] + + for test_api in test_apis: + try: + url = f"{self.base_url}/entry.cgi" + params = { + "api": test_api["api"], + "version": str(test_api["version"]), + "method": test_api["method"], + "_sid": self.sid + } + + response = self.session.get(url, params=params, timeout=self.default_timeout, verify=False) + + if response.status_code == 200: + data = response.json() + if data.get("success"): + logger.debug(f"Session validation successful using {test_api['api']}") + return True + else: + error_code = data.get("error", {}).get("code", -1) + if error_code != 119: # Не сессия истекла + logger.debug(f"Session validation with {test_api['api']} returned error code {error_code}") + return True # Считаем сессию валидной, если ошибка не связана с истечением сессии + except Exception as e: + logger.warning(f"Error during session validation with {test_api['api']}: {str(e)}") + + logger.warning("Session validation failed with all test APIs") + return False + + def logout(self) -> bool: + """Выход из API Synology NAS""" + if not self.sid: + return True + + try: + url = f"{self.base_url}/auth.cgi" + params = { + "api": "SYNO.API.Auth", + "version": "1", + "method": "logout", + "session": "SynologyPowerControlBot", + "_sid": self.sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + self.sid = None + logger.info("Successfully logged out from Synology NAS") + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to log out from Synology NAS: Error code {error_code}") + return False + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + return False + + def _make_api_request(self, api_name: str, method: str, version: int = 1, + params: Optional[Dict[str, Any]] = None, retry_auth: bool = True, retry_count: int = 0) -> Optional[Dict[str, Any]]: + """Обобщенный метод для выполнения API запросов с обработкой ошибок""" + # Ограничение на количество повторных попыток + if retry_count >= 3: + logger.error(f"Too many retries for {api_name}.{method}, giving up") + return None + + # Проверка наличия авторизации + if not self.sid and not self.login(): + logger.error(f"Not authenticated for API request: {api_name}.{method}") + return None + + # Проверка информации API для определения пути и поддерживаемой версии + api_info_url = f"{self.base_url}/entry.cgi" + api_info_params = { + "api": "SYNO.API.Info", + "version": "1", + "method": "query", + "query": api_name + } + + api_path = "entry.cgi" + try: + logger.debug(f"Querying API info for {api_name}") + api_info_response = self.session.get( + api_info_url, + params=api_info_params, + timeout=self.default_timeout, + verify=False + ) + + if api_info_response.status_code == 200: + api_info_data = api_info_response.json() + if api_info_data.get("success"): + api_info = api_info_data.get("data", {}).get(api_name, {}) + if api_info: + max_version = api_info.get("maxVersion", version) + min_version = api_info.get("minVersion", version) + api_path = api_info.get("path", "entry.cgi") + + # Проверка, поддерживается ли запрошенная версия + if version < min_version: + logger.warning(f"API version {version} for {api_name} is below minimum {min_version}, using {min_version}") + version = min_version + elif version > max_version: + logger.warning(f"API version {version} for {api_name} exceeds maximum {max_version}, using {max_version}") + version = max_version + + logger.debug(f"Using API path: {api_path}, version: {version}") + else: + logger.warning(f"API {api_name} not found in API info, using defaults") + except Exception as e: + logger.warning(f"Error querying API info for {api_name}: {str(e)}, using defaults") + + # Подготовка базовых параметров запроса + base_params = { + "api": api_name, + "version": str(version), + "method": method, + "_sid": self.sid # Используем _sid вместо sid для лучшей совместимости + } + + # Добавление дополнительных параметров, если они заданы + if params: + base_params.update(params) + + url = f"{self.base_url}/{api_path}" + logger.debug(f"API request: {api_name}.{method} v{version} to {url}") + logger.debug(f"Full request params: {base_params}") + + try: + start_time = time.time() + response = self.session.get( + url, + params=base_params, + timeout=self.default_timeout, + verify=False + ) + elapsed_time = time.time() - start_time + logger.debug(f"API request completed in {elapsed_time:.2f}s with status {response.status_code}") + + # Проверка статуса HTTP + if response.status_code != 200: + logger.error(f"HTTP error: {response.status_code} for {api_name}.{method}") + + # Повторная попытка при ошибках соединения + if response.status_code in [500, 502, 503, 504]: + logger.info(f"Server error, retrying request for {api_name}.{method}") + sleep(2) + return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1) + + return None + + try: + data = response.json() + except json.JSONDecodeError: + logger.error(f"Failed to decode JSON response for {api_name}.{method}") + logger.debug(f"Response content: {response.text[:200]}") + + # Повторная попытка при ошибках декодирования + logger.info(f"JSON decode error, retrying request for {api_name}.{method}") + sleep(2) + return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1) + + if data.get("success"): + logger.info(f"API request successful for {api_name}.{method}") + return data.get("data", {}) + else: + error_code = data.get("error", {}).get("code", -1) + error_desc = self._get_error_description(error_code) + logger.error(f"API error for {api_name}.{method}: {error_code} - {error_desc}") + + # Ошибки доступа или прав часто встречаются, но они не критичные + # Например, ошибка 102 означает, что нет прав, но NAS доступен + if error_code in [102, 103, 104, 105]: + logger.warning(f"Permission-related error for {api_name}.{method}: {error_code}") + # Возвращаем пустой словарь вместо None, + # чтобы вызывающий код мог понять, что запрос выполнен + return {} + + # Если ошибка связана с авторизацией и нам разрешено повторить попытку + if error_code in [106, 107, 119] and retry_auth: + logger.info(f"Session error (code {error_code}), creating fresh session...") + self.sid = None # Сбрасываем SID + + # Для ошибки 119 (Session timeout) дадим системе немного времени + if error_code == 119: + logger.info("Session timeout detected, waiting before retry...") + sleep(3) + + if self.login(): + logger.info("Re-authenticated with fresh session, retrying API request...") + # Рекурсивный вызов, но со счетчиком повторов + return self._make_api_request(api_name, method, version, params, False, retry_count + 1) + + # Для некоторых ошибок можно автоматически повторить запрос + if error_code in [408, 429, 500, 502, 503, 504]: + logger.info(f"Temporary error {error_code}, retrying request for {api_name}.{method}") + sleep(2) + return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1) + + return None + + except requests.exceptions.Timeout: + logger.error(f"Request timeout for {api_name}.{method}") + + # Повторная попытка при таймауте + if retry_count < 2: + logger.info(f"Timeout, retrying request for {api_name}.{method}") + sleep(2) + return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1) + + return None + except requests.exceptions.ConnectionError as e: + logger.error(f"Connection error for {api_name}.{method}: {str(e)}") + + # Повторная попытка при ошибке соединения + if retry_count < 2: + logger.info(f"Connection error, retrying request for {api_name}.{method}") + sleep(2) + return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1) + + return None + except Exception as e: + logger.error(f"Unexpected error during {api_name}.{method}: {str(e)}") + return None + + def get_system_status(self) -> Dict[str, Any]: + """Получение статуса системы""" + # Проверяем доступность системы + if not self.is_online(): + logger.info("Device is offline, skipping API request") + return {"status": "offline"} + + # Проверяем, есть ли кэшированный результат + cache_key = "system_status" + current_time = time.time() + if cache_key in self._cache and current_time - self._cache_ttl.get(cache_key, 0) < 60: + logger.debug("Using cached system status") + return self._cache[cache_key] + + # Используем рекомендованный API для получения информации о системе + logger.info(f"Getting system info using {SYNOLOGY_INFO_API} v{SYNOLOGY_API_VERSION}") + + # Для SYNO.DSM.Info метод getinfo, для других обычно используется get или info + if SYNOLOGY_INFO_API == "SYNO.DSM.Info": + method = "getinfo" + else: + method = "get" + + result = self._make_api_request(SYNOLOGY_INFO_API, method, version=SYNOLOGY_API_VERSION) + + if result: + logger.info(f"Successfully retrieved system info using {SYNOLOGY_INFO_API}") + + # Формируем расширенный ответ с дополнительной информацией + system_info = { + "status": "online", + "hostname": result.get("hostname", "unknown"), + "model": result.get("model", "unknown"), + "version": result.get("version", "unknown"), + "uptime": result.get("uptime", 0), + "time": current_time, + "is_online": True, + "api_used": SYNOLOGY_INFO_API + } + + # Сохраняем в кэше + self._cache[cache_key] = system_info + self._cache_ttl[cache_key] = current_time + + return system_info + + # Если основной API не сработал, пробуем резервные варианты + logger.warning(f"Failed to retrieve system info with {SYNOLOGY_INFO_API}, trying fallback APIs") + + # Пробуем резервные API + apis_to_try = [ + {"name": "SYNO.DSM.Info", "method": "getinfo", "version": 2}, + {"name": "SYNO.Core.System", "method": "info", "version": 1}, + {"name": "SYNO.Core.System.Status", "method": "get", "version": 1}, + {"name": "SYNO.Core.System.Info", "method": "get", "version": 1}, + ] + + for api in apis_to_try: + if api["name"] == SYNOLOGY_INFO_API: + continue # Пропускаем уже проверенный API + + logger.info(f"Trying to get system info using {api['name']}.{api['method']} v{api['version']}") + result = self._make_api_request(api["name"], api["method"], version=api["version"]) + + if result: + logger.info(f"Successfully retrieved system info using {api['name']}") + + # Формируем расширенный ответ с дополнительной информацией + system_info = { + "status": "online", + "hostname": result.get("hostname", "unknown"), + "model": result.get("model", "unknown"), + "version": result.get("version", "unknown"), + "uptime": result.get("uptime", 0), + "time": current_time, + "is_online": True, + "api_used": api["name"] + } + + # Сохраняем в кэше + self._cache[cache_key] = system_info + self._cache_ttl[cache_key] = current_time + + return system_info + + # Если все запросы не удались, но система онлайн, возвращаем базовую информацию + logger.warning("Failed to retrieve system info with all API methods") + return { + "status": "error", + "error": "Failed to fetch system information", + "is_online": True, + "time": current_time + } + + def shutdown_system(self) -> bool: + """Выключение системы""" + # Проверяем, включено ли устройство перед попыткой его выключить + if not self.is_online(force_check=True): + logger.info("Synology NAS is already offline, no need to shut down") + return True + + logger.info("Attempting to shutdown Synology NAS...") + + # Попробуем сначала использовать предпочтительный API для управления питанием + logger.info(f"Trying shutdown with {SYNOLOGY_POWER_API} v{SYNOLOGY_API_VERSION}") + + # Для SYNO.Core.Hardware.PowerRecovery используется метод setPowerOnState + # Для других API обычно используется метод shutdown или reboot + if SYNOLOGY_POWER_API == "SYNO.Core.Hardware.PowerRecovery": + # Для этого API нужны специальные параметры + params = {"state": "powerbtn"} # powerbtn имитирует нажатие кнопки питания + result = self._make_api_request(SYNOLOGY_POWER_API, "setPowerOnState", version=SYNOLOGY_API_VERSION, params=params) + else: + # Пробуем стандартный метод + result = self._make_api_request(SYNOLOGY_POWER_API, "shutdown", version=SYNOLOGY_API_VERSION) + + if result is not None: + logger.info(f"Successfully initiated system shutdown using {SYNOLOGY_POWER_API}") + + # Даем системе время начать процесс выключения + logger.info("Waiting for shutdown to initialize...") + sleep(5) + + # Проверяем статус + if not self.is_online(force_check=True): + logger.info("System is now offline. Shutdown confirmed successful.") + return True + else: + logger.info("System still appears to be online, but shutdown may be in progress.") + return True + else: + logger.warning(f"Failed to shutdown using {SYNOLOGY_POWER_API}, trying alternative methods") + + # Если не сработал основной метод, пробуем резервные варианты + # Проверка всех доступных методов API для выключения + apis_to_try = [ + {"name": "SYNO.Core.System", "method": "shutdown", "version": 3}, + {"name": "SYNO.Core.System.Power", "method": "shutdown", "version": 1}, + {"name": "SYNO.DSM.Power", "method": "shutdown", "version": 1}, + {"name": "SYNO.System.Power", "method": "shutdown", "version": 1}, + {"name": "SYNO.DSM.System", "method": "shutdown", "version": 1} + ] + + # Проверяем доступные API + try: + api_info_url = f"{self.base_url}/entry.cgi" + api_info_params = { + "api": "SYNO.API.Info", + "version": "1", + "method": "query", + "query": "SYNO.Core.System.Power,SYNO.DSM.Power,SYNO.Core.System,SYNO.System.Power,SYNO.DSM.System" + } + + logger.debug("Checking available shutdown APIs") + api_info_response = self.session.get( + api_info_url, + params=api_info_params, + timeout=self.default_timeout, + verify=False + ) + + if api_info_response.status_code == 200: + api_info_data = api_info_response.json() + if api_info_data.get("success"): + available_apis = api_info_data.get("data", {}) + logger.debug(f"Available APIs: {list(available_apis.keys())}") + + # Фильтруем только доступные API + filtered_apis = [] + for api in apis_to_try: + if api["name"] in available_apis: + api_info = available_apis[api["name"]] + # Проверка версии API + if api_info.get("minVersion", 1) <= api["version"] <= api_info.get("maxVersion", 1): + filtered_apis.append(api) + logger.debug(f"Adding {api['name']} to available shutdown APIs") + else: + logger.debug(f"API {api['name']} version {api['version']} not supported. Supported: {api_info.get('minVersion')}-{api_info.get('maxVersion')}") + + if filtered_apis: + apis_to_try = filtered_apis + else: + logger.warning("No compatible APIs found, trying all methods as fallback") + else: + logger.warning("Failed to query API info, using default methods") + else: + logger.warning(f"API info request failed with status {api_info_response.status_code}, using default methods") + except Exception as e: + logger.warning(f"Error querying API info: {str(e)}, using default methods") + + # Пробуем все доступные методы по порядку + for api in apis_to_try: + logger.info(f"Trying shutdown with {api['name']}.{api['method']} v{api['version']}") + result = self._make_api_request(api["name"], api["method"], version=api["version"]) + + if result is not None: + logger.info(f"Successfully initiated system shutdown using {api['name']}") + + # Даем системе время начать процесс выключения + logger.info("Waiting for shutdown to initialize...") + sleep(5) + + # Проверяем статус + if not self.is_online(force_check=True): + logger.info("System is now offline. Shutdown confirmed successful.") + return True + else: + logger.info("System still appears to be online, but shutdown may be in progress.") + return True + else: + logger.warning(f"Failed to shutdown using {api['name']}.{api['method']} v{api['version']}") + + # Если ни один метод не сработал, но система стала недоступна + if not self.is_online(force_check=True): + logger.info("System appears to be shutting down despite API errors") + return True + + logger.error("Failed to shutdown system after trying multiple APIs") + return False + + def reboot_system(self) -> bool: + """Перезагрузка системы""" + # Проверяем, включена ли система + if not self.is_online(force_check=True): + logger.error("Cannot reboot: System is offline") + return False + + logger.info("Attempting to reboot Synology NAS...") + + # Попробуем сначала использовать предпочтительный API для управления питанием + logger.info(f"Trying reboot with {SYNOLOGY_POWER_API} v{SYNOLOGY_API_VERSION}") + + # Для SYNO.Core.Hardware.PowerRecovery используется метод setPowerOnState или powerButton + # Для других API обычно используется метод restart или reboot + if SYNOLOGY_POWER_API == "SYNO.Core.Hardware.PowerRecovery": + # Для этого API нужны специальные параметры + params = {"reboot": "true"} # Передаем флаг для перезагрузки + result = self._make_api_request(SYNOLOGY_POWER_API, "setPowerOnState", version=SYNOLOGY_API_VERSION, params=params) + else: + # Пробуем стандартный метод + methods_to_try = ["restart", "reboot"] + result = None + for method in methods_to_try: + result = self._make_api_request(SYNOLOGY_POWER_API, method, version=SYNOLOGY_API_VERSION) + if result is not None: + logger.info(f"Successfully used method {method} for {SYNOLOGY_POWER_API}") + break + + if result is not None: + logger.info(f"Successfully initiated system reboot using {SYNOLOGY_POWER_API}") + + # Даем системе время начать процесс перезагрузки + logger.info("Waiting for reboot to initialize...") + sleep(5) + + # Ждем, пока система станет недоступна (признак перезагрузки) + reboot_started = False + for i in range(12): # Проверяем в течение 60 секунд (12 * 5сек) + if not self.is_online(force_check=True): + logger.info(f"System went offline after {i*5} seconds, reboot in progress") + reboot_started = True + break + logger.debug(f"System still online, waiting... ({i+1}/12)") + sleep(5) + + if reboot_started: + # Ждем, пока система снова станет доступна + logger.info("Waiting for system to come back online...") + return self.wait_for_boot(max_attempts=30, delay=10) + else: + logger.warning("System did not go offline after reboot command, but command was accepted") + # Даже если система не ушла оффлайн, команда могла быть принята + return True + else: + logger.warning(f"Failed to reboot using {SYNOLOGY_POWER_API}, trying alternative methods") + + # Если не сработал основной метод, пробуем резервные варианты + # Проверка всех доступных методов API для перезагрузки + apis_to_try = [ + {"name": "SYNO.Core.System.Power", "method": "restart", "version": 1}, + {"name": "SYNO.DSM.Power", "method": "reboot", "version": 1}, + {"name": "SYNO.Core.System", "method": "reboot", "version": 3}, + {"name": "SYNO.System.Power", "method": "reboot", "version": 1}, + {"name": "SYNO.DSM.System", "method": "reboot", "version": 1}, + {"name": "SYNO.Core.Hardware.NeedReboot", "method": "reboot", "version": 1} + ] + + # Получаем список доступных API + try: + api_info_url = f"{self.base_url}/entry.cgi" + api_info_params = { + "api": "SYNO.API.Info", + "version": "1", + "method": "query", + "query": "SYNO.Core.System.Power,SYNO.DSM.Power,SYNO.Core.System,SYNO.System.Power,SYNO.DSM.System,SYNO.Core.Hardware.NeedReboot" + } + + logger.debug("Checking available reboot APIs") + api_info_response = self.session.get( + api_info_url, + params=api_info_params, + timeout=self.default_timeout, + verify=False + ) + + if api_info_response.status_code == 200: + api_info_data = api_info_response.json() + if api_info_data.get("success"): + available_apis = api_info_data.get("data", {}) + logger.debug(f"Available APIs: {list(available_apis.keys())}") + + # Фильтруем только доступные API + filtered_apis = [] + for api in apis_to_try: + if api["name"] in available_apis: + api_info = available_apis[api["name"]] + # Проверка версии API + if api_info.get("minVersion", 1) <= api["version"] <= api_info.get("maxVersion", 1): + filtered_apis.append(api) + logger.debug(f"Adding {api['name']} to available reboot APIs") + else: + logger.debug(f"API {api['name']} version {api['version']} not supported. Supported: {api_info.get('minVersion')}-{api_info.get('maxVersion')}") + + if filtered_apis: + apis_to_try = filtered_apis + else: + logger.warning("No compatible APIs found, trying all methods as fallback") + else: + logger.warning("Failed to query API info, using default methods") + else: + logger.warning(f"API info request failed with status {api_info_response.status_code}, using default methods") + except Exception as e: + logger.warning(f"Error querying API info: {str(e)}, using default methods") + + # Пробуем все доступные методы по порядку + for api in apis_to_try: + logger.info(f"Trying reboot with {api['name']}.{api['method']} v{api['version']}") + result = self._make_api_request(api["name"], api["method"], version=api["version"]) + + if result is not None: + logger.info(f"Successfully initiated system reboot using {api['name']}") + + # Даем системе время начать процесс перезагрузки + logger.info("Waiting for reboot to initialize...") + sleep(5) + + # Ждем, пока система станет недоступна (признак перезагрузки) + reboot_started = False + for i in range(12): # Проверяем в течение 60 секунд (12 * 5сек) + if not self.is_online(force_check=True): + logger.info(f"System went offline after {i*5} seconds, reboot in progress") + reboot_started = True + break + logger.debug(f"System still online, waiting... ({i+1}/12)") + sleep(5) + + if reboot_started: + # Ждем, пока система снова станет доступна + logger.info("Waiting for system to come back online...") + return self.wait_for_boot(max_attempts=30, delay=10) + else: + logger.warning("System did not go offline after reboot command, but command was accepted") + # Даже если система не ушла оффлайн, команда могла быть принята + return True + else: + logger.warning(f"Failed to reboot using {api['name']}.{api['method']} v{api['version']}") + + logger.error("Failed to reboot system after trying multiple APIs") + return False + + def _get_error_description(self, error_code: int) -> str: + """Получение описания ошибки по коду""" + error_descriptions = { + 100: "Unknown error", + 101: "Invalid parameter", + 102: "API does not exist", + 103: "Method does not exist", + 104: "Version does not support", + 105: "Permission denied", + 106: "Session timeout", + 107: "Session interrupted by duplicate login", + 400: "Invalid credentials", + 401: "Account disabled", + 402: "Permission denied", + 403: "2FA required", + 404: "Failed to authenticate with 2FA" + } + return error_descriptions.get(error_code, "Unknown error code") + + def _check_tcp_connection(self) -> bool: + """Проверка базового TCP-соединения с Synology NAS""" + try: + socket_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + socket_obj.settimeout(SYNOLOGY_TIMEOUT) + result = socket_obj.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT)) + socket_obj.close() + + return result == 0 + except socket.error as e: + logger.error(f"Socket error during connection check: {str(e)}") + return False + except Exception as e: + logger.error(f"Unexpected error during connection check: {str(e)}") + return False + + def is_online(self, force_check=False) -> bool: + """Проверка онлайн-статуса Synology NAS""" + # Используем кэшированное значение, если доступно и не устарело + current_time = time.time() + if not force_check and (current_time - self._last_online_check) < self._online_check_interval: + logger.debug(f"Using cached online status: {self._last_online_status}") + return self._last_online_status + + logger.info("Checking if NAS is online...") + + # Проверяем TCP-соединение + online_status = self._check_tcp_connection() + logger.info(f"Detected Synology NAS online status: {online_status}") + + # Если TCP-соединение успешно и у нас есть действующий SID, + # попробуем более детальную проверку через API + if online_status and self.sid: + logger.info("Trying to fetch more detailed online status through API...") + + # Пробуем разные API для проверки онлайн-статуса + api_checks = [ + {"api": "SYNO.DSM.Info", "version": "2", "method": "getinfo"}, + {"api": "SYNO.Core.System", "version": "1", "method": "info"}, + {"api": "SYNO.Core.System.Status", "version": "1", "method": "get"} + ] + + api_success = False + for api_check in api_checks: + try: + url = f"{self.base_url}/entry.cgi" + params = { + "api": api_check["api"], + "version": api_check["version"], + "method": api_check["method"], + "sid": self.sid + } + + logger.debug(f"Trying online status check with {api_check['api']}") + response = self.session.get( + url, + params=params, + timeout=self.default_timeout, + verify=False + ) + + if response.status_code == 200: + data = response.json() + if data.get("success"): + logger.info(f"API request successful for {api_check['api']}") + logger.info("Synology NAS is online with API access") + api_success = True + break + else: + error_code = data.get("error", {}).get("code", -1) + logger.warning(f"API response indicates an error: {error_code}, but NAS is reachable") + else: + logger.warning(f"API returned status code {response.status_code}") + except Exception as e: + logger.warning(f"API check failed for {api_check['api']}, but TCP connection succeeded: {str(e)}") + + if not api_success: + logger.warning("All API checks failed, but TCP connection is successful") + + # Обновляем кэшированное значение + self._last_online_check = current_time + self._last_online_status = online_status + + return online_status + + def wake_on_lan(self) -> bool: + """Отправка Wake-on-LAN пакета для включения Synology NAS""" + if not SYNOLOGY_MAC: + logger.error("MAC address not configured") + return False + + try: + # Преобразование MAC-адреса в байты + mac_address = SYNOLOGY_MAC.replace(':', '').replace('-', '') + if len(mac_address) != 12: + logger.error(f"Invalid MAC address format: {SYNOLOGY_MAC}") + return False + + try: + mac_bytes = bytes.fromhex(mac_address) + except ValueError as e: + logger.error(f"Failed to parse MAC address: {str(e)}") + logger.error(f"MAC address must contain only hex characters: {SYNOLOGY_MAC}") + return False + + # Создание Magic Packet + magic_packet = b'\xff' * 6 + mac_bytes * 16 + + # Отправка пакета на конкретный адрес + try: + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) + sock.sendto(magic_packet, (SYNOLOGY_HOST, WOL_PORT)) + sock.close() + logger.info(f"Wake-on-LAN packet sent to {SYNOLOGY_MAC} at {SYNOLOGY_HOST}:{WOL_PORT}") + except Exception as e: + logger.error(f"Error sending directed WoL packet: {str(e)}") + return False + + # Для надежности отправляем также широковещательный пакет + try: + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) + + # Используем стандартный широковещательный адрес + broadcast_addr = "255.255.255.255" + sock.sendto(magic_packet, (broadcast_addr, WOL_PORT)) + sock.close() + logger.info(f"Wake-on-LAN broadcast packet sent to {broadcast_addr}:{WOL_PORT}") + except Exception as e: + logger.warning(f"Error sending broadcast WoL packet (non-critical): {str(e)}") + # Не считаем ошибкой, т.к. основной пакет уже отправлен + + return True + + except Exception as e: + logger.error(f"Unexpected error in wake_on_lan: {str(e)}") + return False + + def wait_for_boot(self, max_attempts: int = 30, delay: int = 10) -> bool: + """Ожидание загрузки Synology NAS""" + logger.info(f"Waiting for Synology NAS to boot (timeout: {max_attempts * delay}s)...") + + for attempt in range(max_attempts): + # Принудительно проверяем статус без использования кэша + if self.is_online(force_check=True): + logger.info(f"Synology NAS is online after {attempt + 1} attempts ({(attempt + 1) * delay}s)") + + # Проверяем, что не только сеть доступна, но и API загрузился + api_ready = False + logger.info("Waiting for API services to initialize...") + + for api_check in range(5): # Даем еще до 50 секунд для загрузки API + if self.sid or self.login(): + api_ready = True + logger.info(f"API services are ready after {api_check + 1} attempts") + break + logger.debug(f"API not ready yet, waiting... ({api_check + 1}/5)") + sleep(10) + + if not api_ready: + logger.warning("System is online but API services may not be fully initialized") + + # Дадим дополнительное время для полной загрузки всех сервисов + sleep(delay) + return True + + sleep(delay) + logger.info(f"Waiting for boot... Attempt {attempt + 1}/{max_attempts}") + + logger.error(f"Synology NAS didn't come online after {max_attempts * delay} seconds") + return False + + def power_on(self) -> bool: + """Включение Synology NAS""" + # Принудительная проверка статуса + if self.is_online(force_check=True): + logger.info("Synology NAS is already online") + return True + + logger.info("Powering on Synology NAS via Wake-on-LAN...") + + # Проверяем, настроен ли MAC-адрес + if not SYNOLOGY_MAC: + logger.error("Cannot power on: MAC address not configured in settings") + return False + + # Пробуем отправить несколько WoL пакетов для надежности + success = False + for attempt in range(3): + logger.debug(f"Sending WoL packet, attempt {attempt + 1}/3") + if self.wake_on_lan(): + success = True + break + sleep(1) + + if not success: + logger.error("Failed to send Wake-on-LAN packets") + return False + + # Ожидание загрузки + logger.info("WoL packets sent successfully, waiting for system to boot...") + boot_result = self.wait_for_boot(max_attempts=30, delay=10) + + if boot_result: + # Проверяем доступность API после загрузки + system_status = self.get_system_status() + if system_status.get("status") == "online": + logger.info("System booted successfully with API access") + return True + else: + logger.warning("System appears to be online but API may not be fully ready") + return True + else: + logger.error("System did not come online after WoL") + return False + + def power_off(self) -> bool: + """Выключение Synology NAS""" + if not self.is_online(force_check=True): + logger.info("Synology NAS is already offline") + return True + + logger.info("Powering off Synology NAS...") + result = self.shutdown_system() + + if result: + # Дополнительная проверка, что система действительно выключилась + logger.info("Verifying system is offline...") + for attempt in range(12): # Проверяем в течение 2 минут (12 * 10 сек) + if not self.is_online(force_check=True): + logger.info(f"System confirmed offline after {attempt * 10} seconds") + return True + logger.debug(f"System still shutting down, checking again... ({attempt + 1}/12)") + sleep(10) + + logger.warning("System still appears to be online after shutdown command") + return False + else: + logger.error("Failed to initiate shutdown") + return False + + # Заглушки для расширенных методов + def get_shared_folders(self) -> List[Dict[str, Any]]: + """Получение списка общих папок (заглушка)""" + logger.warning("Function get_shared_folders() is not implemented yet") + return [] + + def get_system_load(self) -> Dict[str, Any]: + """Получение информации о загрузке системы (заглушка)""" + logger.warning("Function get_system_load() is not implemented yet") + return {} + + def get_storage_status(self) -> Dict[str, Any]: + """Получение подробной информации о хранилище (заглушка)""" + logger.warning("Function get_storage_status() is not implemented yet") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for storage status request") + return {"error": "authentication_failed"} + + try: + # Здесь будет реализация после добавления соответствующего API + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.Storage.CGI.Storage", + "version": "1", + "method": "load_info", + "sid": self.sid + } + + # В текущей версии просто возвращаем заглушку + return { + "volumes": [], + "disks": [], + "total_size": 0, + "total_used": 0, + "error": "not_implemented" + } + except Exception as e: + logger.error(f"Error in get_storage_status: {str(e)}") + return {"error": str(e)} + + def get_security_status(self) -> Dict[str, Any]: + """Получение информации о состоянии безопасности (заглушка)""" + logger.warning("Function get_security_status() is not implemented yet") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for security status request") + return {"success": False, "error": "authentication_failed"} + + try: + # Здесь будет реализация после добавления соответствующего API + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.Core.SecurityScan.Status", + "version": "1", + "method": "get", + "sid": self.sid + } + + # В текущей версии просто возвращаем заглушку + return { + "success": False, + "status": "not_implemented", + "last_check": None, + "is_secure": False, + "error": "not_implemented" + } + except Exception as e: + logger.error(f"Error in get_security_status: {str(e)}") + return {"success": False, "error": str(e)} diff --git a/.history/src/api/synology_20250830084803.py b/.history/src/api/synology_20250830084803.py new file mode 100644 index 0000000..b31704e --- /dev/null +++ b/.history/src/api/synology_20250830084803.py @@ -0,0 +1,1218 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Модуль для взаимодействия с API Synology NAS +""" + +import requests +from requests.adapters import HTTPAdapter +import json +import logging +import time +import urllib3 +from urllib3.util import Retry +from typing import Dict, Any, Optional, List +import socket +import struct +from time import sleep + +from src.config.config import ( + SYNOLOGY_HOST, + SYNOLOGY_PORT, + SYNOLOGY_USERNAME, + SYNOLOGY_PASSWORD, + SYNOLOGY_SECURE, + SYNOLOGY_TIMEOUT, + SYNOLOGY_MAC, + WOL_PORT, + SYNOLOGY_API_VERSION, + SYNOLOGY_POWER_API, + SYNOLOGY_INFO_API +) +from src.api.api_discovery import discover_available_apis, find_compatible_api + +# Отключение предупреждений о небезопасных SSL-соединениях +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + +logger = logging.getLogger(__name__) + +class SynologyAPI: + """Класс для взаимодействия с API Synology NAS""" + + def __init__(self): + """Инициализация класса SynologyAPI""" + logger.info("Creating API with auto-retry and connection pool") + logger.debug(f"Connection details: Host={SYNOLOGY_HOST}, Port={SYNOLOGY_PORT}, Secure={SYNOLOGY_SECURE}") + + self.protocol = "https" if SYNOLOGY_SECURE else "http" + self.base_url = f"{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi" + self.sid = None + self.session = requests.Session() + + # Настройка SSL + if self.protocol == "https": + logger.debug("SSL enabled, disabling certificate verification for internal network") + self.session.verify = False # Отключаем проверку SSL для внутренней сети + + # Добавляем пользовательские заголовки для улучшения совместимости с API + custom_headers = { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36', + 'Accept': 'application/json, text/javascript, */*; q=0.01', + 'Accept-Language': 'ru-RU,ru;q=0.9,en-US;q=0.8,en;q=0.7', + 'X-Requested-With': 'XMLHttpRequest', + 'Connection': 'keep-alive', + 'Referer': f'{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/' + } + self.session.headers.update(custom_headers) + logger.debug("Added browser-like headers for API compatibility") + + # Добавляем повторные попытки для HTTP-запросов + retry_strategy = Retry( + total=5, # Увеличиваем количество попыток + status_forcelist=[429, 500, 502, 503, 504, 404], + allowed_methods=["GET", "POST"], + backoff_factor=1.5, # Увеличиваем задержку между попытками + respect_retry_after_header=True + ) + adapter = HTTPAdapter( + max_retries=retry_strategy, + pool_connections=3, + pool_maxsize=10 + ) + self.session.mount("http://", adapter) + self.session.mount("https://", adapter) + + # Таймауты будут указаны в запросах + self.default_timeout = (SYNOLOGY_TIMEOUT, SYNOLOGY_TIMEOUT*2) + logger.debug(f"Setting default request timeout: {self.default_timeout}") + + # Кэш для хранения результатов запросов + self._cache = {} + self._cache_ttl = {} + self._last_online_check = 0 + self._last_online_status = False + self._online_check_interval = 30 # Интервал для кэширования проверок онлайн-статуса + + # Время последней успешной аутентификации и срок действия сессии + self._last_auth_time = 0 + self._auth_expiry = 3600 # По умолчанию 1 час + + # Информация о доступных API + self._available_apis = {} + self._api_info_ttl = 0 + + # Инициализируем API version resolver для автоматического определения совместимых API + self.api_resolver = None # Будет создан при необходимости + + def login(self) -> bool: + """Авторизация в API Synology NAS""" + # Сбрасываем SID для новой сессии + self.sid = None + + logger.info("Attempting to authenticate with Synology NAS...") + logger.debug(f"Base URL: {self.base_url}") + + # Проверяем доступность NAS перед запросом с помощью прямой TCP-проверки + # Избегаем вызова is_online(), чтобы не создавать рекурсию + online_status = self._check_tcp_connection() + if not online_status: + logger.error("Cannot login: Synology NAS is not reachable") + return False + + # Пробуем различные версии API для аутентификации + # Начинаем с версии 3, которая показала лучшую совместимость в тестах + auth_versions_to_try = [3, 2, 1, 6] # Пробуем сначала более стабильные версии + + for auth_version in auth_versions_to_try: + try: + # Определяем путь к API аутентификации + auth_path = "entry.cgi" # Используем entry.cgi вместо auth.cgi по умолчанию + + # Проверка информации API для определения доступных версий API + api_info_url = f"{self.base_url}/entry.cgi" + api_info_params = { + "api": "SYNO.API.Info", + "version": "1", + "method": "query", + "query": "SYNO.API.Auth" + } + + logger.debug(f"Querying API info for auth version {auth_version}") + try: + api_info_response = self.session.get( + api_info_url, + params=api_info_params, + timeout=self.default_timeout, + verify=False + ) + + if api_info_response.status_code == 200: + api_info_data = api_info_response.json() + if api_info_data.get("success"): + auth_info = api_info_data.get("data", {}).get("SYNO.API.Auth", {}) + max_version = auth_info.get("maxVersion", 6) + min_version = auth_info.get("minVersion", 1) + auth_path = auth_info.get("path", "entry.cgi") + logger.debug(f"Auth API versions: min={min_version}, max={max_version}, path={auth_path}") + + # Проверяем поддержку текущей версии + if auth_version < min_version or auth_version > max_version: + logger.warning(f"Auth API version {auth_version} not supported (min={min_version}, max={max_version}), trying next version") + continue + else: + logger.warning("Failed to query API info, using default auth path") + else: + logger.warning(f"API info request failed with status {api_info_response.status_code}, using default auth path") + except Exception as e: + logger.warning(f"Error querying API info: {str(e)}, using default auth path") + + # Основной запрос авторизации + url = f"{self.base_url}/{auth_path}" + params = { + "api": "SYNO.API.Auth", + "version": str(auth_version), + "method": "login", + "account": SYNOLOGY_USERNAME, + "passwd": SYNOLOGY_PASSWORD, + "session": "SynologyPowerControlBot", + "format": "cookie" + } + + # Для версии 6+ используем немного другой формат + if auth_version >= 6: + params["enable_syno_token"] = "yes" + + logger.debug(f"Sending auth request to {url} with API version {auth_version}") + start_time = time.time() + response = self.session.get( + url, + params=params, + timeout=self.default_timeout, + verify=False + ) + elapsed_time = time.time() - start_time + logger.debug(f"Auth request completed in {elapsed_time:.2f}s with status code {response.status_code}") + + # Проверка статуса HTTP + if response.status_code != 200: + logger.error(f"HTTP error: {response.status_code}") + logger.debug(f"Response content: {response.text[:200]}") + continue # Пробуем следующую версию + + try: + data = response.json() + except json.JSONDecodeError: + logger.error("Failed to decode JSON response") + logger.debug(f"Response content: {response.text[:200]}") + continue # Пробуем следующую версию + + if data.get("success"): + self.sid = data.get("data", {}).get("sid") + self._last_auth_time = time.time() + logger.info(f"Successfully logged in to Synology NAS using API version {auth_version}") + logger.debug(f"Session ID obtained: {self.sid[:5] if self.sid else 'None'}...") + + # Получаем и сохраняем токен SYNO, если он есть + syno_token = data.get("data", {}).get("synotoken") + if syno_token: + self.session.headers.update({'X-SYNO-TOKEN': syno_token}) + logger.debug("Added X-SYNO-TOKEN header for improved API compatibility") + + # Также добавляем SID в cookies для улучшения совместимости + self.session.cookies.update({ + 'id': self.sid, + 'sid': self.sid + }) + logger.debug("Added SID to session cookies for improved compatibility") + + # Проверка валидности полученной сессии с помощью простого запроса + # Будем использовать SYNO.API.Info без проверки сложных методов + + # Даем системе немного времени для инициализации сессии + time.sleep(0.5) + + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to log in with version {auth_version}: Error code {error_code}") + + # Если ошибка связана с версией API, пробуем следующую версию + if error_code in [104, 105]: + logger.warning(f"Auth version {auth_version} not supported, trying next version") + continue + + # Дополнительная диагностика + if error_code == 400: + logger.error("Authentication error: Invalid credentials") + elif error_code == 401: + logger.error("Authentication error: Account disabled") + elif error_code == 402: + logger.error("Authentication error: Permission denied") + elif error_code == 403: + logger.error("Authentication error: 2-factor authentication required") + elif error_code == 404: + logger.error("Authentication error: Failed to authenticate with 2-factor authentication") + + # Если ошибка связана с неверными учетными данными, нет смысла пробовать другие версии API + if error_code in [400, 401, 402, 403, 404]: + return False + + except requests.exceptions.Timeout: + logger.error(f"Connection timeout during auth with version {auth_version}") + continue # Пробуем следующую версию + except requests.exceptions.ConnectionError as e: + logger.error(f"Connection error during auth with version {auth_version}: {str(e)}") + continue # Пробуем следующую версию + except requests.RequestException as e: + logger.error(f"Request error during auth with version {auth_version}: {str(e)}") + continue # Пробуем следующую версию + except Exception as e: + logger.error(f"Unexpected error during login with version {auth_version}: {str(e)}", exc_info=True) + continue # Пробуем следующую версию + + # Если все версии не сработали + logger.error("Failed to authenticate with any API version") + return False + + def _validate_session(self) -> bool: + """Проверяет валидность сессии после авторизации""" + if not self.sid: + return False + + # Попробуем сделать простой запрос для проверки сессии + test_apis = [ + {"api": "SYNO.Core.System", "method": "info", "version": 1}, + {"api": "SYNO.DSM.Info", "method": "getinfo", "version": 1} + ] + + for test_api in test_apis: + try: + url = f"{self.base_url}/entry.cgi" + params = { + "api": test_api["api"], + "version": str(test_api["version"]), + "method": test_api["method"], + "_sid": self.sid + } + + response = self.session.get(url, params=params, timeout=self.default_timeout, verify=False) + + if response.status_code == 200: + data = response.json() + if data.get("success"): + logger.debug(f"Session validation successful using {test_api['api']}") + return True + else: + error_code = data.get("error", {}).get("code", -1) + if error_code != 119: # Не сессия истекла + logger.debug(f"Session validation with {test_api['api']} returned error code {error_code}") + return True # Считаем сессию валидной, если ошибка не связана с истечением сессии + except Exception as e: + logger.warning(f"Error during session validation with {test_api['api']}: {str(e)}") + + logger.warning("Session validation failed with all test APIs") + return False + + def logout(self) -> bool: + """Выход из API Synology NAS""" + if not self.sid: + return True + + try: + url = f"{self.base_url}/auth.cgi" + params = { + "api": "SYNO.API.Auth", + "version": "1", + "method": "logout", + "session": "SynologyPowerControlBot", + "_sid": self.sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + self.sid = None + logger.info("Successfully logged out from Synology NAS") + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to log out from Synology NAS: Error code {error_code}") + return False + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + return False + + def _make_api_request(self, api_name: str, method: str, version: int = 1, + params: Optional[Dict[str, Any]] = None, retry_auth: bool = True, retry_count: int = 0) -> Optional[Dict[str, Any]]: + """Обобщенный метод для выполнения API запросов с обработкой ошибок""" + # Ограничение на количество повторных попыток + if retry_count >= 3: + logger.error(f"Too many retries for {api_name}.{method}, giving up") + return None + + # Проверка наличия авторизации + if not self.sid and not self.login(): + logger.error(f"Not authenticated for API request: {api_name}.{method}") + return None + + # Проверка информации API для определения пути и поддерживаемой версии + api_info_url = f"{self.base_url}/entry.cgi" + api_info_params = { + "api": "SYNO.API.Info", + "version": "1", + "method": "query", + "query": api_name + } + + api_path = "entry.cgi" + try: + logger.debug(f"Querying API info for {api_name}") + api_info_response = self.session.get( + api_info_url, + params=api_info_params, + timeout=self.default_timeout, + verify=False + ) + + if api_info_response.status_code == 200: + api_info_data = api_info_response.json() + if api_info_data.get("success"): + api_info = api_info_data.get("data", {}).get(api_name, {}) + if api_info: + max_version = api_info.get("maxVersion", version) + min_version = api_info.get("minVersion", version) + api_path = api_info.get("path", "entry.cgi") + + # Проверка, поддерживается ли запрошенная версия + if version < min_version: + logger.warning(f"API version {version} for {api_name} is below minimum {min_version}, using {min_version}") + version = min_version + elif version > max_version: + logger.warning(f"API version {version} for {api_name} exceeds maximum {max_version}, using {max_version}") + version = max_version + + logger.debug(f"Using API path: {api_path}, version: {version}") + else: + logger.warning(f"API {api_name} not found in API info, using defaults") + except Exception as e: + logger.warning(f"Error querying API info for {api_name}: {str(e)}, using defaults") + + # Подготовка базовых параметров запроса + base_params = { + "api": api_name, + "version": str(version), + "method": method, + "_sid": self.sid # Используем _sid вместо sid для лучшей совместимости + } + + # Добавление дополнительных параметров, если они заданы + if params: + base_params.update(params) + + url = f"{self.base_url}/{api_path}" + logger.debug(f"API request: {api_name}.{method} v{version} to {url}") + logger.debug(f"Full request params: {base_params}") + + try: + start_time = time.time() + response = self.session.get( + url, + params=base_params, + timeout=self.default_timeout, + verify=False + ) + elapsed_time = time.time() - start_time + logger.debug(f"API request completed in {elapsed_time:.2f}s with status {response.status_code}") + + # Проверка статуса HTTP + if response.status_code != 200: + logger.error(f"HTTP error: {response.status_code} for {api_name}.{method}") + + # Повторная попытка при ошибках соединения + if response.status_code in [500, 502, 503, 504]: + logger.info(f"Server error, retrying request for {api_name}.{method}") + sleep(2) + return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1) + + return None + + try: + data = response.json() + except json.JSONDecodeError: + logger.error(f"Failed to decode JSON response for {api_name}.{method}") + logger.debug(f"Response content: {response.text[:200]}") + + # Повторная попытка при ошибках декодирования + logger.info(f"JSON decode error, retrying request for {api_name}.{method}") + sleep(2) + return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1) + + if data.get("success"): + logger.info(f"API request successful for {api_name}.{method}") + return data.get("data", {}) + else: + error_code = data.get("error", {}).get("code", -1) + error_desc = self._get_error_description(error_code) + logger.error(f"API error for {api_name}.{method}: {error_code} - {error_desc}") + + # Ошибки доступа или прав часто встречаются, но они не критичные + # Например, ошибка 102 означает, что нет прав, но NAS доступен + if error_code in [102, 103, 104, 105]: + logger.warning(f"Permission-related error for {api_name}.{method}: {error_code}") + # Возвращаем пустой словарь вместо None, + # чтобы вызывающий код мог понять, что запрос выполнен + return {} + + # Если ошибка связана с авторизацией и нам разрешено повторить попытку + if error_code in [106, 107, 119] and retry_auth: + logger.info(f"Session error (code {error_code}), creating fresh session...") + self.sid = None # Сбрасываем SID + + # Для ошибки 119 (Session timeout) дадим системе немного времени + if error_code == 119: + logger.info("Session timeout detected, waiting before retry...") + sleep(3) + + if self.login(): + logger.info("Re-authenticated with fresh session, retrying API request...") + # Рекурсивный вызов, но со счетчиком повторов + return self._make_api_request(api_name, method, version, params, False, retry_count + 1) + + # Для некоторых ошибок можно автоматически повторить запрос + if error_code in [408, 429, 500, 502, 503, 504]: + logger.info(f"Temporary error {error_code}, retrying request for {api_name}.{method}") + sleep(2) + return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1) + + return None + + except requests.exceptions.Timeout: + logger.error(f"Request timeout for {api_name}.{method}") + + # Повторная попытка при таймауте + if retry_count < 2: + logger.info(f"Timeout, retrying request for {api_name}.{method}") + sleep(2) + return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1) + + return None + except requests.exceptions.ConnectionError as e: + logger.error(f"Connection error for {api_name}.{method}: {str(e)}") + + # Повторная попытка при ошибке соединения + if retry_count < 2: + logger.info(f"Connection error, retrying request for {api_name}.{method}") + sleep(2) + return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1) + + return None + except Exception as e: + logger.error(f"Unexpected error during {api_name}.{method}: {str(e)}") + return None + + def get_system_status(self) -> Dict[str, Any]: + """Получение статуса системы""" + # Проверяем доступность системы + if not self.is_online(): + logger.info("Device is offline, skipping API request") + return {"status": "offline"} + + # Проверяем, есть ли кэшированный результат + cache_key = "system_status" + current_time = time.time() + if cache_key in self._cache and current_time - self._cache_ttl.get(cache_key, 0) < 60: + logger.debug("Using cached system status") + return self._cache[cache_key] + + # Используем рекомендованный API для получения информации о системе + logger.info(f"Getting system info using {SYNOLOGY_INFO_API} v{SYNOLOGY_API_VERSION}") + + # Для SYNO.DSM.Info метод getinfo, для других обычно используется get или info + if SYNOLOGY_INFO_API == "SYNO.DSM.Info": + method = "getinfo" + else: + method = "get" + + result = self._make_api_request(SYNOLOGY_INFO_API, method, version=SYNOLOGY_API_VERSION) + + if result: + logger.info(f"Successfully retrieved system info using {SYNOLOGY_INFO_API}") + + # Формируем расширенный ответ с дополнительной информацией + system_info = { + "status": "online", + "hostname": result.get("hostname", "unknown"), + "model": result.get("model", "unknown"), + "version": result.get("version", "unknown"), + "uptime": result.get("uptime", 0), + "time": current_time, + "is_online": True, + "api_used": SYNOLOGY_INFO_API + } + + # Сохраняем в кэше + self._cache[cache_key] = system_info + self._cache_ttl[cache_key] = current_time + + return system_info + + # Если основной API не сработал, пробуем резервные варианты + logger.warning(f"Failed to retrieve system info with {SYNOLOGY_INFO_API}, trying fallback APIs") + + # Пробуем резервные API + apis_to_try = [ + {"name": "SYNO.DSM.Info", "method": "getinfo", "version": 2}, + {"name": "SYNO.Core.System", "method": "info", "version": 1}, + {"name": "SYNO.Core.System.Status", "method": "get", "version": 1}, + {"name": "SYNO.Core.System.Info", "method": "get", "version": 1}, + ] + + for api in apis_to_try: + if api["name"] == SYNOLOGY_INFO_API: + continue # Пропускаем уже проверенный API + + logger.info(f"Trying to get system info using {api['name']}.{api['method']} v{api['version']}") + result = self._make_api_request(api["name"], api["method"], version=api["version"]) + + if result: + logger.info(f"Successfully retrieved system info using {api['name']}") + + # Формируем расширенный ответ с дополнительной информацией + system_info = { + "status": "online", + "hostname": result.get("hostname", "unknown"), + "model": result.get("model", "unknown"), + "version": result.get("version", "unknown"), + "uptime": result.get("uptime", 0), + "time": current_time, + "is_online": True, + "api_used": api["name"] + } + + # Сохраняем в кэше + self._cache[cache_key] = system_info + self._cache_ttl[cache_key] = current_time + + return system_info + + # Если все запросы не удались, но система онлайн, возвращаем базовую информацию + logger.warning("Failed to retrieve system info with all API methods") + return { + "status": "error", + "error": "Failed to fetch system information", + "is_online": True, + "time": current_time + } + + def shutdown_system(self) -> bool: + """Выключение системы""" + # Проверяем, включено ли устройство перед попыткой его выключить + if not self.is_online(force_check=True): + logger.info("Synology NAS is already offline, no need to shut down") + return True + + logger.info("Attempting to shutdown Synology NAS...") + + # Попробуем сначала использовать предпочтительный API для управления питанием + logger.info(f"Trying shutdown with {SYNOLOGY_POWER_API} v{SYNOLOGY_API_VERSION}") + + # Для SYNO.Core.Hardware.PowerRecovery используется метод setPowerOnState + # Для других API обычно используется метод shutdown или reboot + if SYNOLOGY_POWER_API == "SYNO.Core.Hardware.PowerRecovery": + # Для этого API нужны специальные параметры + params = {"state": "powerbtn"} # powerbtn имитирует нажатие кнопки питания + result = self._make_api_request(SYNOLOGY_POWER_API, "setPowerOnState", version=SYNOLOGY_API_VERSION, params=params) + else: + # Пробуем стандартный метод + result = self._make_api_request(SYNOLOGY_POWER_API, "shutdown", version=SYNOLOGY_API_VERSION) + + if result is not None: + logger.info(f"Successfully initiated system shutdown using {SYNOLOGY_POWER_API}") + + # Даем системе время начать процесс выключения + logger.info("Waiting for shutdown to initialize...") + sleep(5) + + # Проверяем статус + if not self.is_online(force_check=True): + logger.info("System is now offline. Shutdown confirmed successful.") + return True + else: + logger.info("System still appears to be online, but shutdown may be in progress.") + return True + else: + logger.warning(f"Failed to shutdown using {SYNOLOGY_POWER_API}, trying alternative methods") + + # Если не сработал основной метод, пробуем резервные варианты + # Проверка всех доступных методов API для выключения + apis_to_try = [ + {"name": "SYNO.Core.System", "method": "shutdown", "version": 3}, + {"name": "SYNO.Core.System.Power", "method": "shutdown", "version": 1}, + {"name": "SYNO.DSM.Power", "method": "shutdown", "version": 1}, + {"name": "SYNO.System.Power", "method": "shutdown", "version": 1}, + {"name": "SYNO.DSM.System", "method": "shutdown", "version": 1} + ] + + # Проверяем доступные API + try: + api_info_url = f"{self.base_url}/entry.cgi" + api_info_params = { + "api": "SYNO.API.Info", + "version": "1", + "method": "query", + "query": "SYNO.Core.System.Power,SYNO.DSM.Power,SYNO.Core.System,SYNO.System.Power,SYNO.DSM.System" + } + + logger.debug("Checking available shutdown APIs") + api_info_response = self.session.get( + api_info_url, + params=api_info_params, + timeout=self.default_timeout, + verify=False + ) + + if api_info_response.status_code == 200: + api_info_data = api_info_response.json() + if api_info_data.get("success"): + available_apis = api_info_data.get("data", {}) + logger.debug(f"Available APIs: {list(available_apis.keys())}") + + # Фильтруем только доступные API + filtered_apis = [] + for api in apis_to_try: + if api["name"] in available_apis: + api_info = available_apis[api["name"]] + # Проверка версии API + if api_info.get("minVersion", 1) <= api["version"] <= api_info.get("maxVersion", 1): + filtered_apis.append(api) + logger.debug(f"Adding {api['name']} to available shutdown APIs") + else: + logger.debug(f"API {api['name']} version {api['version']} not supported. Supported: {api_info.get('minVersion')}-{api_info.get('maxVersion')}") + + if filtered_apis: + apis_to_try = filtered_apis + else: + logger.warning("No compatible APIs found, trying all methods as fallback") + else: + logger.warning("Failed to query API info, using default methods") + else: + logger.warning(f"API info request failed with status {api_info_response.status_code}, using default methods") + except Exception as e: + logger.warning(f"Error querying API info: {str(e)}, using default methods") + + # Пробуем все доступные методы по порядку + for api in apis_to_try: + logger.info(f"Trying shutdown with {api['name']}.{api['method']} v{api['version']}") + result = self._make_api_request(api["name"], api["method"], version=api["version"]) + + if result is not None: + logger.info(f"Successfully initiated system shutdown using {api['name']}") + + # Даем системе время начать процесс выключения + logger.info("Waiting for shutdown to initialize...") + sleep(5) + + # Проверяем статус + if not self.is_online(force_check=True): + logger.info("System is now offline. Shutdown confirmed successful.") + return True + else: + logger.info("System still appears to be online, but shutdown may be in progress.") + return True + else: + logger.warning(f"Failed to shutdown using {api['name']}.{api['method']} v{api['version']}") + + # Если ни один метод не сработал, но система стала недоступна + if not self.is_online(force_check=True): + logger.info("System appears to be shutting down despite API errors") + return True + + logger.error("Failed to shutdown system after trying multiple APIs") + return False + + def reboot_system(self) -> bool: + """Перезагрузка системы""" + # Проверяем, включена ли система + if not self.is_online(force_check=True): + logger.error("Cannot reboot: System is offline") + return False + + logger.info("Attempting to reboot Synology NAS...") + + # Попробуем сначала использовать предпочтительный API для управления питанием + logger.info(f"Trying reboot with {SYNOLOGY_POWER_API} v{SYNOLOGY_API_VERSION}") + + # Для SYNO.Core.Hardware.PowerRecovery используется метод setPowerOnState или powerButton + # Для других API обычно используется метод restart или reboot + if SYNOLOGY_POWER_API == "SYNO.Core.Hardware.PowerRecovery": + # Для этого API нужны специальные параметры + params = {"reboot": "true"} # Передаем флаг для перезагрузки + result = self._make_api_request(SYNOLOGY_POWER_API, "setPowerOnState", version=SYNOLOGY_API_VERSION, params=params) + else: + # Пробуем стандартный метод + methods_to_try = ["restart", "reboot"] + result = None + for method in methods_to_try: + result = self._make_api_request(SYNOLOGY_POWER_API, method, version=SYNOLOGY_API_VERSION) + if result is not None: + logger.info(f"Successfully used method {method} for {SYNOLOGY_POWER_API}") + break + + if result is not None: + logger.info(f"Successfully initiated system reboot using {SYNOLOGY_POWER_API}") + + # Даем системе время начать процесс перезагрузки + logger.info("Waiting for reboot to initialize...") + sleep(5) + + # Ждем, пока система станет недоступна (признак перезагрузки) + reboot_started = False + for i in range(12): # Проверяем в течение 60 секунд (12 * 5сек) + if not self.is_online(force_check=True): + logger.info(f"System went offline after {i*5} seconds, reboot in progress") + reboot_started = True + break + logger.debug(f"System still online, waiting... ({i+1}/12)") + sleep(5) + + if reboot_started: + # Ждем, пока система снова станет доступна + logger.info("Waiting for system to come back online...") + return self.wait_for_boot(max_attempts=30, delay=10) + else: + logger.warning("System did not go offline after reboot command, but command was accepted") + # Даже если система не ушла оффлайн, команда могла быть принята + return True + else: + logger.warning(f"Failed to reboot using {SYNOLOGY_POWER_API}, trying alternative methods") + + # Если не сработал основной метод, пробуем резервные варианты + # Проверка всех доступных методов API для перезагрузки + apis_to_try = [ + {"name": "SYNO.Core.System.Power", "method": "restart", "version": 1}, + {"name": "SYNO.DSM.Power", "method": "reboot", "version": 1}, + {"name": "SYNO.Core.System", "method": "reboot", "version": 3}, + {"name": "SYNO.System.Power", "method": "reboot", "version": 1}, + {"name": "SYNO.DSM.System", "method": "reboot", "version": 1}, + {"name": "SYNO.Core.Hardware.NeedReboot", "method": "reboot", "version": 1} + ] + + # Получаем список доступных API + try: + api_info_url = f"{self.base_url}/entry.cgi" + api_info_params = { + "api": "SYNO.API.Info", + "version": "1", + "method": "query", + "query": "SYNO.Core.System.Power,SYNO.DSM.Power,SYNO.Core.System,SYNO.System.Power,SYNO.DSM.System,SYNO.Core.Hardware.NeedReboot" + } + + logger.debug("Checking available reboot APIs") + api_info_response = self.session.get( + api_info_url, + params=api_info_params, + timeout=self.default_timeout, + verify=False + ) + + if api_info_response.status_code == 200: + api_info_data = api_info_response.json() + if api_info_data.get("success"): + available_apis = api_info_data.get("data", {}) + logger.debug(f"Available APIs: {list(available_apis.keys())}") + + # Фильтруем только доступные API + filtered_apis = [] + for api in apis_to_try: + if api["name"] in available_apis: + api_info = available_apis[api["name"]] + # Проверка версии API + if api_info.get("minVersion", 1) <= api["version"] <= api_info.get("maxVersion", 1): + filtered_apis.append(api) + logger.debug(f"Adding {api['name']} to available reboot APIs") + else: + logger.debug(f"API {api['name']} version {api['version']} not supported. Supported: {api_info.get('minVersion')}-{api_info.get('maxVersion')}") + + if filtered_apis: + apis_to_try = filtered_apis + else: + logger.warning("No compatible APIs found, trying all methods as fallback") + else: + logger.warning("Failed to query API info, using default methods") + else: + logger.warning(f"API info request failed with status {api_info_response.status_code}, using default methods") + except Exception as e: + logger.warning(f"Error querying API info: {str(e)}, using default methods") + + # Пробуем все доступные методы по порядку + for api in apis_to_try: + logger.info(f"Trying reboot with {api['name']}.{api['method']} v{api['version']}") + result = self._make_api_request(api["name"], api["method"], version=api["version"]) + + if result is not None: + logger.info(f"Successfully initiated system reboot using {api['name']}") + + # Даем системе время начать процесс перезагрузки + logger.info("Waiting for reboot to initialize...") + sleep(5) + + # Ждем, пока система станет недоступна (признак перезагрузки) + reboot_started = False + for i in range(12): # Проверяем в течение 60 секунд (12 * 5сек) + if not self.is_online(force_check=True): + logger.info(f"System went offline after {i*5} seconds, reboot in progress") + reboot_started = True + break + logger.debug(f"System still online, waiting... ({i+1}/12)") + sleep(5) + + if reboot_started: + # Ждем, пока система снова станет доступна + logger.info("Waiting for system to come back online...") + return self.wait_for_boot(max_attempts=30, delay=10) + else: + logger.warning("System did not go offline after reboot command, but command was accepted") + # Даже если система не ушла оффлайн, команда могла быть принята + return True + else: + logger.warning(f"Failed to reboot using {api['name']}.{api['method']} v{api['version']}") + + logger.error("Failed to reboot system after trying multiple APIs") + return False + + def _get_error_description(self, error_code: int) -> str: + """Получение описания ошибки по коду""" + error_descriptions = { + 100: "Unknown error", + 101: "Invalid parameter", + 102: "API does not exist", + 103: "Method does not exist", + 104: "Version does not support", + 105: "Permission denied", + 106: "Session timeout", + 107: "Session interrupted by duplicate login", + 400: "Invalid credentials", + 401: "Account disabled", + 402: "Permission denied", + 403: "2FA required", + 404: "Failed to authenticate with 2FA" + } + return error_descriptions.get(error_code, "Unknown error code") + + def _check_tcp_connection(self) -> bool: + """Проверка базового TCP-соединения с Synology NAS""" + try: + socket_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + socket_obj.settimeout(SYNOLOGY_TIMEOUT) + result = socket_obj.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT)) + socket_obj.close() + + return result == 0 + except socket.error as e: + logger.error(f"Socket error during connection check: {str(e)}") + return False + except Exception as e: + logger.error(f"Unexpected error during connection check: {str(e)}") + return False + + def is_online(self, force_check=False) -> bool: + """Проверка онлайн-статуса Synology NAS""" + # Используем кэшированное значение, если доступно и не устарело + current_time = time.time() + if not force_check and (current_time - self._last_online_check) < self._online_check_interval: + logger.debug(f"Using cached online status: {self._last_online_status}") + return self._last_online_status + + logger.info("Checking if NAS is online...") + + # Проверяем TCP-соединение + online_status = self._check_tcp_connection() + logger.info(f"Detected Synology NAS online status: {online_status}") + + # Если TCP-соединение успешно и у нас есть действующий SID, + # попробуем более детальную проверку через API + if online_status and self.sid: + logger.info("Trying to fetch more detailed online status through API...") + + # Пробуем разные API для проверки онлайн-статуса + api_checks = [ + {"api": "SYNO.DSM.Info", "version": "2", "method": "getinfo"}, + {"api": "SYNO.Core.System", "version": "1", "method": "info"}, + {"api": "SYNO.Core.System.Status", "version": "1", "method": "get"} + ] + + api_success = False + for api_check in api_checks: + try: + url = f"{self.base_url}/entry.cgi" + params = { + "api": api_check["api"], + "version": api_check["version"], + "method": api_check["method"], + "sid": self.sid + } + + logger.debug(f"Trying online status check with {api_check['api']}") + response = self.session.get( + url, + params=params, + timeout=self.default_timeout, + verify=False + ) + + if response.status_code == 200: + data = response.json() + if data.get("success"): + logger.info(f"API request successful for {api_check['api']}") + logger.info("Synology NAS is online with API access") + api_success = True + break + else: + error_code = data.get("error", {}).get("code", -1) + logger.warning(f"API response indicates an error: {error_code}, but NAS is reachable") + else: + logger.warning(f"API returned status code {response.status_code}") + except Exception as e: + logger.warning(f"API check failed for {api_check['api']}, but TCP connection succeeded: {str(e)}") + + if not api_success: + logger.warning("All API checks failed, but TCP connection is successful") + + # Обновляем кэшированное значение + self._last_online_check = current_time + self._last_online_status = online_status + + return online_status + + def wake_on_lan(self) -> bool: + """Отправка Wake-on-LAN пакета для включения Synology NAS""" + if not SYNOLOGY_MAC: + logger.error("MAC address not configured") + return False + + try: + # Преобразование MAC-адреса в байты + mac_address = SYNOLOGY_MAC.replace(':', '').replace('-', '') + if len(mac_address) != 12: + logger.error(f"Invalid MAC address format: {SYNOLOGY_MAC}") + return False + + try: + mac_bytes = bytes.fromhex(mac_address) + except ValueError as e: + logger.error(f"Failed to parse MAC address: {str(e)}") + logger.error(f"MAC address must contain only hex characters: {SYNOLOGY_MAC}") + return False + + # Создание Magic Packet + magic_packet = b'\xff' * 6 + mac_bytes * 16 + + # Отправка пакета на конкретный адрес + try: + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) + sock.sendto(magic_packet, (SYNOLOGY_HOST, WOL_PORT)) + sock.close() + logger.info(f"Wake-on-LAN packet sent to {SYNOLOGY_MAC} at {SYNOLOGY_HOST}:{WOL_PORT}") + except Exception as e: + logger.error(f"Error sending directed WoL packet: {str(e)}") + return False + + # Для надежности отправляем также широковещательный пакет + try: + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) + + # Используем стандартный широковещательный адрес + broadcast_addr = "255.255.255.255" + sock.sendto(magic_packet, (broadcast_addr, WOL_PORT)) + sock.close() + logger.info(f"Wake-on-LAN broadcast packet sent to {broadcast_addr}:{WOL_PORT}") + except Exception as e: + logger.warning(f"Error sending broadcast WoL packet (non-critical): {str(e)}") + # Не считаем ошибкой, т.к. основной пакет уже отправлен + + return True + + except Exception as e: + logger.error(f"Unexpected error in wake_on_lan: {str(e)}") + return False + + def wait_for_boot(self, max_attempts: int = 30, delay: int = 10) -> bool: + """Ожидание загрузки Synology NAS""" + logger.info(f"Waiting for Synology NAS to boot (timeout: {max_attempts * delay}s)...") + + for attempt in range(max_attempts): + # Принудительно проверяем статус без использования кэша + if self.is_online(force_check=True): + logger.info(f"Synology NAS is online after {attempt + 1} attempts ({(attempt + 1) * delay}s)") + + # Проверяем, что не только сеть доступна, но и API загрузился + api_ready = False + logger.info("Waiting for API services to initialize...") + + for api_check in range(5): # Даем еще до 50 секунд для загрузки API + if self.sid or self.login(): + api_ready = True + logger.info(f"API services are ready after {api_check + 1} attempts") + break + logger.debug(f"API not ready yet, waiting... ({api_check + 1}/5)") + sleep(10) + + if not api_ready: + logger.warning("System is online but API services may not be fully initialized") + + # Дадим дополнительное время для полной загрузки всех сервисов + sleep(delay) + return True + + sleep(delay) + logger.info(f"Waiting for boot... Attempt {attempt + 1}/{max_attempts}") + + logger.error(f"Synology NAS didn't come online after {max_attempts * delay} seconds") + return False + + def power_on(self) -> bool: + """Включение Synology NAS""" + # Принудительная проверка статуса + if self.is_online(force_check=True): + logger.info("Synology NAS is already online") + return True + + logger.info("Powering on Synology NAS via Wake-on-LAN...") + + # Проверяем, настроен ли MAC-адрес + if not SYNOLOGY_MAC: + logger.error("Cannot power on: MAC address not configured in settings") + return False + + # Пробуем отправить несколько WoL пакетов для надежности + success = False + for attempt in range(3): + logger.debug(f"Sending WoL packet, attempt {attempt + 1}/3") + if self.wake_on_lan(): + success = True + break + sleep(1) + + if not success: + logger.error("Failed to send Wake-on-LAN packets") + return False + + # Ожидание загрузки + logger.info("WoL packets sent successfully, waiting for system to boot...") + boot_result = self.wait_for_boot(max_attempts=30, delay=10) + + if boot_result: + # Проверяем доступность API после загрузки + system_status = self.get_system_status() + if system_status.get("status") == "online": + logger.info("System booted successfully with API access") + return True + else: + logger.warning("System appears to be online but API may not be fully ready") + return True + else: + logger.error("System did not come online after WoL") + return False + + def power_off(self) -> bool: + """Выключение Synology NAS""" + if not self.is_online(force_check=True): + logger.info("Synology NAS is already offline") + return True + + logger.info("Powering off Synology NAS...") + result = self.shutdown_system() + + if result: + # Дополнительная проверка, что система действительно выключилась + logger.info("Verifying system is offline...") + for attempt in range(12): # Проверяем в течение 2 минут (12 * 10 сек) + if not self.is_online(force_check=True): + logger.info(f"System confirmed offline after {attempt * 10} seconds") + return True + logger.debug(f"System still shutting down, checking again... ({attempt + 1}/12)") + sleep(10) + + logger.warning("System still appears to be online after shutdown command") + return False + else: + logger.error("Failed to initiate shutdown") + return False + + # Заглушки для расширенных методов + def get_shared_folders(self) -> List[Dict[str, Any]]: + """Получение списка общих папок (заглушка)""" + logger.warning("Function get_shared_folders() is not implemented yet") + return [] + + def get_system_load(self) -> Dict[str, Any]: + """Получение информации о загрузке системы (заглушка)""" + logger.warning("Function get_system_load() is not implemented yet") + return {} + + def get_storage_status(self) -> Dict[str, Any]: + """Получение подробной информации о хранилище (заглушка)""" + logger.warning("Function get_storage_status() is not implemented yet") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for storage status request") + return {"error": "authentication_failed"} + + try: + # Здесь будет реализация после добавления соответствующего API + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.Storage.CGI.Storage", + "version": "1", + "method": "load_info", + "sid": self.sid + } + + # В текущей версии просто возвращаем заглушку + return { + "volumes": [], + "disks": [], + "total_size": 0, + "total_used": 0, + "error": "not_implemented" + } + except Exception as e: + logger.error(f"Error in get_storage_status: {str(e)}") + return {"error": str(e)} + + def get_security_status(self) -> Dict[str, Any]: + """Получение информации о состоянии безопасности (заглушка)""" + logger.warning("Function get_security_status() is not implemented yet") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for security status request") + return {"success": False, "error": "authentication_failed"} + + try: + # Здесь будет реализация после добавления соответствующего API + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.Core.SecurityScan.Status", + "version": "1", + "method": "get", + "sid": self.sid + } + + # В текущей версии просто возвращаем заглушку + return { + "success": False, + "status": "not_implemented", + "last_check": None, + "is_secure": False, + "error": "not_implemented" + } + except Exception as e: + logger.error(f"Error in get_security_status: {str(e)}") + return {"success": False, "error": str(e)} diff --git a/.history/src/api/synology_20250830090902.py b/.history/src/api/synology_20250830090902.py new file mode 100644 index 0000000..8c4a557 --- /dev/null +++ b/.history/src/api/synology_20250830090902.py @@ -0,0 +1,1317 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Модуль для взаимодействия с API Synology NAS +""" + +import requests +from requests.adapters import HTTPAdapter +import json +import logging +import time +import urllib3 +from urllib3.util import Retry +from typing import Dict, Any, Optional, List +import socket +import struct +from time import sleep + +from src.config.config import ( + SYNOLOGY_HOST, + SYNOLOGY_PORT, + SYNOLOGY_USERNAME, + SYNOLOGY_PASSWORD, + SYNOLOGY_SECURE, + SYNOLOGY_TIMEOUT, + SYNOLOGY_MAC, + WOL_PORT, + SYNOLOGY_API_VERSION, + SYNOLOGY_POWER_API, + SYNOLOGY_INFO_API +) +from src.api.api_discovery import discover_available_apis, find_compatible_api + +# Отключение предупреждений о небезопасных SSL-соединениях +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + +logger = logging.getLogger(__name__) + +class SynologyAPI: + """Класс для взаимодействия с API Synology NAS""" + + def __init__(self): + """Инициализация класса SynologyAPI""" + logger.info("Creating API with auto-retry and connection pool") + logger.debug(f"Connection details: Host={SYNOLOGY_HOST}, Port={SYNOLOGY_PORT}, Secure={SYNOLOGY_SECURE}") + + self.protocol = "https" if SYNOLOGY_SECURE else "http" + self.base_url = f"{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi" + self.sid = None + self.session = requests.Session() + + # Настройка SSL + if self.protocol == "https": + logger.debug("SSL enabled, disabling certificate verification for internal network") + self.session.verify = False # Отключаем проверку SSL для внутренней сети + + # Добавляем пользовательские заголовки для улучшения совместимости с API + custom_headers = { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36', + 'Accept': 'application/json, text/javascript, */*; q=0.01', + 'Accept-Language': 'ru-RU,ru;q=0.9,en-US;q=0.8,en;q=0.7', + 'X-Requested-With': 'XMLHttpRequest', + 'Connection': 'keep-alive', + 'Referer': f'{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/' + } + self.session.headers.update(custom_headers) + logger.debug("Added browser-like headers for API compatibility") + + # Добавляем повторные попытки для HTTP-запросов + retry_strategy = Retry( + total=5, # Увеличиваем количество попыток + status_forcelist=[429, 500, 502, 503, 504, 404], + allowed_methods=["GET", "POST"], + backoff_factor=1.5, # Увеличиваем задержку между попытками + respect_retry_after_header=True + ) + adapter = HTTPAdapter( + max_retries=retry_strategy, + pool_connections=3, + pool_maxsize=10 + ) + self.session.mount("http://", adapter) + self.session.mount("https://", adapter) + + # Таймауты будут указаны в запросах + self.default_timeout = (SYNOLOGY_TIMEOUT, SYNOLOGY_TIMEOUT*2) + logger.debug(f"Setting default request timeout: {self.default_timeout}") + + # Кэш для хранения результатов запросов + self._cache = {} + self._cache_ttl = {} + self._last_online_check = 0 + self._last_online_status = False + self._online_check_interval = 30 # Интервал для кэширования проверок онлайн-статуса + + # Время последней успешной аутентификации и срок действия сессии + self._last_auth_time = 0 + self._auth_expiry = 3600 # По умолчанию 1 час + + # Информация о доступных API + self._available_apis = {} + self._api_info_ttl = 0 + + # Инициализируем API version resolver для автоматического определения совместимых API + self.api_resolver = None # Будет создан при необходимости + + def login(self) -> bool: + """Авторизация в API Synology NAS""" + # Сбрасываем SID для новой сессии + self.sid = None + + logger.info("Attempting to authenticate with Synology NAS...") + logger.debug(f"Base URL: {self.base_url}") + + # Проверяем доступность NAS перед запросом с помощью прямой TCP-проверки + # Избегаем вызова is_online(), чтобы не создавать рекурсию + online_status = self._check_tcp_connection() + if not online_status: + logger.error("Cannot login: Synology NAS is not reachable") + return False + + # Пробуем различные версии API для аутентификации + # Начинаем с версии 3, которая показала лучшую совместимость в тестах + auth_versions_to_try = [3, 2, 1, 6] # Пробуем сначала более стабильные версии + + for auth_version in auth_versions_to_try: + try: + # Определяем путь к API аутентификации + auth_path = "entry.cgi" # Используем entry.cgi вместо auth.cgi по умолчанию + + # Проверка информации API для определения доступных версий API + api_info_url = f"{self.base_url}/entry.cgi" + api_info_params = { + "api": "SYNO.API.Info", + "version": "1", + "method": "query", + "query": "SYNO.API.Auth" + } + + logger.debug(f"Querying API info for auth version {auth_version}") + try: + api_info_response = self.session.get( + api_info_url, + params=api_info_params, + timeout=self.default_timeout, + verify=False + ) + + if api_info_response.status_code == 200: + api_info_data = api_info_response.json() + if api_info_data.get("success"): + auth_info = api_info_data.get("data", {}).get("SYNO.API.Auth", {}) + max_version = auth_info.get("maxVersion", 6) + min_version = auth_info.get("minVersion", 1) + auth_path = auth_info.get("path", "entry.cgi") + logger.debug(f"Auth API versions: min={min_version}, max={max_version}, path={auth_path}") + + # Проверяем поддержку текущей версии + if auth_version < min_version or auth_version > max_version: + logger.warning(f"Auth API version {auth_version} not supported (min={min_version}, max={max_version}), trying next version") + continue + else: + logger.warning("Failed to query API info, using default auth path") + else: + logger.warning(f"API info request failed with status {api_info_response.status_code}, using default auth path") + except Exception as e: + logger.warning(f"Error querying API info: {str(e)}, using default auth path") + + # Основной запрос авторизации + url = f"{self.base_url}/{auth_path}" + params = { + "api": "SYNO.API.Auth", + "version": str(auth_version), + "method": "login", + "account": SYNOLOGY_USERNAME, + "passwd": SYNOLOGY_PASSWORD, + "session": "SynologyPowerControlBot", + "format": "cookie" + } + + # Для версии 6+ используем немного другой формат + if auth_version >= 6: + params["enable_syno_token"] = "yes" + + logger.debug(f"Sending auth request to {url} with API version {auth_version}") + start_time = time.time() + response = self.session.get( + url, + params=params, + timeout=self.default_timeout, + verify=False + ) + elapsed_time = time.time() - start_time + logger.debug(f"Auth request completed in {elapsed_time:.2f}s with status code {response.status_code}") + + # Проверка статуса HTTP + if response.status_code != 200: + logger.error(f"HTTP error: {response.status_code}") + logger.debug(f"Response content: {response.text[:200]}") + continue # Пробуем следующую версию + + try: + data = response.json() + except json.JSONDecodeError: + logger.error("Failed to decode JSON response") + logger.debug(f"Response content: {response.text[:200]}") + continue # Пробуем следующую версию + + if data.get("success"): + self.sid = data.get("data", {}).get("sid") + self._last_auth_time = time.time() + logger.info(f"Successfully logged in to Synology NAS using API version {auth_version}") + logger.debug(f"Session ID obtained: {self.sid[:5] if self.sid else 'None'}...") + + # Получаем и сохраняем токен SYNO, если он есть + syno_token = data.get("data", {}).get("synotoken") + if syno_token: + self.session.headers.update({'X-SYNO-TOKEN': syno_token}) + logger.debug("Added X-SYNO-TOKEN header for improved API compatibility") + + # Также добавляем SID в cookies для улучшения совместимости + self.session.cookies.update({ + 'id': self.sid, + 'sid': self.sid + }) + logger.debug("Added SID to session cookies for improved compatibility") + + # Проверка валидности полученной сессии с помощью простого запроса + # Будем использовать SYNO.API.Info без проверки сложных методов + + # Даем системе немного времени для инициализации сессии + time.sleep(0.5) + + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to log in with version {auth_version}: Error code {error_code}") + + # Если ошибка связана с версией API, пробуем следующую версию + if error_code in [104, 105]: + logger.warning(f"Auth version {auth_version} not supported, trying next version") + continue + + # Дополнительная диагностика + if error_code == 400: + logger.error("Authentication error: Invalid credentials") + elif error_code == 401: + logger.error("Authentication error: Account disabled") + elif error_code == 402: + logger.error("Authentication error: Permission denied") + elif error_code == 403: + logger.error("Authentication error: 2-factor authentication required") + elif error_code == 404: + logger.error("Authentication error: Failed to authenticate with 2-factor authentication") + + # Если ошибка связана с неверными учетными данными, нет смысла пробовать другие версии API + if error_code in [400, 401, 402, 403, 404]: + return False + + except requests.exceptions.Timeout: + logger.error(f"Connection timeout during auth with version {auth_version}") + continue # Пробуем следующую версию + except requests.exceptions.ConnectionError as e: + logger.error(f"Connection error during auth with version {auth_version}: {str(e)}") + continue # Пробуем следующую версию + except requests.RequestException as e: + logger.error(f"Request error during auth with version {auth_version}: {str(e)}") + continue # Пробуем следующую версию + except Exception as e: + logger.error(f"Unexpected error during login with version {auth_version}: {str(e)}", exc_info=True) + continue # Пробуем следующую версию + + # Если все версии не сработали + logger.error("Failed to authenticate with any API version") + return False + + def _validate_session(self) -> bool: + """Проверяет валидность сессии после авторизации""" + if not self.sid: + return False + + # Попробуем сделать простой запрос для проверки сессии + test_apis = [ + {"api": "SYNO.Core.System", "method": "info", "version": 1}, + {"api": "SYNO.DSM.Info", "method": "getinfo", "version": 1} + ] + + for test_api in test_apis: + try: + url = f"{self.base_url}/entry.cgi" + params = { + "api": test_api["api"], + "version": str(test_api["version"]), + "method": test_api["method"], + "_sid": self.sid + } + + response = self.session.get(url, params=params, timeout=self.default_timeout, verify=False) + + if response.status_code == 200: + data = response.json() + if data.get("success"): + logger.debug(f"Session validation successful using {test_api['api']}") + return True + else: + error_code = data.get("error", {}).get("code", -1) + if error_code != 119: # Не сессия истекла + logger.debug(f"Session validation with {test_api['api']} returned error code {error_code}") + return True # Считаем сессию валидной, если ошибка не связана с истечением сессии + except Exception as e: + logger.warning(f"Error during session validation with {test_api['api']}: {str(e)}") + + logger.warning("Session validation failed with all test APIs") + return False + + def logout(self) -> bool: + """Выход из API Synology NAS""" + if not self.sid: + return True + + try: + url = f"{self.base_url}/auth.cgi" + params = { + "api": "SYNO.API.Auth", + "version": "1", + "method": "logout", + "session": "SynologyPowerControlBot", + "_sid": self.sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + self.sid = None + logger.info("Successfully logged out from Synology NAS") + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to log out from Synology NAS: Error code {error_code}") + return False + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + return False + + def _make_api_request(self, api_name: str, method: str, version: int = 1, + params: Optional[Dict[str, Any]] = None, retry_auth: bool = True, retry_count: int = 0) -> Optional[Dict[str, Any]]: + """Обобщенный метод для выполнения API запросов с обработкой ошибок""" + # Ограничение на количество повторных попыток + if retry_count >= 3: + logger.error(f"Too many retries for {api_name}.{method}, giving up") + return None + + # Проверка наличия авторизации + if not self.sid and not self.login(): + logger.error(f"Not authenticated for API request: {api_name}.{method}") + return None + + # Проверка информации API для определения пути и поддерживаемой версии + api_info_url = f"{self.base_url}/entry.cgi" + api_info_params = { + "api": "SYNO.API.Info", + "version": "1", + "method": "query", + "query": api_name + } + + api_path = "entry.cgi" + try: + logger.debug(f"Querying API info for {api_name}") + api_info_response = self.session.get( + api_info_url, + params=api_info_params, + timeout=self.default_timeout, + verify=False + ) + + if api_info_response.status_code == 200: + api_info_data = api_info_response.json() + if api_info_data.get("success"): + api_info = api_info_data.get("data", {}).get(api_name, {}) + if api_info: + max_version = api_info.get("maxVersion", version) + min_version = api_info.get("minVersion", version) + api_path = api_info.get("path", "entry.cgi") + + # Проверка, поддерживается ли запрошенная версия + if version < min_version: + logger.warning(f"API version {version} for {api_name} is below minimum {min_version}, using {min_version}") + version = min_version + elif version > max_version: + logger.warning(f"API version {version} for {api_name} exceeds maximum {max_version}, using {max_version}") + version = max_version + + logger.debug(f"Using API path: {api_path}, version: {version}") + else: + logger.warning(f"API {api_name} not found in API info, using defaults") + except Exception as e: + logger.warning(f"Error querying API info for {api_name}: {str(e)}, using defaults") + + # Подготовка базовых параметров запроса + base_params = { + "api": api_name, + "version": str(version), + "method": method, + "_sid": self.sid # Используем _sid вместо sid для лучшей совместимости + } + + # Добавление дополнительных параметров, если они заданы + if params: + base_params.update(params) + + url = f"{self.base_url}/{api_path}" + logger.debug(f"API request: {api_name}.{method} v{version} to {url}") + logger.debug(f"Full request params: {base_params}") + + try: + start_time = time.time() + response = self.session.get( + url, + params=base_params, + timeout=self.default_timeout, + verify=False + ) + elapsed_time = time.time() - start_time + logger.debug(f"API request completed in {elapsed_time:.2f}s with status {response.status_code}") + + # Проверка статуса HTTP + if response.status_code != 200: + logger.error(f"HTTP error: {response.status_code} for {api_name}.{method}") + + # Повторная попытка при ошибках соединения + if response.status_code in [500, 502, 503, 504]: + logger.info(f"Server error, retrying request for {api_name}.{method}") + sleep(2) + return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1) + + return None + + try: + data = response.json() + except json.JSONDecodeError: + logger.error(f"Failed to decode JSON response for {api_name}.{method}") + logger.debug(f"Response content: {response.text[:200]}") + + # Повторная попытка при ошибках декодирования + logger.info(f"JSON decode error, retrying request for {api_name}.{method}") + sleep(2) + return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1) + + if data.get("success"): + logger.info(f"API request successful for {api_name}.{method}") + return data.get("data", {}) + else: + error_code = data.get("error", {}).get("code", -1) + error_desc = self._get_error_description(error_code) + logger.error(f"API error for {api_name}.{method}: {error_code} - {error_desc}") + + # Ошибки доступа или прав часто встречаются, но они не критичные + # Например, ошибка 102 означает, что нет прав, но NAS доступен + if error_code in [102, 103, 104, 105]: + logger.warning(f"Permission-related error for {api_name}.{method}: {error_code}") + # Возвращаем пустой словарь вместо None, + # чтобы вызывающий код мог понять, что запрос выполнен + return {} + + # Если ошибка связана с авторизацией и нам разрешено повторить попытку + if error_code in [106, 107, 119] and retry_auth: + logger.info(f"Session error (code {error_code}), creating fresh session...") + self.sid = None # Сбрасываем SID + + # Для ошибки 119 (Session timeout) дадим системе немного времени + if error_code == 119: + logger.info("Session timeout detected, waiting before retry...") + sleep(3) + + if self.login(): + logger.info("Re-authenticated with fresh session, retrying API request...") + # Рекурсивный вызов, но со счетчиком повторов + return self._make_api_request(api_name, method, version, params, False, retry_count + 1) + + # Для некоторых ошибок можно автоматически повторить запрос + if error_code in [408, 429, 500, 502, 503, 504]: + logger.info(f"Temporary error {error_code}, retrying request for {api_name}.{method}") + sleep(2) + return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1) + + return None + + except requests.exceptions.Timeout: + logger.error(f"Request timeout for {api_name}.{method}") + + # Повторная попытка при таймауте + if retry_count < 2: + logger.info(f"Timeout, retrying request for {api_name}.{method}") + sleep(2) + return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1) + + return None + except requests.exceptions.ConnectionError as e: + logger.error(f"Connection error for {api_name}.{method}: {str(e)}") + + # Повторная попытка при ошибке соединения + if retry_count < 2: + logger.info(f"Connection error, retrying request for {api_name}.{method}") + sleep(2) + return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1) + + return None + except Exception as e: + logger.error(f"Unexpected error during {api_name}.{method}: {str(e)}") + return None + + def get_system_status(self) -> Dict[str, Any]: + """Получение статуса системы""" + # Проверяем доступность системы + if not self.is_online(): + logger.info("Device is offline, skipping API request") + return {"status": "offline"} + + # Проверяем, есть ли кэшированный результат + cache_key = "system_status" + current_time = time.time() + if cache_key in self._cache and current_time - self._cache_ttl.get(cache_key, 0) < 60: + logger.debug("Using cached system status") + return self._cache[cache_key] + + # Используем рекомендованный API для получения информации о системе + logger.info(f"Getting system info using {SYNOLOGY_INFO_API} v{SYNOLOGY_API_VERSION}") + + # Для SYNO.DSM.Info метод getinfo, для других обычно используется get или info + if SYNOLOGY_INFO_API == "SYNO.DSM.Info": + method = "getinfo" + else: + method = "get" + + result = self._make_api_request(SYNOLOGY_INFO_API, method, version=SYNOLOGY_API_VERSION) + + if result: + logger.info(f"Successfully retrieved system info using {SYNOLOGY_INFO_API}") + + # Формируем расширенный ответ с дополнительной информацией + system_info = { + "status": "online", + "hostname": result.get("hostname", "unknown"), + "model": result.get("model", "unknown"), + "version": result.get("version", "unknown"), + "uptime": result.get("uptime", 0), + "time": current_time, + "is_online": True, + "api_used": SYNOLOGY_INFO_API + } + + # Сохраняем в кэше + self._cache[cache_key] = system_info + self._cache_ttl[cache_key] = current_time + + return system_info + + # Если основной API не сработал, пробуем резервные варианты + logger.warning(f"Failed to retrieve system info with {SYNOLOGY_INFO_API}, trying fallback APIs") + + # Пробуем резервные API + apis_to_try = [ + {"name": "SYNO.DSM.Info", "method": "getinfo", "version": 2}, + {"name": "SYNO.Core.System", "method": "info", "version": 1}, + {"name": "SYNO.Core.System.Status", "method": "get", "version": 1}, + {"name": "SYNO.Core.System.Info", "method": "get", "version": 1}, + ] + + for api in apis_to_try: + if api["name"] == SYNOLOGY_INFO_API: + continue # Пропускаем уже проверенный API + + logger.info(f"Trying to get system info using {api['name']}.{api['method']} v{api['version']}") + result = self._make_api_request(api["name"], api["method"], version=api["version"]) + + if result: + logger.info(f"Successfully retrieved system info using {api['name']}") + + # Формируем расширенный ответ с дополнительной информацией + system_info = { + "status": "online", + "hostname": result.get("hostname", "unknown"), + "model": result.get("model", "unknown"), + "version": result.get("version", "unknown"), + "uptime": result.get("uptime", 0), + "time": current_time, + "is_online": True, + "api_used": api["name"] + } + + # Сохраняем в кэше + self._cache[cache_key] = system_info + self._cache_ttl[cache_key] = current_time + + return system_info + + # Если все запросы не удались, но система онлайн, возвращаем базовую информацию + logger.warning("Failed to retrieve system info with all API methods") + return { + "status": "error", + "error": "Failed to fetch system information", + "is_online": True, + "time": current_time + } + + def shutdown_system(self) -> bool: + """Выключение системы""" + # Проверяем, включено ли устройство перед попыткой его выключить + if not self.is_online(force_check=True): + logger.info("Synology NAS is already offline, no need to shut down") + return True + + logger.info("Attempting to shutdown Synology NAS...") + + # Попробуем сначала использовать предпочтительный API для управления питанием + logger.info(f"Trying shutdown with {SYNOLOGY_POWER_API} v{SYNOLOGY_API_VERSION}") + + # Для SYNO.Core.Hardware.PowerRecovery используется метод setPowerOnState + # Для других API обычно используется метод shutdown или reboot + if SYNOLOGY_POWER_API == "SYNO.Core.Hardware.PowerRecovery": + # Для этого API нужны специальные параметры + params = {"state": "powerbtn"} # powerbtn имитирует нажатие кнопки питания + result = self._make_api_request(SYNOLOGY_POWER_API, "setPowerOnState", version=SYNOLOGY_API_VERSION, params=params) + else: + # Пробуем стандартный метод + result = self._make_api_request(SYNOLOGY_POWER_API, "shutdown", version=SYNOLOGY_API_VERSION) + + if result is not None: + logger.info(f"Successfully initiated system shutdown using {SYNOLOGY_POWER_API}") + + # Даем системе время начать процесс выключения + logger.info("Waiting for shutdown to initialize...") + sleep(5) + + # Проверяем статус + if not self.is_online(force_check=True): + logger.info("System is now offline. Shutdown confirmed successful.") + return True + else: + logger.info("System still appears to be online, but shutdown may be in progress.") + return True + else: + logger.warning(f"Failed to shutdown using {SYNOLOGY_POWER_API}, trying alternative methods") + + # Если не сработал основной метод, пробуем резервные варианты + # Проверка всех доступных методов API для выключения + apis_to_try = [ + {"name": "SYNO.Core.System", "method": "shutdown", "version": 3}, + {"name": "SYNO.Core.System.Power", "method": "shutdown", "version": 1}, + {"name": "SYNO.DSM.Power", "method": "shutdown", "version": 1}, + {"name": "SYNO.System.Power", "method": "shutdown", "version": 1}, + {"name": "SYNO.DSM.System", "method": "shutdown", "version": 1} + ] + + # Проверяем доступные API + try: + api_info_url = f"{self.base_url}/entry.cgi" + api_info_params = { + "api": "SYNO.API.Info", + "version": "1", + "method": "query", + "query": "SYNO.Core.System.Power,SYNO.DSM.Power,SYNO.Core.System,SYNO.System.Power,SYNO.DSM.System" + } + + logger.debug("Checking available shutdown APIs") + api_info_response = self.session.get( + api_info_url, + params=api_info_params, + timeout=self.default_timeout, + verify=False + ) + + if api_info_response.status_code == 200: + api_info_data = api_info_response.json() + if api_info_data.get("success"): + available_apis = api_info_data.get("data", {}) + logger.debug(f"Available APIs: {list(available_apis.keys())}") + + # Фильтруем только доступные API + filtered_apis = [] + for api in apis_to_try: + if api["name"] in available_apis: + api_info = available_apis[api["name"]] + # Проверка версии API + if api_info.get("minVersion", 1) <= api["version"] <= api_info.get("maxVersion", 1): + filtered_apis.append(api) + logger.debug(f"Adding {api['name']} to available shutdown APIs") + else: + logger.debug(f"API {api['name']} version {api['version']} not supported. Supported: {api_info.get('minVersion')}-{api_info.get('maxVersion')}") + + if filtered_apis: + apis_to_try = filtered_apis + else: + logger.warning("No compatible APIs found, trying all methods as fallback") + else: + logger.warning("Failed to query API info, using default methods") + else: + logger.warning(f"API info request failed with status {api_info_response.status_code}, using default methods") + except Exception as e: + logger.warning(f"Error querying API info: {str(e)}, using default methods") + + # Пробуем все доступные методы по порядку + for api in apis_to_try: + logger.info(f"Trying shutdown with {api['name']}.{api['method']} v{api['version']}") + result = self._make_api_request(api["name"], api["method"], version=api["version"]) + + if result is not None: + logger.info(f"Successfully initiated system shutdown using {api['name']}") + + # Даем системе время начать процесс выключения + logger.info("Waiting for shutdown to initialize...") + sleep(5) + + # Проверяем статус + if not self.is_online(force_check=True): + logger.info("System is now offline. Shutdown confirmed successful.") + return True + else: + logger.info("System still appears to be online, but shutdown may be in progress.") + return True + else: + logger.warning(f"Failed to shutdown using {api['name']}.{api['method']} v{api['version']}") + + # Если ни один метод не сработал, но система стала недоступна + if not self.is_online(force_check=True): + logger.info("System appears to be shutting down despite API errors") + return True + + logger.error("Failed to shutdown system after trying multiple APIs") + return False + + def reboot_system(self) -> bool: + """Перезагрузка системы""" + # Проверяем, включена ли система + if not self.is_online(force_check=True): + logger.error("Cannot reboot: System is offline") + return False + + logger.info("Attempting to reboot Synology NAS...") + + # Попробуем сначала использовать предпочтительный API для управления питанием + logger.info(f"Trying reboot with {SYNOLOGY_POWER_API} v{SYNOLOGY_API_VERSION}") + + # Для SYNO.Core.Hardware.PowerRecovery используется метод setPowerOnState или powerButton + # Для других API обычно используется метод restart или reboot + if SYNOLOGY_POWER_API == "SYNO.Core.Hardware.PowerRecovery": + # Для этого API нужны специальные параметры + params = {"reboot": "true"} # Передаем флаг для перезагрузки + result = self._make_api_request(SYNOLOGY_POWER_API, "setPowerOnState", version=SYNOLOGY_API_VERSION, params=params) + else: + # Пробуем стандартный метод + methods_to_try = ["restart", "reboot"] + result = None + for method in methods_to_try: + result = self._make_api_request(SYNOLOGY_POWER_API, method, version=SYNOLOGY_API_VERSION) + if result is not None: + logger.info(f"Successfully used method {method} for {SYNOLOGY_POWER_API}") + break + + if result is not None: + logger.info(f"Successfully initiated system reboot using {SYNOLOGY_POWER_API}") + + # Даем системе время начать процесс перезагрузки + logger.info("Waiting for reboot to initialize...") + sleep(5) + + # Ждем, пока система станет недоступна (признак перезагрузки) + reboot_started = False + for i in range(12): # Проверяем в течение 60 секунд (12 * 5сек) + if not self.is_online(force_check=True): + logger.info(f"System went offline after {i*5} seconds, reboot in progress") + reboot_started = True + break + logger.debug(f"System still online, waiting... ({i+1}/12)") + sleep(5) + + if reboot_started: + # Ждем, пока система снова станет доступна + logger.info("Waiting for system to come back online...") + return self.wait_for_boot(max_attempts=30, delay=10) + else: + logger.warning("System did not go offline after reboot command, but command was accepted") + # Даже если система не ушла оффлайн, команда могла быть принята + return True + else: + logger.warning(f"Failed to reboot using {SYNOLOGY_POWER_API}, trying alternative methods") + + # Если не сработал основной метод, пробуем резервные варианты + # Проверка всех доступных методов API для перезагрузки + apis_to_try = [ + {"name": "SYNO.Core.System.Power", "method": "restart", "version": 1}, + {"name": "SYNO.DSM.Power", "method": "reboot", "version": 1}, + {"name": "SYNO.Core.System", "method": "reboot", "version": 3}, + {"name": "SYNO.System.Power", "method": "reboot", "version": 1}, + {"name": "SYNO.DSM.System", "method": "reboot", "version": 1}, + {"name": "SYNO.Core.Hardware.NeedReboot", "method": "reboot", "version": 1} + ] + + # Получаем список доступных API + try: + api_info_url = f"{self.base_url}/entry.cgi" + api_info_params = { + "api": "SYNO.API.Info", + "version": "1", + "method": "query", + "query": "SYNO.Core.System.Power,SYNO.DSM.Power,SYNO.Core.System,SYNO.System.Power,SYNO.DSM.System,SYNO.Core.Hardware.NeedReboot" + } + + logger.debug("Checking available reboot APIs") + api_info_response = self.session.get( + api_info_url, + params=api_info_params, + timeout=self.default_timeout, + verify=False + ) + + if api_info_response.status_code == 200: + api_info_data = api_info_response.json() + if api_info_data.get("success"): + available_apis = api_info_data.get("data", {}) + logger.debug(f"Available APIs: {list(available_apis.keys())}") + + # Фильтруем только доступные API + filtered_apis = [] + for api in apis_to_try: + if api["name"] in available_apis: + api_info = available_apis[api["name"]] + # Проверка версии API + if api_info.get("minVersion", 1) <= api["version"] <= api_info.get("maxVersion", 1): + filtered_apis.append(api) + logger.debug(f"Adding {api['name']} to available reboot APIs") + else: + logger.debug(f"API {api['name']} version {api['version']} not supported. Supported: {api_info.get('minVersion')}-{api_info.get('maxVersion')}") + + if filtered_apis: + apis_to_try = filtered_apis + else: + logger.warning("No compatible APIs found, trying all methods as fallback") + else: + logger.warning("Failed to query API info, using default methods") + else: + logger.warning(f"API info request failed with status {api_info_response.status_code}, using default methods") + except Exception as e: + logger.warning(f"Error querying API info: {str(e)}, using default methods") + + # Пробуем все доступные методы по порядку + for api in apis_to_try: + logger.info(f"Trying reboot with {api['name']}.{api['method']} v{api['version']}") + result = self._make_api_request(api["name"], api["method"], version=api["version"]) + + if result is not None: + logger.info(f"Successfully initiated system reboot using {api['name']}") + + # Даем системе время начать процесс перезагрузки + logger.info("Waiting for reboot to initialize...") + sleep(5) + + # Ждем, пока система станет недоступна (признак перезагрузки) + reboot_started = False + for i in range(12): # Проверяем в течение 60 секунд (12 * 5сек) + if not self.is_online(force_check=True): + logger.info(f"System went offline after {i*5} seconds, reboot in progress") + reboot_started = True + break + logger.debug(f"System still online, waiting... ({i+1}/12)") + sleep(5) + + if reboot_started: + # Ждем, пока система снова станет доступна + logger.info("Waiting for system to come back online...") + return self.wait_for_boot(max_attempts=30, delay=10) + else: + logger.warning("System did not go offline after reboot command, but command was accepted") + # Даже если система не ушла оффлайн, команда могла быть принята + return True + else: + logger.warning(f"Failed to reboot using {api['name']}.{api['method']} v{api['version']}") + + logger.error("Failed to reboot system after trying multiple APIs") + return False + + def _get_error_description(self, error_code: int) -> str: + """Получение описания ошибки по коду""" + error_descriptions = { + 100: "Unknown error", + 101: "Invalid parameter", + 102: "API does not exist", + 103: "Method does not exist", + 104: "Version does not support", + 105: "Permission denied", + 106: "Session timeout", + 107: "Session interrupted by duplicate login", + 400: "Invalid credentials", + 401: "Account disabled", + 402: "Permission denied", + 403: "2FA required", + 404: "Failed to authenticate with 2FA" + } + return error_descriptions.get(error_code, "Unknown error code") + + def _check_tcp_connection(self) -> bool: + """Проверка базового TCP-соединения с Synology NAS""" + try: + socket_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + socket_obj.settimeout(SYNOLOGY_TIMEOUT) + result = socket_obj.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT)) + socket_obj.close() + + return result == 0 + except socket.error as e: + logger.error(f"Socket error during connection check: {str(e)}") + return False + except Exception as e: + logger.error(f"Unexpected error during connection check: {str(e)}") + return False + + def is_online(self, force_check=False) -> bool: + """Проверка онлайн-статуса Synology NAS""" + # Используем кэшированное значение, если доступно и не устарело + current_time = time.time() + if not force_check and (current_time - self._last_online_check) < self._online_check_interval: + logger.debug(f"Using cached online status: {self._last_online_status}") + return self._last_online_status + + logger.info("Checking if NAS is online...") + + # Проверяем TCP-соединение + online_status = self._check_tcp_connection() + logger.info(f"Detected Synology NAS online status: {online_status}") + + # Если TCP-соединение успешно и у нас есть действующий SID, + # попробуем более детальную проверку через API + if online_status and self.sid: + logger.info("Trying to fetch more detailed online status through API...") + + # Пробуем разные API для проверки онлайн-статуса + api_checks = [ + {"api": "SYNO.DSM.Info", "version": "2", "method": "getinfo"}, + {"api": "SYNO.Core.System", "version": "1", "method": "info"}, + {"api": "SYNO.Core.System.Status", "version": "1", "method": "get"} + ] + + api_success = False + for api_check in api_checks: + try: + url = f"{self.base_url}/entry.cgi" + params = { + "api": api_check["api"], + "version": api_check["version"], + "method": api_check["method"], + "sid": self.sid + } + + logger.debug(f"Trying online status check with {api_check['api']}") + response = self.session.get( + url, + params=params, + timeout=self.default_timeout, + verify=False + ) + + if response.status_code == 200: + data = response.json() + if data.get("success"): + logger.info(f"API request successful for {api_check['api']}") + logger.info("Synology NAS is online with API access") + api_success = True + break + else: + error_code = data.get("error", {}).get("code", -1) + logger.warning(f"API response indicates an error: {error_code}, but NAS is reachable") + else: + logger.warning(f"API returned status code {response.status_code}") + except Exception as e: + logger.warning(f"API check failed for {api_check['api']}, but TCP connection succeeded: {str(e)}") + + if not api_success: + logger.warning("All API checks failed, but TCP connection is successful") + + # Обновляем кэшированное значение + self._last_online_check = current_time + self._last_online_status = online_status + + return online_status + + def wake_on_lan(self) -> bool: + """Отправка Wake-on-LAN пакета для включения Synology NAS""" + if not SYNOLOGY_MAC: + logger.error("MAC address not configured") + return False + + try: + # Преобразование MAC-адреса в байты + mac_address = SYNOLOGY_MAC.replace(':', '').replace('-', '') + if len(mac_address) != 12: + logger.error(f"Invalid MAC address format: {SYNOLOGY_MAC}") + return False + + try: + mac_bytes = bytes.fromhex(mac_address) + except ValueError as e: + logger.error(f"Failed to parse MAC address: {str(e)}") + logger.error(f"MAC address must contain only hex characters: {SYNOLOGY_MAC}") + return False + + # Создание Magic Packet + magic_packet = b'\xff' * 6 + mac_bytes * 16 + + # Отправка пакета на конкретный адрес + try: + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) + sock.sendto(magic_packet, (SYNOLOGY_HOST, WOL_PORT)) + sock.close() + logger.info(f"Wake-on-LAN packet sent to {SYNOLOGY_MAC} at {SYNOLOGY_HOST}:{WOL_PORT}") + except Exception as e: + logger.error(f"Error sending directed WoL packet: {str(e)}") + return False + + # Для надежности отправляем также широковещательный пакет + try: + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) + + # Используем стандартный широковещательный адрес + broadcast_addr = "255.255.255.255" + sock.sendto(magic_packet, (broadcast_addr, WOL_PORT)) + sock.close() + logger.info(f"Wake-on-LAN broadcast packet sent to {broadcast_addr}:{WOL_PORT}") + except Exception as e: + logger.warning(f"Error sending broadcast WoL packet (non-critical): {str(e)}") + # Не считаем ошибкой, т.к. основной пакет уже отправлен + + return True + + except Exception as e: + logger.error(f"Unexpected error in wake_on_lan: {str(e)}") + return False + + def wait_for_boot(self, max_attempts: int = 30, delay: int = 10) -> bool: + """Ожидание загрузки Synology NAS""" + logger.info(f"Waiting for Synology NAS to boot (timeout: {max_attempts * delay}s)...") + + for attempt in range(max_attempts): + # Принудительно проверяем статус без использования кэша + if self.is_online(force_check=True): + logger.info(f"Synology NAS is online after {attempt + 1} attempts ({(attempt + 1) * delay}s)") + + # Проверяем, что не только сеть доступна, но и API загрузился + api_ready = False + logger.info("Waiting for API services to initialize...") + + for api_check in range(5): # Даем еще до 50 секунд для загрузки API + if self.sid or self.login(): + api_ready = True + logger.info(f"API services are ready after {api_check + 1} attempts") + break + logger.debug(f"API not ready yet, waiting... ({api_check + 1}/5)") + sleep(10) + + if not api_ready: + logger.warning("System is online but API services may not be fully initialized") + + # Дадим дополнительное время для полной загрузки всех сервисов + sleep(delay) + return True + + sleep(delay) + logger.info(f"Waiting for boot... Attempt {attempt + 1}/{max_attempts}") + + logger.error(f"Synology NAS didn't come online after {max_attempts * delay} seconds") + return False + + def power_on(self) -> bool: + """Включение Synology NAS""" + # Принудительная проверка статуса + if self.is_online(force_check=True): + logger.info("Synology NAS is already online") + return True + + logger.info("Powering on Synology NAS via Wake-on-LAN...") + + # Проверяем, настроен ли MAC-адрес + if not SYNOLOGY_MAC: + logger.error("Cannot power on: MAC address not configured in settings") + return False + + # Пробуем отправить несколько WoL пакетов для надежности + success = False + for attempt in range(3): + logger.debug(f"Sending WoL packet, attempt {attempt + 1}/3") + if self.wake_on_lan(): + success = True + break + sleep(1) + + if not success: + logger.error("Failed to send Wake-on-LAN packets") + return False + + # Ожидание загрузки + logger.info("WoL packets sent successfully, waiting for system to boot...") + boot_result = self.wait_for_boot(max_attempts=30, delay=10) + + if boot_result: + # Проверяем доступность API после загрузки + system_status = self.get_system_status() + if system_status.get("status") == "online": + logger.info("System booted successfully with API access") + return True + else: + logger.warning("System appears to be online but API may not be fully ready") + return True + else: + logger.error("System did not come online after WoL") + return False + + def power_off(self) -> bool: + """Выключение Synology NAS""" + if not self.is_online(force_check=True): + logger.info("Synology NAS is already offline") + return True + + logger.info("Powering off Synology NAS...") + result = self.shutdown_system() + + if result: + # Дополнительная проверка, что система действительно выключилась + logger.info("Verifying system is offline...") + for attempt in range(12): # Проверяем в течение 2 минут (12 * 10 сек) + if not self.is_online(force_check=True): + logger.info(f"System confirmed offline after {attempt * 10} seconds") + return True + logger.debug(f"System still shutting down, checking again... ({attempt + 1}/12)") + sleep(10) + + logger.warning("System still appears to be online after shutdown command") + return False + else: + logger.error("Failed to initiate shutdown") + return False + + # Заглушки для расширенных методов + def get_shared_folders(self) -> List[Dict[str, Any]]: + """Получение списка общих папок""" + logger.info("Getting list of shared folders") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for shared folders request") + return [] + + try: + # Запрашиваем список общих папок через FileStation API + result = self._make_api_request("SYNO.FileStation.List", "list_share", version=2) + + if not result: + # Пробуем альтернативный API + logger.info("Trying alternative API for shared folders") + alt_result = self._make_api_request("SYNO.FileStation.List", "list_share", version=1) + if alt_result: + return alt_result.get("shares", []) + return [] + + return result.get("shares", []) + + except Exception as e: + logger.error(f"Error getting shared folders: {str(e)}") + return [] + + def get_system_load(self) -> Dict[str, Any]: + """Получение информации о загрузке системы""" + logger.info("Getting system load information") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for system load request") + return {} + + try: + # Запрашиваем информацию о загрузке системы + result = self._make_api_request("SYNO.Core.System.Utilization", "get", version=1) + + if not result: + # Пробуем альтернативный API + logger.info("Trying alternative API for system load") + alt_result = self._make_api_request("SYNO.Core.System", "info", version=3) + + if not alt_result: + return {} + + # Формируем из частичных данных + return { + "cpu_load": alt_result.get("cpu_usage", 0), + "memory": { + "total": alt_result.get("memory_size", 0), + "used": alt_result.get("memory_usage", 0), + "usage_percent": alt_result.get("memory_usage_percent", 0) + } + } + + # Формируем структурированный результат + return { + "cpu_load": result.get("cpu", {}).get("user", 0) + result.get("cpu", {}).get("system", 0), + "memory": result.get("memory", {}), + "network": result.get("network", {}) + } + + except Exception as e: + logger.error(f"Error getting system load: {str(e)}") + return {} + + def get_storage_status(self) -> Dict[str, Any]: + """Получение подробной информации о хранилище""" + logger.info("Getting storage status information") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for storage status request") + return {"error": "authentication_failed"} + + try: + # Пробуем получить информацию о хранилище через SYNO.Storage.CGI.Storage API + result = self._make_api_request("SYNO.Storage.CGI.Storage", "load_info", version=1) + + if not result: + # Пробуем альтернативный API + logger.info("Trying alternative API for storage info") + alt_result = self._make_api_request("SYNO.Core.Storage", "info", version=1) + + if not alt_result: + # Пробуем еще один альтернативный API + logger.info("Trying SYNO.Core.System API for storage info") + sys_result = self._make_api_request("SYNO.Core.System", "info", version=3) + + if not sys_result: + return { + "volumes": [], + "disks": [], + "total_size": 0, + "total_used": 0, + "error": "no_data" + } + + # Извлекаем базовую информацию о хранилище из системной информации + return { + "volumes": [], + "disks": [], + "total_size": sys_result.get("hdd_total_size", 0) * 1024**2, # МБ в байты + "total_used": sys_result.get("hdd_total_size", 0) * sys_result.get("hdd_usage", 0) / 100 * 1024**2, + } + + # Обрабатываем данные из альтернативного API + volumes = alt_result.get("volumes", []) + disks = alt_result.get("disks", []) + + else: + # Обрабатываем данные из основного API + volumes = result.get("volumes", []) + disks = result.get("disks", []) + + # Рассчитываем общие размеры + total_size = 0 + total_used = 0 + + for volume in volumes: + volume_size = volume.get("size", {}).get("total", 0) + volume_used = volume.get("size", {}).get("used", 0) + + total_size += volume_size + total_used += volume_used + + return { + "volumes": volumes, + "disks": disks, + "total_size": total_size, + "total_used": total_used + } + + except Exception as e: + logger.error(f"Error in get_storage_status: {str(e)}") + return {"error": str(e)} + + def get_security_status(self) -> Dict[str, Any]: + """Получение информации о состоянии безопасности (заглушка)""" + logger.warning("Function get_security_status() is not implemented yet") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for security status request") + return {"success": False, "error": "authentication_failed"} + + try: + # Здесь будет реализация после добавления соответствующего API + url = f"{self.base_url}/entry.cgi" + params = { + "api": "SYNO.Core.SecurityScan.Status", + "version": "1", + "method": "get", + "sid": self.sid + } + + # В текущей версии просто возвращаем заглушку + return { + "success": False, + "status": "not_implemented", + "last_check": None, + "is_secure": False, + "error": "not_implemented" + } + except Exception as e: + logger.error(f"Error in get_security_status: {str(e)}") + return {"success": False, "error": str(e)} diff --git a/.history/src/api/synology_20250830090936.py b/.history/src/api/synology_20250830090936.py new file mode 100644 index 0000000..fd6bba7 --- /dev/null +++ b/.history/src/api/synology_20250830090936.py @@ -0,0 +1,1364 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Модуль для взаимодействия с API Synology NAS +""" + +import requests +from requests.adapters import HTTPAdapter +import json +import logging +import time +import urllib3 +from urllib3.util import Retry +from typing import Dict, Any, Optional, List +import socket +import struct +from time import sleep + +from src.config.config import ( + SYNOLOGY_HOST, + SYNOLOGY_PORT, + SYNOLOGY_USERNAME, + SYNOLOGY_PASSWORD, + SYNOLOGY_SECURE, + SYNOLOGY_TIMEOUT, + SYNOLOGY_MAC, + WOL_PORT, + SYNOLOGY_API_VERSION, + SYNOLOGY_POWER_API, + SYNOLOGY_INFO_API +) +from src.api.api_discovery import discover_available_apis, find_compatible_api + +# Отключение предупреждений о небезопасных SSL-соединениях +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + +logger = logging.getLogger(__name__) + +class SynologyAPI: + """Класс для взаимодействия с API Synology NAS""" + + def __init__(self): + """Инициализация класса SynologyAPI""" + logger.info("Creating API with auto-retry and connection pool") + logger.debug(f"Connection details: Host={SYNOLOGY_HOST}, Port={SYNOLOGY_PORT}, Secure={SYNOLOGY_SECURE}") + + self.protocol = "https" if SYNOLOGY_SECURE else "http" + self.base_url = f"{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi" + self.sid = None + self.session = requests.Session() + + # Настройка SSL + if self.protocol == "https": + logger.debug("SSL enabled, disabling certificate verification for internal network") + self.session.verify = False # Отключаем проверку SSL для внутренней сети + + # Добавляем пользовательские заголовки для улучшения совместимости с API + custom_headers = { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36', + 'Accept': 'application/json, text/javascript, */*; q=0.01', + 'Accept-Language': 'ru-RU,ru;q=0.9,en-US;q=0.8,en;q=0.7', + 'X-Requested-With': 'XMLHttpRequest', + 'Connection': 'keep-alive', + 'Referer': f'{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/' + } + self.session.headers.update(custom_headers) + logger.debug("Added browser-like headers for API compatibility") + + # Добавляем повторные попытки для HTTP-запросов + retry_strategy = Retry( + total=5, # Увеличиваем количество попыток + status_forcelist=[429, 500, 502, 503, 504, 404], + allowed_methods=["GET", "POST"], + backoff_factor=1.5, # Увеличиваем задержку между попытками + respect_retry_after_header=True + ) + adapter = HTTPAdapter( + max_retries=retry_strategy, + pool_connections=3, + pool_maxsize=10 + ) + self.session.mount("http://", adapter) + self.session.mount("https://", adapter) + + # Таймауты будут указаны в запросах + self.default_timeout = (SYNOLOGY_TIMEOUT, SYNOLOGY_TIMEOUT*2) + logger.debug(f"Setting default request timeout: {self.default_timeout}") + + # Кэш для хранения результатов запросов + self._cache = {} + self._cache_ttl = {} + self._last_online_check = 0 + self._last_online_status = False + self._online_check_interval = 30 # Интервал для кэширования проверок онлайн-статуса + + # Время последней успешной аутентификации и срок действия сессии + self._last_auth_time = 0 + self._auth_expiry = 3600 # По умолчанию 1 час + + # Информация о доступных API + self._available_apis = {} + self._api_info_ttl = 0 + + # Инициализируем API version resolver для автоматического определения совместимых API + self.api_resolver = None # Будет создан при необходимости + + def login(self) -> bool: + """Авторизация в API Synology NAS""" + # Сбрасываем SID для новой сессии + self.sid = None + + logger.info("Attempting to authenticate with Synology NAS...") + logger.debug(f"Base URL: {self.base_url}") + + # Проверяем доступность NAS перед запросом с помощью прямой TCP-проверки + # Избегаем вызова is_online(), чтобы не создавать рекурсию + online_status = self._check_tcp_connection() + if not online_status: + logger.error("Cannot login: Synology NAS is not reachable") + return False + + # Пробуем различные версии API для аутентификации + # Начинаем с версии 3, которая показала лучшую совместимость в тестах + auth_versions_to_try = [3, 2, 1, 6] # Пробуем сначала более стабильные версии + + for auth_version in auth_versions_to_try: + try: + # Определяем путь к API аутентификации + auth_path = "entry.cgi" # Используем entry.cgi вместо auth.cgi по умолчанию + + # Проверка информации API для определения доступных версий API + api_info_url = f"{self.base_url}/entry.cgi" + api_info_params = { + "api": "SYNO.API.Info", + "version": "1", + "method": "query", + "query": "SYNO.API.Auth" + } + + logger.debug(f"Querying API info for auth version {auth_version}") + try: + api_info_response = self.session.get( + api_info_url, + params=api_info_params, + timeout=self.default_timeout, + verify=False + ) + + if api_info_response.status_code == 200: + api_info_data = api_info_response.json() + if api_info_data.get("success"): + auth_info = api_info_data.get("data", {}).get("SYNO.API.Auth", {}) + max_version = auth_info.get("maxVersion", 6) + min_version = auth_info.get("minVersion", 1) + auth_path = auth_info.get("path", "entry.cgi") + logger.debug(f"Auth API versions: min={min_version}, max={max_version}, path={auth_path}") + + # Проверяем поддержку текущей версии + if auth_version < min_version or auth_version > max_version: + logger.warning(f"Auth API version {auth_version} not supported (min={min_version}, max={max_version}), trying next version") + continue + else: + logger.warning("Failed to query API info, using default auth path") + else: + logger.warning(f"API info request failed with status {api_info_response.status_code}, using default auth path") + except Exception as e: + logger.warning(f"Error querying API info: {str(e)}, using default auth path") + + # Основной запрос авторизации + url = f"{self.base_url}/{auth_path}" + params = { + "api": "SYNO.API.Auth", + "version": str(auth_version), + "method": "login", + "account": SYNOLOGY_USERNAME, + "passwd": SYNOLOGY_PASSWORD, + "session": "SynologyPowerControlBot", + "format": "cookie" + } + + # Для версии 6+ используем немного другой формат + if auth_version >= 6: + params["enable_syno_token"] = "yes" + + logger.debug(f"Sending auth request to {url} with API version {auth_version}") + start_time = time.time() + response = self.session.get( + url, + params=params, + timeout=self.default_timeout, + verify=False + ) + elapsed_time = time.time() - start_time + logger.debug(f"Auth request completed in {elapsed_time:.2f}s with status code {response.status_code}") + + # Проверка статуса HTTP + if response.status_code != 200: + logger.error(f"HTTP error: {response.status_code}") + logger.debug(f"Response content: {response.text[:200]}") + continue # Пробуем следующую версию + + try: + data = response.json() + except json.JSONDecodeError: + logger.error("Failed to decode JSON response") + logger.debug(f"Response content: {response.text[:200]}") + continue # Пробуем следующую версию + + if data.get("success"): + self.sid = data.get("data", {}).get("sid") + self._last_auth_time = time.time() + logger.info(f"Successfully logged in to Synology NAS using API version {auth_version}") + logger.debug(f"Session ID obtained: {self.sid[:5] if self.sid else 'None'}...") + + # Получаем и сохраняем токен SYNO, если он есть + syno_token = data.get("data", {}).get("synotoken") + if syno_token: + self.session.headers.update({'X-SYNO-TOKEN': syno_token}) + logger.debug("Added X-SYNO-TOKEN header for improved API compatibility") + + # Также добавляем SID в cookies для улучшения совместимости + self.session.cookies.update({ + 'id': self.sid, + 'sid': self.sid + }) + logger.debug("Added SID to session cookies for improved compatibility") + + # Проверка валидности полученной сессии с помощью простого запроса + # Будем использовать SYNO.API.Info без проверки сложных методов + + # Даем системе немного времени для инициализации сессии + time.sleep(0.5) + + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to log in with version {auth_version}: Error code {error_code}") + + # Если ошибка связана с версией API, пробуем следующую версию + if error_code in [104, 105]: + logger.warning(f"Auth version {auth_version} not supported, trying next version") + continue + + # Дополнительная диагностика + if error_code == 400: + logger.error("Authentication error: Invalid credentials") + elif error_code == 401: + logger.error("Authentication error: Account disabled") + elif error_code == 402: + logger.error("Authentication error: Permission denied") + elif error_code == 403: + logger.error("Authentication error: 2-factor authentication required") + elif error_code == 404: + logger.error("Authentication error: Failed to authenticate with 2-factor authentication") + + # Если ошибка связана с неверными учетными данными, нет смысла пробовать другие версии API + if error_code in [400, 401, 402, 403, 404]: + return False + + except requests.exceptions.Timeout: + logger.error(f"Connection timeout during auth with version {auth_version}") + continue # Пробуем следующую версию + except requests.exceptions.ConnectionError as e: + logger.error(f"Connection error during auth with version {auth_version}: {str(e)}") + continue # Пробуем следующую версию + except requests.RequestException as e: + logger.error(f"Request error during auth with version {auth_version}: {str(e)}") + continue # Пробуем следующую версию + except Exception as e: + logger.error(f"Unexpected error during login with version {auth_version}: {str(e)}", exc_info=True) + continue # Пробуем следующую версию + + # Если все версии не сработали + logger.error("Failed to authenticate with any API version") + return False + + def _validate_session(self) -> bool: + """Проверяет валидность сессии после авторизации""" + if not self.sid: + return False + + # Попробуем сделать простой запрос для проверки сессии + test_apis = [ + {"api": "SYNO.Core.System", "method": "info", "version": 1}, + {"api": "SYNO.DSM.Info", "method": "getinfo", "version": 1} + ] + + for test_api in test_apis: + try: + url = f"{self.base_url}/entry.cgi" + params = { + "api": test_api["api"], + "version": str(test_api["version"]), + "method": test_api["method"], + "_sid": self.sid + } + + response = self.session.get(url, params=params, timeout=self.default_timeout, verify=False) + + if response.status_code == 200: + data = response.json() + if data.get("success"): + logger.debug(f"Session validation successful using {test_api['api']}") + return True + else: + error_code = data.get("error", {}).get("code", -1) + if error_code != 119: # Не сессия истекла + logger.debug(f"Session validation with {test_api['api']} returned error code {error_code}") + return True # Считаем сессию валидной, если ошибка не связана с истечением сессии + except Exception as e: + logger.warning(f"Error during session validation with {test_api['api']}: {str(e)}") + + logger.warning("Session validation failed with all test APIs") + return False + + def logout(self) -> bool: + """Выход из API Synology NAS""" + if not self.sid: + return True + + try: + url = f"{self.base_url}/auth.cgi" + params = { + "api": "SYNO.API.Auth", + "version": "1", + "method": "logout", + "session": "SynologyPowerControlBot", + "_sid": self.sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + self.sid = None + logger.info("Successfully logged out from Synology NAS") + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to log out from Synology NAS: Error code {error_code}") + return False + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + return False + + def _make_api_request(self, api_name: str, method: str, version: int = 1, + params: Optional[Dict[str, Any]] = None, retry_auth: bool = True, retry_count: int = 0) -> Optional[Dict[str, Any]]: + """Обобщенный метод для выполнения API запросов с обработкой ошибок""" + # Ограничение на количество повторных попыток + if retry_count >= 3: + logger.error(f"Too many retries for {api_name}.{method}, giving up") + return None + + # Проверка наличия авторизации + if not self.sid and not self.login(): + logger.error(f"Not authenticated for API request: {api_name}.{method}") + return None + + # Проверка информации API для определения пути и поддерживаемой версии + api_info_url = f"{self.base_url}/entry.cgi" + api_info_params = { + "api": "SYNO.API.Info", + "version": "1", + "method": "query", + "query": api_name + } + + api_path = "entry.cgi" + try: + logger.debug(f"Querying API info for {api_name}") + api_info_response = self.session.get( + api_info_url, + params=api_info_params, + timeout=self.default_timeout, + verify=False + ) + + if api_info_response.status_code == 200: + api_info_data = api_info_response.json() + if api_info_data.get("success"): + api_info = api_info_data.get("data", {}).get(api_name, {}) + if api_info: + max_version = api_info.get("maxVersion", version) + min_version = api_info.get("minVersion", version) + api_path = api_info.get("path", "entry.cgi") + + # Проверка, поддерживается ли запрошенная версия + if version < min_version: + logger.warning(f"API version {version} for {api_name} is below minimum {min_version}, using {min_version}") + version = min_version + elif version > max_version: + logger.warning(f"API version {version} for {api_name} exceeds maximum {max_version}, using {max_version}") + version = max_version + + logger.debug(f"Using API path: {api_path}, version: {version}") + else: + logger.warning(f"API {api_name} not found in API info, using defaults") + except Exception as e: + logger.warning(f"Error querying API info for {api_name}: {str(e)}, using defaults") + + # Подготовка базовых параметров запроса + base_params = { + "api": api_name, + "version": str(version), + "method": method, + "_sid": self.sid # Используем _sid вместо sid для лучшей совместимости + } + + # Добавление дополнительных параметров, если они заданы + if params: + base_params.update(params) + + url = f"{self.base_url}/{api_path}" + logger.debug(f"API request: {api_name}.{method} v{version} to {url}") + logger.debug(f"Full request params: {base_params}") + + try: + start_time = time.time() + response = self.session.get( + url, + params=base_params, + timeout=self.default_timeout, + verify=False + ) + elapsed_time = time.time() - start_time + logger.debug(f"API request completed in {elapsed_time:.2f}s with status {response.status_code}") + + # Проверка статуса HTTP + if response.status_code != 200: + logger.error(f"HTTP error: {response.status_code} for {api_name}.{method}") + + # Повторная попытка при ошибках соединения + if response.status_code in [500, 502, 503, 504]: + logger.info(f"Server error, retrying request for {api_name}.{method}") + sleep(2) + return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1) + + return None + + try: + data = response.json() + except json.JSONDecodeError: + logger.error(f"Failed to decode JSON response for {api_name}.{method}") + logger.debug(f"Response content: {response.text[:200]}") + + # Повторная попытка при ошибках декодирования + logger.info(f"JSON decode error, retrying request for {api_name}.{method}") + sleep(2) + return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1) + + if data.get("success"): + logger.info(f"API request successful for {api_name}.{method}") + return data.get("data", {}) + else: + error_code = data.get("error", {}).get("code", -1) + error_desc = self._get_error_description(error_code) + logger.error(f"API error for {api_name}.{method}: {error_code} - {error_desc}") + + # Ошибки доступа или прав часто встречаются, но они не критичные + # Например, ошибка 102 означает, что нет прав, но NAS доступен + if error_code in [102, 103, 104, 105]: + logger.warning(f"Permission-related error for {api_name}.{method}: {error_code}") + # Возвращаем пустой словарь вместо None, + # чтобы вызывающий код мог понять, что запрос выполнен + return {} + + # Если ошибка связана с авторизацией и нам разрешено повторить попытку + if error_code in [106, 107, 119] and retry_auth: + logger.info(f"Session error (code {error_code}), creating fresh session...") + self.sid = None # Сбрасываем SID + + # Для ошибки 119 (Session timeout) дадим системе немного времени + if error_code == 119: + logger.info("Session timeout detected, waiting before retry...") + sleep(3) + + if self.login(): + logger.info("Re-authenticated with fresh session, retrying API request...") + # Рекурсивный вызов, но со счетчиком повторов + return self._make_api_request(api_name, method, version, params, False, retry_count + 1) + + # Для некоторых ошибок можно автоматически повторить запрос + if error_code in [408, 429, 500, 502, 503, 504]: + logger.info(f"Temporary error {error_code}, retrying request for {api_name}.{method}") + sleep(2) + return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1) + + return None + + except requests.exceptions.Timeout: + logger.error(f"Request timeout for {api_name}.{method}") + + # Повторная попытка при таймауте + if retry_count < 2: + logger.info(f"Timeout, retrying request for {api_name}.{method}") + sleep(2) + return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1) + + return None + except requests.exceptions.ConnectionError as e: + logger.error(f"Connection error for {api_name}.{method}: {str(e)}") + + # Повторная попытка при ошибке соединения + if retry_count < 2: + logger.info(f"Connection error, retrying request for {api_name}.{method}") + sleep(2) + return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1) + + return None + except Exception as e: + logger.error(f"Unexpected error during {api_name}.{method}: {str(e)}") + return None + + def get_system_status(self) -> Dict[str, Any]: + """Получение статуса системы""" + # Проверяем доступность системы + if not self.is_online(): + logger.info("Device is offline, skipping API request") + return {"status": "offline"} + + # Проверяем, есть ли кэшированный результат + cache_key = "system_status" + current_time = time.time() + if cache_key in self._cache and current_time - self._cache_ttl.get(cache_key, 0) < 60: + logger.debug("Using cached system status") + return self._cache[cache_key] + + # Используем рекомендованный API для получения информации о системе + logger.info(f"Getting system info using {SYNOLOGY_INFO_API} v{SYNOLOGY_API_VERSION}") + + # Для SYNO.DSM.Info метод getinfo, для других обычно используется get или info + if SYNOLOGY_INFO_API == "SYNO.DSM.Info": + method = "getinfo" + else: + method = "get" + + result = self._make_api_request(SYNOLOGY_INFO_API, method, version=SYNOLOGY_API_VERSION) + + if result: + logger.info(f"Successfully retrieved system info using {SYNOLOGY_INFO_API}") + + # Формируем расширенный ответ с дополнительной информацией + system_info = { + "status": "online", + "hostname": result.get("hostname", "unknown"), + "model": result.get("model", "unknown"), + "version": result.get("version", "unknown"), + "uptime": result.get("uptime", 0), + "time": current_time, + "is_online": True, + "api_used": SYNOLOGY_INFO_API + } + + # Сохраняем в кэше + self._cache[cache_key] = system_info + self._cache_ttl[cache_key] = current_time + + return system_info + + # Если основной API не сработал, пробуем резервные варианты + logger.warning(f"Failed to retrieve system info with {SYNOLOGY_INFO_API}, trying fallback APIs") + + # Пробуем резервные API + apis_to_try = [ + {"name": "SYNO.DSM.Info", "method": "getinfo", "version": 2}, + {"name": "SYNO.Core.System", "method": "info", "version": 1}, + {"name": "SYNO.Core.System.Status", "method": "get", "version": 1}, + {"name": "SYNO.Core.System.Info", "method": "get", "version": 1}, + ] + + for api in apis_to_try: + if api["name"] == SYNOLOGY_INFO_API: + continue # Пропускаем уже проверенный API + + logger.info(f"Trying to get system info using {api['name']}.{api['method']} v{api['version']}") + result = self._make_api_request(api["name"], api["method"], version=api["version"]) + + if result: + logger.info(f"Successfully retrieved system info using {api['name']}") + + # Формируем расширенный ответ с дополнительной информацией + system_info = { + "status": "online", + "hostname": result.get("hostname", "unknown"), + "model": result.get("model", "unknown"), + "version": result.get("version", "unknown"), + "uptime": result.get("uptime", 0), + "time": current_time, + "is_online": True, + "api_used": api["name"] + } + + # Сохраняем в кэше + self._cache[cache_key] = system_info + self._cache_ttl[cache_key] = current_time + + return system_info + + # Если все запросы не удались, но система онлайн, возвращаем базовую информацию + logger.warning("Failed to retrieve system info with all API methods") + return { + "status": "error", + "error": "Failed to fetch system information", + "is_online": True, + "time": current_time + } + + def shutdown_system(self) -> bool: + """Выключение системы""" + # Проверяем, включено ли устройство перед попыткой его выключить + if not self.is_online(force_check=True): + logger.info("Synology NAS is already offline, no need to shut down") + return True + + logger.info("Attempting to shutdown Synology NAS...") + + # Попробуем сначала использовать предпочтительный API для управления питанием + logger.info(f"Trying shutdown with {SYNOLOGY_POWER_API} v{SYNOLOGY_API_VERSION}") + + # Для SYNO.Core.Hardware.PowerRecovery используется метод setPowerOnState + # Для других API обычно используется метод shutdown или reboot + if SYNOLOGY_POWER_API == "SYNO.Core.Hardware.PowerRecovery": + # Для этого API нужны специальные параметры + params = {"state": "powerbtn"} # powerbtn имитирует нажатие кнопки питания + result = self._make_api_request(SYNOLOGY_POWER_API, "setPowerOnState", version=SYNOLOGY_API_VERSION, params=params) + else: + # Пробуем стандартный метод + result = self._make_api_request(SYNOLOGY_POWER_API, "shutdown", version=SYNOLOGY_API_VERSION) + + if result is not None: + logger.info(f"Successfully initiated system shutdown using {SYNOLOGY_POWER_API}") + + # Даем системе время начать процесс выключения + logger.info("Waiting for shutdown to initialize...") + sleep(5) + + # Проверяем статус + if not self.is_online(force_check=True): + logger.info("System is now offline. Shutdown confirmed successful.") + return True + else: + logger.info("System still appears to be online, but shutdown may be in progress.") + return True + else: + logger.warning(f"Failed to shutdown using {SYNOLOGY_POWER_API}, trying alternative methods") + + # Если не сработал основной метод, пробуем резервные варианты + # Проверка всех доступных методов API для выключения + apis_to_try = [ + {"name": "SYNO.Core.System", "method": "shutdown", "version": 3}, + {"name": "SYNO.Core.System.Power", "method": "shutdown", "version": 1}, + {"name": "SYNO.DSM.Power", "method": "shutdown", "version": 1}, + {"name": "SYNO.System.Power", "method": "shutdown", "version": 1}, + {"name": "SYNO.DSM.System", "method": "shutdown", "version": 1} + ] + + # Проверяем доступные API + try: + api_info_url = f"{self.base_url}/entry.cgi" + api_info_params = { + "api": "SYNO.API.Info", + "version": "1", + "method": "query", + "query": "SYNO.Core.System.Power,SYNO.DSM.Power,SYNO.Core.System,SYNO.System.Power,SYNO.DSM.System" + } + + logger.debug("Checking available shutdown APIs") + api_info_response = self.session.get( + api_info_url, + params=api_info_params, + timeout=self.default_timeout, + verify=False + ) + + if api_info_response.status_code == 200: + api_info_data = api_info_response.json() + if api_info_data.get("success"): + available_apis = api_info_data.get("data", {}) + logger.debug(f"Available APIs: {list(available_apis.keys())}") + + # Фильтруем только доступные API + filtered_apis = [] + for api in apis_to_try: + if api["name"] in available_apis: + api_info = available_apis[api["name"]] + # Проверка версии API + if api_info.get("minVersion", 1) <= api["version"] <= api_info.get("maxVersion", 1): + filtered_apis.append(api) + logger.debug(f"Adding {api['name']} to available shutdown APIs") + else: + logger.debug(f"API {api['name']} version {api['version']} not supported. Supported: {api_info.get('minVersion')}-{api_info.get('maxVersion')}") + + if filtered_apis: + apis_to_try = filtered_apis + else: + logger.warning("No compatible APIs found, trying all methods as fallback") + else: + logger.warning("Failed to query API info, using default methods") + else: + logger.warning(f"API info request failed with status {api_info_response.status_code}, using default methods") + except Exception as e: + logger.warning(f"Error querying API info: {str(e)}, using default methods") + + # Пробуем все доступные методы по порядку + for api in apis_to_try: + logger.info(f"Trying shutdown with {api['name']}.{api['method']} v{api['version']}") + result = self._make_api_request(api["name"], api["method"], version=api["version"]) + + if result is not None: + logger.info(f"Successfully initiated system shutdown using {api['name']}") + + # Даем системе время начать процесс выключения + logger.info("Waiting for shutdown to initialize...") + sleep(5) + + # Проверяем статус + if not self.is_online(force_check=True): + logger.info("System is now offline. Shutdown confirmed successful.") + return True + else: + logger.info("System still appears to be online, but shutdown may be in progress.") + return True + else: + logger.warning(f"Failed to shutdown using {api['name']}.{api['method']} v{api['version']}") + + # Если ни один метод не сработал, но система стала недоступна + if not self.is_online(force_check=True): + logger.info("System appears to be shutting down despite API errors") + return True + + logger.error("Failed to shutdown system after trying multiple APIs") + return False + + def reboot_system(self) -> bool: + """Перезагрузка системы""" + # Проверяем, включена ли система + if not self.is_online(force_check=True): + logger.error("Cannot reboot: System is offline") + return False + + logger.info("Attempting to reboot Synology NAS...") + + # Попробуем сначала использовать предпочтительный API для управления питанием + logger.info(f"Trying reboot with {SYNOLOGY_POWER_API} v{SYNOLOGY_API_VERSION}") + + # Для SYNO.Core.Hardware.PowerRecovery используется метод setPowerOnState или powerButton + # Для других API обычно используется метод restart или reboot + if SYNOLOGY_POWER_API == "SYNO.Core.Hardware.PowerRecovery": + # Для этого API нужны специальные параметры + params = {"reboot": "true"} # Передаем флаг для перезагрузки + result = self._make_api_request(SYNOLOGY_POWER_API, "setPowerOnState", version=SYNOLOGY_API_VERSION, params=params) + else: + # Пробуем стандартный метод + methods_to_try = ["restart", "reboot"] + result = None + for method in methods_to_try: + result = self._make_api_request(SYNOLOGY_POWER_API, method, version=SYNOLOGY_API_VERSION) + if result is not None: + logger.info(f"Successfully used method {method} for {SYNOLOGY_POWER_API}") + break + + if result is not None: + logger.info(f"Successfully initiated system reboot using {SYNOLOGY_POWER_API}") + + # Даем системе время начать процесс перезагрузки + logger.info("Waiting for reboot to initialize...") + sleep(5) + + # Ждем, пока система станет недоступна (признак перезагрузки) + reboot_started = False + for i in range(12): # Проверяем в течение 60 секунд (12 * 5сек) + if not self.is_online(force_check=True): + logger.info(f"System went offline after {i*5} seconds, reboot in progress") + reboot_started = True + break + logger.debug(f"System still online, waiting... ({i+1}/12)") + sleep(5) + + if reboot_started: + # Ждем, пока система снова станет доступна + logger.info("Waiting for system to come back online...") + return self.wait_for_boot(max_attempts=30, delay=10) + else: + logger.warning("System did not go offline after reboot command, but command was accepted") + # Даже если система не ушла оффлайн, команда могла быть принята + return True + else: + logger.warning(f"Failed to reboot using {SYNOLOGY_POWER_API}, trying alternative methods") + + # Если не сработал основной метод, пробуем резервные варианты + # Проверка всех доступных методов API для перезагрузки + apis_to_try = [ + {"name": "SYNO.Core.System.Power", "method": "restart", "version": 1}, + {"name": "SYNO.DSM.Power", "method": "reboot", "version": 1}, + {"name": "SYNO.Core.System", "method": "reboot", "version": 3}, + {"name": "SYNO.System.Power", "method": "reboot", "version": 1}, + {"name": "SYNO.DSM.System", "method": "reboot", "version": 1}, + {"name": "SYNO.Core.Hardware.NeedReboot", "method": "reboot", "version": 1} + ] + + # Получаем список доступных API + try: + api_info_url = f"{self.base_url}/entry.cgi" + api_info_params = { + "api": "SYNO.API.Info", + "version": "1", + "method": "query", + "query": "SYNO.Core.System.Power,SYNO.DSM.Power,SYNO.Core.System,SYNO.System.Power,SYNO.DSM.System,SYNO.Core.Hardware.NeedReboot" + } + + logger.debug("Checking available reboot APIs") + api_info_response = self.session.get( + api_info_url, + params=api_info_params, + timeout=self.default_timeout, + verify=False + ) + + if api_info_response.status_code == 200: + api_info_data = api_info_response.json() + if api_info_data.get("success"): + available_apis = api_info_data.get("data", {}) + logger.debug(f"Available APIs: {list(available_apis.keys())}") + + # Фильтруем только доступные API + filtered_apis = [] + for api in apis_to_try: + if api["name"] in available_apis: + api_info = available_apis[api["name"]] + # Проверка версии API + if api_info.get("minVersion", 1) <= api["version"] <= api_info.get("maxVersion", 1): + filtered_apis.append(api) + logger.debug(f"Adding {api['name']} to available reboot APIs") + else: + logger.debug(f"API {api['name']} version {api['version']} not supported. Supported: {api_info.get('minVersion')}-{api_info.get('maxVersion')}") + + if filtered_apis: + apis_to_try = filtered_apis + else: + logger.warning("No compatible APIs found, trying all methods as fallback") + else: + logger.warning("Failed to query API info, using default methods") + else: + logger.warning(f"API info request failed with status {api_info_response.status_code}, using default methods") + except Exception as e: + logger.warning(f"Error querying API info: {str(e)}, using default methods") + + # Пробуем все доступные методы по порядку + for api in apis_to_try: + logger.info(f"Trying reboot with {api['name']}.{api['method']} v{api['version']}") + result = self._make_api_request(api["name"], api["method"], version=api["version"]) + + if result is not None: + logger.info(f"Successfully initiated system reboot using {api['name']}") + + # Даем системе время начать процесс перезагрузки + logger.info("Waiting for reboot to initialize...") + sleep(5) + + # Ждем, пока система станет недоступна (признак перезагрузки) + reboot_started = False + for i in range(12): # Проверяем в течение 60 секунд (12 * 5сек) + if not self.is_online(force_check=True): + logger.info(f"System went offline after {i*5} seconds, reboot in progress") + reboot_started = True + break + logger.debug(f"System still online, waiting... ({i+1}/12)") + sleep(5) + + if reboot_started: + # Ждем, пока система снова станет доступна + logger.info("Waiting for system to come back online...") + return self.wait_for_boot(max_attempts=30, delay=10) + else: + logger.warning("System did not go offline after reboot command, but command was accepted") + # Даже если система не ушла оффлайн, команда могла быть принята + return True + else: + logger.warning(f"Failed to reboot using {api['name']}.{api['method']} v{api['version']}") + + logger.error("Failed to reboot system after trying multiple APIs") + return False + + def _get_error_description(self, error_code: int) -> str: + """Получение описания ошибки по коду""" + error_descriptions = { + 100: "Unknown error", + 101: "Invalid parameter", + 102: "API does not exist", + 103: "Method does not exist", + 104: "Version does not support", + 105: "Permission denied", + 106: "Session timeout", + 107: "Session interrupted by duplicate login", + 400: "Invalid credentials", + 401: "Account disabled", + 402: "Permission denied", + 403: "2FA required", + 404: "Failed to authenticate with 2FA" + } + return error_descriptions.get(error_code, "Unknown error code") + + def _check_tcp_connection(self) -> bool: + """Проверка базового TCP-соединения с Synology NAS""" + try: + socket_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + socket_obj.settimeout(SYNOLOGY_TIMEOUT) + result = socket_obj.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT)) + socket_obj.close() + + return result == 0 + except socket.error as e: + logger.error(f"Socket error during connection check: {str(e)}") + return False + except Exception as e: + logger.error(f"Unexpected error during connection check: {str(e)}") + return False + + def is_online(self, force_check=False) -> bool: + """Проверка онлайн-статуса Synology NAS""" + # Используем кэшированное значение, если доступно и не устарело + current_time = time.time() + if not force_check and (current_time - self._last_online_check) < self._online_check_interval: + logger.debug(f"Using cached online status: {self._last_online_status}") + return self._last_online_status + + logger.info("Checking if NAS is online...") + + # Проверяем TCP-соединение + online_status = self._check_tcp_connection() + logger.info(f"Detected Synology NAS online status: {online_status}") + + # Если TCP-соединение успешно и у нас есть действующий SID, + # попробуем более детальную проверку через API + if online_status and self.sid: + logger.info("Trying to fetch more detailed online status through API...") + + # Пробуем разные API для проверки онлайн-статуса + api_checks = [ + {"api": "SYNO.DSM.Info", "version": "2", "method": "getinfo"}, + {"api": "SYNO.Core.System", "version": "1", "method": "info"}, + {"api": "SYNO.Core.System.Status", "version": "1", "method": "get"} + ] + + api_success = False + for api_check in api_checks: + try: + url = f"{self.base_url}/entry.cgi" + params = { + "api": api_check["api"], + "version": api_check["version"], + "method": api_check["method"], + "sid": self.sid + } + + logger.debug(f"Trying online status check with {api_check['api']}") + response = self.session.get( + url, + params=params, + timeout=self.default_timeout, + verify=False + ) + + if response.status_code == 200: + data = response.json() + if data.get("success"): + logger.info(f"API request successful for {api_check['api']}") + logger.info("Synology NAS is online with API access") + api_success = True + break + else: + error_code = data.get("error", {}).get("code", -1) + logger.warning(f"API response indicates an error: {error_code}, but NAS is reachable") + else: + logger.warning(f"API returned status code {response.status_code}") + except Exception as e: + logger.warning(f"API check failed for {api_check['api']}, but TCP connection succeeded: {str(e)}") + + if not api_success: + logger.warning("All API checks failed, but TCP connection is successful") + + # Обновляем кэшированное значение + self._last_online_check = current_time + self._last_online_status = online_status + + return online_status + + def wake_on_lan(self) -> bool: + """Отправка Wake-on-LAN пакета для включения Synology NAS""" + if not SYNOLOGY_MAC: + logger.error("MAC address not configured") + return False + + try: + # Преобразование MAC-адреса в байты + mac_address = SYNOLOGY_MAC.replace(':', '').replace('-', '') + if len(mac_address) != 12: + logger.error(f"Invalid MAC address format: {SYNOLOGY_MAC}") + return False + + try: + mac_bytes = bytes.fromhex(mac_address) + except ValueError as e: + logger.error(f"Failed to parse MAC address: {str(e)}") + logger.error(f"MAC address must contain only hex characters: {SYNOLOGY_MAC}") + return False + + # Создание Magic Packet + magic_packet = b'\xff' * 6 + mac_bytes * 16 + + # Отправка пакета на конкретный адрес + try: + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) + sock.sendto(magic_packet, (SYNOLOGY_HOST, WOL_PORT)) + sock.close() + logger.info(f"Wake-on-LAN packet sent to {SYNOLOGY_MAC} at {SYNOLOGY_HOST}:{WOL_PORT}") + except Exception as e: + logger.error(f"Error sending directed WoL packet: {str(e)}") + return False + + # Для надежности отправляем также широковещательный пакет + try: + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) + + # Используем стандартный широковещательный адрес + broadcast_addr = "255.255.255.255" + sock.sendto(magic_packet, (broadcast_addr, WOL_PORT)) + sock.close() + logger.info(f"Wake-on-LAN broadcast packet sent to {broadcast_addr}:{WOL_PORT}") + except Exception as e: + logger.warning(f"Error sending broadcast WoL packet (non-critical): {str(e)}") + # Не считаем ошибкой, т.к. основной пакет уже отправлен + + return True + + except Exception as e: + logger.error(f"Unexpected error in wake_on_lan: {str(e)}") + return False + + def wait_for_boot(self, max_attempts: int = 30, delay: int = 10) -> bool: + """Ожидание загрузки Synology NAS""" + logger.info(f"Waiting for Synology NAS to boot (timeout: {max_attempts * delay}s)...") + + for attempt in range(max_attempts): + # Принудительно проверяем статус без использования кэша + if self.is_online(force_check=True): + logger.info(f"Synology NAS is online after {attempt + 1} attempts ({(attempt + 1) * delay}s)") + + # Проверяем, что не только сеть доступна, но и API загрузился + api_ready = False + logger.info("Waiting for API services to initialize...") + + for api_check in range(5): # Даем еще до 50 секунд для загрузки API + if self.sid or self.login(): + api_ready = True + logger.info(f"API services are ready after {api_check + 1} attempts") + break + logger.debug(f"API not ready yet, waiting... ({api_check + 1}/5)") + sleep(10) + + if not api_ready: + logger.warning("System is online but API services may not be fully initialized") + + # Дадим дополнительное время для полной загрузки всех сервисов + sleep(delay) + return True + + sleep(delay) + logger.info(f"Waiting for boot... Attempt {attempt + 1}/{max_attempts}") + + logger.error(f"Synology NAS didn't come online after {max_attempts * delay} seconds") + return False + + def power_on(self) -> bool: + """Включение Synology NAS""" + # Принудительная проверка статуса + if self.is_online(force_check=True): + logger.info("Synology NAS is already online") + return True + + logger.info("Powering on Synology NAS via Wake-on-LAN...") + + # Проверяем, настроен ли MAC-адрес + if not SYNOLOGY_MAC: + logger.error("Cannot power on: MAC address not configured in settings") + return False + + # Пробуем отправить несколько WoL пакетов для надежности + success = False + for attempt in range(3): + logger.debug(f"Sending WoL packet, attempt {attempt + 1}/3") + if self.wake_on_lan(): + success = True + break + sleep(1) + + if not success: + logger.error("Failed to send Wake-on-LAN packets") + return False + + # Ожидание загрузки + logger.info("WoL packets sent successfully, waiting for system to boot...") + boot_result = self.wait_for_boot(max_attempts=30, delay=10) + + if boot_result: + # Проверяем доступность API после загрузки + system_status = self.get_system_status() + if system_status.get("status") == "online": + logger.info("System booted successfully with API access") + return True + else: + logger.warning("System appears to be online but API may not be fully ready") + return True + else: + logger.error("System did not come online after WoL") + return False + + def power_off(self) -> bool: + """Выключение Synology NAS""" + if not self.is_online(force_check=True): + logger.info("Synology NAS is already offline") + return True + + logger.info("Powering off Synology NAS...") + result = self.shutdown_system() + + if result: + # Дополнительная проверка, что система действительно выключилась + logger.info("Verifying system is offline...") + for attempt in range(12): # Проверяем в течение 2 минут (12 * 10 сек) + if not self.is_online(force_check=True): + logger.info(f"System confirmed offline after {attempt * 10} seconds") + return True + logger.debug(f"System still shutting down, checking again... ({attempt + 1}/12)") + sleep(10) + + logger.warning("System still appears to be online after shutdown command") + return False + else: + logger.error("Failed to initiate shutdown") + return False + + # Заглушки для расширенных методов + def get_shared_folders(self) -> List[Dict[str, Any]]: + """Получение списка общих папок""" + logger.info("Getting list of shared folders") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for shared folders request") + return [] + + try: + # Запрашиваем список общих папок через FileStation API + result = self._make_api_request("SYNO.FileStation.List", "list_share", version=2) + + if not result: + # Пробуем альтернативный API + logger.info("Trying alternative API for shared folders") + alt_result = self._make_api_request("SYNO.FileStation.List", "list_share", version=1) + if alt_result: + return alt_result.get("shares", []) + return [] + + return result.get("shares", []) + + except Exception as e: + logger.error(f"Error getting shared folders: {str(e)}") + return [] + + def get_system_load(self) -> Dict[str, Any]: + """Получение информации о загрузке системы""" + logger.info("Getting system load information") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for system load request") + return {} + + try: + # Запрашиваем информацию о загрузке системы + result = self._make_api_request("SYNO.Core.System.Utilization", "get", version=1) + + if not result: + # Пробуем альтернативный API + logger.info("Trying alternative API for system load") + alt_result = self._make_api_request("SYNO.Core.System", "info", version=3) + + if not alt_result: + return {} + + # Формируем из частичных данных + return { + "cpu_load": alt_result.get("cpu_usage", 0), + "memory": { + "total": alt_result.get("memory_size", 0), + "used": alt_result.get("memory_usage", 0), + "usage_percent": alt_result.get("memory_usage_percent", 0) + } + } + + # Формируем структурированный результат + return { + "cpu_load": result.get("cpu", {}).get("user", 0) + result.get("cpu", {}).get("system", 0), + "memory": result.get("memory", {}), + "network": result.get("network", {}) + } + + except Exception as e: + logger.error(f"Error getting system load: {str(e)}") + return {} + + def get_storage_status(self) -> Dict[str, Any]: + """Получение подробной информации о хранилище""" + logger.info("Getting storage status information") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for storage status request") + return {"error": "authentication_failed"} + + try: + # Пробуем получить информацию о хранилище через SYNO.Storage.CGI.Storage API + result = self._make_api_request("SYNO.Storage.CGI.Storage", "load_info", version=1) + + if not result: + # Пробуем альтернативный API + logger.info("Trying alternative API for storage info") + alt_result = self._make_api_request("SYNO.Core.Storage", "info", version=1) + + if not alt_result: + # Пробуем еще один альтернативный API + logger.info("Trying SYNO.Core.System API for storage info") + sys_result = self._make_api_request("SYNO.Core.System", "info", version=3) + + if not sys_result: + return { + "volumes": [], + "disks": [], + "total_size": 0, + "total_used": 0, + "error": "no_data" + } + + # Извлекаем базовую информацию о хранилище из системной информации + return { + "volumes": [], + "disks": [], + "total_size": sys_result.get("hdd_total_size", 0) * 1024**2, # МБ в байты + "total_used": sys_result.get("hdd_total_size", 0) * sys_result.get("hdd_usage", 0) / 100 * 1024**2, + } + + # Обрабатываем данные из альтернативного API + volumes = alt_result.get("volumes", []) + disks = alt_result.get("disks", []) + + else: + # Обрабатываем данные из основного API + volumes = result.get("volumes", []) + disks = result.get("disks", []) + + # Рассчитываем общие размеры + total_size = 0 + total_used = 0 + + for volume in volumes: + volume_size = volume.get("size", {}).get("total", 0) + volume_used = volume.get("size", {}).get("used", 0) + + total_size += volume_size + total_used += volume_used + + return { + "volumes": volumes, + "disks": disks, + "total_size": total_size, + "total_used": total_used + } + + except Exception as e: + logger.error(f"Error in get_storage_status: {str(e)}") + return {"error": str(e)} + + def get_security_status(self) -> Dict[str, Any]: + """Получение информации о состоянии безопасности""" + logger.info("Getting security status information") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for security status request") + return {"success": False, "error": "authentication_failed"} + + try: + # Пробуем получить информацию о безопасности через API Security Scan + result = self._make_api_request("SYNO.Core.SecurityScan.Status", "get", version=1) + + if not result: + # Пробуем альтернативный API + logger.info("Trying alternative API for security status") + # Проверяем статус брандмауэра + firewall_result = self._make_api_request("SYNO.Core.Security.Firewall.Status", "get", version=1) + + # Проверяем статус автоматических обновлений + update_result = self._make_api_request("SYNO.Core.Upgrade.Server.Setting", "get", version=1) + + # Если ни один из API не отвечает + if not firewall_result and not update_result: + # Получаем общую информацию о системе для базовой проверки безопасности + sys_result = self._make_api_request("SYNO.DSM.Info", "getinfo", version=2) + + if not sys_result: + return { + "success": False, + "status": "unknown", + "last_check": None, + "is_secure": False, + "error": "no_security_api" + } + + # Собираем базовые сведения из системной информации + return { + "success": True, + "status": "basic", + "last_check": None, + "is_secure": True, # Предполагаем, что система в целом безопасна + "firewall_enabled": None, + "auto_update": None, + "version_latest": sys_result.get("version_string", "") + } + + # Собираем информацию из доступных результатов + firewall_enabled = firewall_result.get("enabled", False) if firewall_result else None + auto_update = update_result.get("auto_update", False) if update_result else None + + # Определяем, насколько система безопасна + is_secure = True # По умолчанию предполагаем, что система безопасна + if firewall_enabled is not None and not firewall_enabled: + is_secure = False + + return { + "success": True, + "status": "partial", + "last_check": None, + "is_secure": is_secure, + "firewall_enabled": firewall_enabled, + "auto_update": auto_update + } + + # Если основное API отвечает, возвращаем его данные + return { + "success": True, + "status": result.get("status", "unknown"), + "last_check": result.get("last_check", None), + "is_secure": result.get("is_secure", False), + "details": result.get("details", {}) + } + + except Exception as e: + logger.error(f"Error in get_security_status: {str(e)}") + return {"success": False, "error": str(e)} diff --git a/.history/src/api/synology_20250830091024.py b/.history/src/api/synology_20250830091024.py new file mode 100644 index 0000000..4b06eb0 --- /dev/null +++ b/.history/src/api/synology_20250830091024.py @@ -0,0 +1,1480 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Модуль для взаимодействия с API Synology NAS +""" + +import requests +from requests.adapters import HTTPAdapter +import json +import logging +import time +import urllib3 +from urllib3.util import Retry +from typing import Dict, Any, Optional, List +import socket +import struct +from time import sleep + +from src.config.config import ( + SYNOLOGY_HOST, + SYNOLOGY_PORT, + SYNOLOGY_USERNAME, + SYNOLOGY_PASSWORD, + SYNOLOGY_SECURE, + SYNOLOGY_TIMEOUT, + SYNOLOGY_MAC, + WOL_PORT, + SYNOLOGY_API_VERSION, + SYNOLOGY_POWER_API, + SYNOLOGY_INFO_API +) +from src.api.api_discovery import discover_available_apis, find_compatible_api + +# Отключение предупреждений о небезопасных SSL-соединениях +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + +logger = logging.getLogger(__name__) + +class SynologyAPI: + """Класс для взаимодействия с API Synology NAS""" + + def __init__(self): + """Инициализация класса SynologyAPI""" + logger.info("Creating API with auto-retry and connection pool") + logger.debug(f"Connection details: Host={SYNOLOGY_HOST}, Port={SYNOLOGY_PORT}, Secure={SYNOLOGY_SECURE}") + + self.protocol = "https" if SYNOLOGY_SECURE else "http" + self.base_url = f"{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi" + self.sid = None + self.session = requests.Session() + + # Настройка SSL + if self.protocol == "https": + logger.debug("SSL enabled, disabling certificate verification for internal network") + self.session.verify = False # Отключаем проверку SSL для внутренней сети + + # Добавляем пользовательские заголовки для улучшения совместимости с API + custom_headers = { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36', + 'Accept': 'application/json, text/javascript, */*; q=0.01', + 'Accept-Language': 'ru-RU,ru;q=0.9,en-US;q=0.8,en;q=0.7', + 'X-Requested-With': 'XMLHttpRequest', + 'Connection': 'keep-alive', + 'Referer': f'{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/' + } + self.session.headers.update(custom_headers) + logger.debug("Added browser-like headers for API compatibility") + + # Добавляем повторные попытки для HTTP-запросов + retry_strategy = Retry( + total=5, # Увеличиваем количество попыток + status_forcelist=[429, 500, 502, 503, 504, 404], + allowed_methods=["GET", "POST"], + backoff_factor=1.5, # Увеличиваем задержку между попытками + respect_retry_after_header=True + ) + adapter = HTTPAdapter( + max_retries=retry_strategy, + pool_connections=3, + pool_maxsize=10 + ) + self.session.mount("http://", adapter) + self.session.mount("https://", adapter) + + # Таймауты будут указаны в запросах + self.default_timeout = (SYNOLOGY_TIMEOUT, SYNOLOGY_TIMEOUT*2) + logger.debug(f"Setting default request timeout: {self.default_timeout}") + + # Кэш для хранения результатов запросов + self._cache = {} + self._cache_ttl = {} + self._last_online_check = 0 + self._last_online_status = False + self._online_check_interval = 30 # Интервал для кэширования проверок онлайн-статуса + + # Время последней успешной аутентификации и срок действия сессии + self._last_auth_time = 0 + self._auth_expiry = 3600 # По умолчанию 1 час + + # Информация о доступных API + self._available_apis = {} + self._api_info_ttl = 0 + + # Инициализируем API version resolver для автоматического определения совместимых API + self.api_resolver = None # Будет создан при необходимости + + def login(self) -> bool: + """Авторизация в API Synology NAS""" + # Сбрасываем SID для новой сессии + self.sid = None + + logger.info("Attempting to authenticate with Synology NAS...") + logger.debug(f"Base URL: {self.base_url}") + + # Проверяем доступность NAS перед запросом с помощью прямой TCP-проверки + # Избегаем вызова is_online(), чтобы не создавать рекурсию + online_status = self._check_tcp_connection() + if not online_status: + logger.error("Cannot login: Synology NAS is not reachable") + return False + + # Пробуем различные версии API для аутентификации + # Начинаем с версии 3, которая показала лучшую совместимость в тестах + auth_versions_to_try = [3, 2, 1, 6] # Пробуем сначала более стабильные версии + + for auth_version in auth_versions_to_try: + try: + # Определяем путь к API аутентификации + auth_path = "entry.cgi" # Используем entry.cgi вместо auth.cgi по умолчанию + + # Проверка информации API для определения доступных версий API + api_info_url = f"{self.base_url}/entry.cgi" + api_info_params = { + "api": "SYNO.API.Info", + "version": "1", + "method": "query", + "query": "SYNO.API.Auth" + } + + logger.debug(f"Querying API info for auth version {auth_version}") + try: + api_info_response = self.session.get( + api_info_url, + params=api_info_params, + timeout=self.default_timeout, + verify=False + ) + + if api_info_response.status_code == 200: + api_info_data = api_info_response.json() + if api_info_data.get("success"): + auth_info = api_info_data.get("data", {}).get("SYNO.API.Auth", {}) + max_version = auth_info.get("maxVersion", 6) + min_version = auth_info.get("minVersion", 1) + auth_path = auth_info.get("path", "entry.cgi") + logger.debug(f"Auth API versions: min={min_version}, max={max_version}, path={auth_path}") + + # Проверяем поддержку текущей версии + if auth_version < min_version or auth_version > max_version: + logger.warning(f"Auth API version {auth_version} not supported (min={min_version}, max={max_version}), trying next version") + continue + else: + logger.warning("Failed to query API info, using default auth path") + else: + logger.warning(f"API info request failed with status {api_info_response.status_code}, using default auth path") + except Exception as e: + logger.warning(f"Error querying API info: {str(e)}, using default auth path") + + # Основной запрос авторизации + url = f"{self.base_url}/{auth_path}" + params = { + "api": "SYNO.API.Auth", + "version": str(auth_version), + "method": "login", + "account": SYNOLOGY_USERNAME, + "passwd": SYNOLOGY_PASSWORD, + "session": "SynologyPowerControlBot", + "format": "cookie" + } + + # Для версии 6+ используем немного другой формат + if auth_version >= 6: + params["enable_syno_token"] = "yes" + + logger.debug(f"Sending auth request to {url} with API version {auth_version}") + start_time = time.time() + response = self.session.get( + url, + params=params, + timeout=self.default_timeout, + verify=False + ) + elapsed_time = time.time() - start_time + logger.debug(f"Auth request completed in {elapsed_time:.2f}s with status code {response.status_code}") + + # Проверка статуса HTTP + if response.status_code != 200: + logger.error(f"HTTP error: {response.status_code}") + logger.debug(f"Response content: {response.text[:200]}") + continue # Пробуем следующую версию + + try: + data = response.json() + except json.JSONDecodeError: + logger.error("Failed to decode JSON response") + logger.debug(f"Response content: {response.text[:200]}") + continue # Пробуем следующую версию + + if data.get("success"): + self.sid = data.get("data", {}).get("sid") + self._last_auth_time = time.time() + logger.info(f"Successfully logged in to Synology NAS using API version {auth_version}") + logger.debug(f"Session ID obtained: {self.sid[:5] if self.sid else 'None'}...") + + # Получаем и сохраняем токен SYNO, если он есть + syno_token = data.get("data", {}).get("synotoken") + if syno_token: + self.session.headers.update({'X-SYNO-TOKEN': syno_token}) + logger.debug("Added X-SYNO-TOKEN header for improved API compatibility") + + # Также добавляем SID в cookies для улучшения совместимости + self.session.cookies.update({ + 'id': self.sid, + 'sid': self.sid + }) + logger.debug("Added SID to session cookies for improved compatibility") + + # Проверка валидности полученной сессии с помощью простого запроса + # Будем использовать SYNO.API.Info без проверки сложных методов + + # Даем системе немного времени для инициализации сессии + time.sleep(0.5) + + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to log in with version {auth_version}: Error code {error_code}") + + # Если ошибка связана с версией API, пробуем следующую версию + if error_code in [104, 105]: + logger.warning(f"Auth version {auth_version} not supported, trying next version") + continue + + # Дополнительная диагностика + if error_code == 400: + logger.error("Authentication error: Invalid credentials") + elif error_code == 401: + logger.error("Authentication error: Account disabled") + elif error_code == 402: + logger.error("Authentication error: Permission denied") + elif error_code == 403: + logger.error("Authentication error: 2-factor authentication required") + elif error_code == 404: + logger.error("Authentication error: Failed to authenticate with 2-factor authentication") + + # Если ошибка связана с неверными учетными данными, нет смысла пробовать другие версии API + if error_code in [400, 401, 402, 403, 404]: + return False + + except requests.exceptions.Timeout: + logger.error(f"Connection timeout during auth with version {auth_version}") + continue # Пробуем следующую версию + except requests.exceptions.ConnectionError as e: + logger.error(f"Connection error during auth with version {auth_version}: {str(e)}") + continue # Пробуем следующую версию + except requests.RequestException as e: + logger.error(f"Request error during auth with version {auth_version}: {str(e)}") + continue # Пробуем следующую версию + except Exception as e: + logger.error(f"Unexpected error during login with version {auth_version}: {str(e)}", exc_info=True) + continue # Пробуем следующую версию + + # Если все версии не сработали + logger.error("Failed to authenticate with any API version") + return False + + def _validate_session(self) -> bool: + """Проверяет валидность сессии после авторизации""" + if not self.sid: + return False + + # Попробуем сделать простой запрос для проверки сессии + test_apis = [ + {"api": "SYNO.Core.System", "method": "info", "version": 1}, + {"api": "SYNO.DSM.Info", "method": "getinfo", "version": 1} + ] + + for test_api in test_apis: + try: + url = f"{self.base_url}/entry.cgi" + params = { + "api": test_api["api"], + "version": str(test_api["version"]), + "method": test_api["method"], + "_sid": self.sid + } + + response = self.session.get(url, params=params, timeout=self.default_timeout, verify=False) + + if response.status_code == 200: + data = response.json() + if data.get("success"): + logger.debug(f"Session validation successful using {test_api['api']}") + return True + else: + error_code = data.get("error", {}).get("code", -1) + if error_code != 119: # Не сессия истекла + logger.debug(f"Session validation with {test_api['api']} returned error code {error_code}") + return True # Считаем сессию валидной, если ошибка не связана с истечением сессии + except Exception as e: + logger.warning(f"Error during session validation with {test_api['api']}: {str(e)}") + + logger.warning("Session validation failed with all test APIs") + return False + + def logout(self) -> bool: + """Выход из API Synology NAS""" + if not self.sid: + return True + + try: + url = f"{self.base_url}/auth.cgi" + params = { + "api": "SYNO.API.Auth", + "version": "1", + "method": "logout", + "session": "SynologyPowerControlBot", + "_sid": self.sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + self.sid = None + logger.info("Successfully logged out from Synology NAS") + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to log out from Synology NAS: Error code {error_code}") + return False + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + return False + + def _make_api_request(self, api_name: str, method: str, version: int = 1, + params: Optional[Dict[str, Any]] = None, retry_auth: bool = True, retry_count: int = 0) -> Optional[Dict[str, Any]]: + """Обобщенный метод для выполнения API запросов с обработкой ошибок""" + # Ограничение на количество повторных попыток + if retry_count >= 3: + logger.error(f"Too many retries for {api_name}.{method}, giving up") + return None + + # Проверка наличия авторизации + if not self.sid and not self.login(): + logger.error(f"Not authenticated for API request: {api_name}.{method}") + return None + + # Проверка информации API для определения пути и поддерживаемой версии + api_info_url = f"{self.base_url}/entry.cgi" + api_info_params = { + "api": "SYNO.API.Info", + "version": "1", + "method": "query", + "query": api_name + } + + api_path = "entry.cgi" + try: + logger.debug(f"Querying API info for {api_name}") + api_info_response = self.session.get( + api_info_url, + params=api_info_params, + timeout=self.default_timeout, + verify=False + ) + + if api_info_response.status_code == 200: + api_info_data = api_info_response.json() + if api_info_data.get("success"): + api_info = api_info_data.get("data", {}).get(api_name, {}) + if api_info: + max_version = api_info.get("maxVersion", version) + min_version = api_info.get("minVersion", version) + api_path = api_info.get("path", "entry.cgi") + + # Проверка, поддерживается ли запрошенная версия + if version < min_version: + logger.warning(f"API version {version} for {api_name} is below minimum {min_version}, using {min_version}") + version = min_version + elif version > max_version: + logger.warning(f"API version {version} for {api_name} exceeds maximum {max_version}, using {max_version}") + version = max_version + + logger.debug(f"Using API path: {api_path}, version: {version}") + else: + logger.warning(f"API {api_name} not found in API info, using defaults") + except Exception as e: + logger.warning(f"Error querying API info for {api_name}: {str(e)}, using defaults") + + # Подготовка базовых параметров запроса + base_params = { + "api": api_name, + "version": str(version), + "method": method, + "_sid": self.sid # Используем _sid вместо sid для лучшей совместимости + } + + # Добавление дополнительных параметров, если они заданы + if params: + base_params.update(params) + + url = f"{self.base_url}/{api_path}" + logger.debug(f"API request: {api_name}.{method} v{version} to {url}") + logger.debug(f"Full request params: {base_params}") + + try: + start_time = time.time() + response = self.session.get( + url, + params=base_params, + timeout=self.default_timeout, + verify=False + ) + elapsed_time = time.time() - start_time + logger.debug(f"API request completed in {elapsed_time:.2f}s with status {response.status_code}") + + # Проверка статуса HTTP + if response.status_code != 200: + logger.error(f"HTTP error: {response.status_code} for {api_name}.{method}") + + # Повторная попытка при ошибках соединения + if response.status_code in [500, 502, 503, 504]: + logger.info(f"Server error, retrying request for {api_name}.{method}") + sleep(2) + return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1) + + return None + + try: + data = response.json() + except json.JSONDecodeError: + logger.error(f"Failed to decode JSON response for {api_name}.{method}") + logger.debug(f"Response content: {response.text[:200]}") + + # Повторная попытка при ошибках декодирования + logger.info(f"JSON decode error, retrying request for {api_name}.{method}") + sleep(2) + return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1) + + if data.get("success"): + logger.info(f"API request successful for {api_name}.{method}") + return data.get("data", {}) + else: + error_code = data.get("error", {}).get("code", -1) + error_desc = self._get_error_description(error_code) + logger.error(f"API error for {api_name}.{method}: {error_code} - {error_desc}") + + # Ошибки доступа или прав часто встречаются, но они не критичные + # Например, ошибка 102 означает, что нет прав, но NAS доступен + if error_code in [102, 103, 104, 105]: + logger.warning(f"Permission-related error for {api_name}.{method}: {error_code}") + # Возвращаем пустой словарь вместо None, + # чтобы вызывающий код мог понять, что запрос выполнен + return {} + + # Если ошибка связана с авторизацией и нам разрешено повторить попытку + if error_code in [106, 107, 119] and retry_auth: + logger.info(f"Session error (code {error_code}), creating fresh session...") + self.sid = None # Сбрасываем SID + + # Для ошибки 119 (Session timeout) дадим системе немного времени + if error_code == 119: + logger.info("Session timeout detected, waiting before retry...") + sleep(3) + + if self.login(): + logger.info("Re-authenticated with fresh session, retrying API request...") + # Рекурсивный вызов, но со счетчиком повторов + return self._make_api_request(api_name, method, version, params, False, retry_count + 1) + + # Для некоторых ошибок можно автоматически повторить запрос + if error_code in [408, 429, 500, 502, 503, 504]: + logger.info(f"Temporary error {error_code}, retrying request for {api_name}.{method}") + sleep(2) + return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1) + + return None + + except requests.exceptions.Timeout: + logger.error(f"Request timeout for {api_name}.{method}") + + # Повторная попытка при таймауте + if retry_count < 2: + logger.info(f"Timeout, retrying request for {api_name}.{method}") + sleep(2) + return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1) + + return None + except requests.exceptions.ConnectionError as e: + logger.error(f"Connection error for {api_name}.{method}: {str(e)}") + + # Повторная попытка при ошибке соединения + if retry_count < 2: + logger.info(f"Connection error, retrying request for {api_name}.{method}") + sleep(2) + return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1) + + return None + except Exception as e: + logger.error(f"Unexpected error during {api_name}.{method}: {str(e)}") + return None + + def get_system_status(self) -> Dict[str, Any]: + """Получение статуса системы""" + # Проверяем доступность системы + if not self.is_online(): + logger.info("Device is offline, skipping API request") + return {"status": "offline"} + + # Проверяем, есть ли кэшированный результат + cache_key = "system_status" + current_time = time.time() + if cache_key in self._cache and current_time - self._cache_ttl.get(cache_key, 0) < 60: + logger.debug("Using cached system status") + return self._cache[cache_key] + + # Используем рекомендованный API для получения информации о системе + logger.info(f"Getting system info using {SYNOLOGY_INFO_API} v{SYNOLOGY_API_VERSION}") + + # Для SYNO.DSM.Info метод getinfo, для других обычно используется get или info + if SYNOLOGY_INFO_API == "SYNO.DSM.Info": + method = "getinfo" + else: + method = "get" + + result = self._make_api_request(SYNOLOGY_INFO_API, method, version=SYNOLOGY_API_VERSION) + + if result: + logger.info(f"Successfully retrieved system info using {SYNOLOGY_INFO_API}") + + # Формируем расширенный ответ с дополнительной информацией + system_info = { + "status": "online", + "hostname": result.get("hostname", "unknown"), + "model": result.get("model", "unknown"), + "version": result.get("version", "unknown"), + "uptime": result.get("uptime", 0), + "time": current_time, + "is_online": True, + "api_used": SYNOLOGY_INFO_API + } + + # Сохраняем в кэше + self._cache[cache_key] = system_info + self._cache_ttl[cache_key] = current_time + + return system_info + + # Если основной API не сработал, пробуем резервные варианты + logger.warning(f"Failed to retrieve system info with {SYNOLOGY_INFO_API}, trying fallback APIs") + + # Пробуем резервные API + apis_to_try = [ + {"name": "SYNO.DSM.Info", "method": "getinfo", "version": 2}, + {"name": "SYNO.Core.System", "method": "info", "version": 1}, + {"name": "SYNO.Core.System.Status", "method": "get", "version": 1}, + {"name": "SYNO.Core.System.Info", "method": "get", "version": 1}, + ] + + for api in apis_to_try: + if api["name"] == SYNOLOGY_INFO_API: + continue # Пропускаем уже проверенный API + + logger.info(f"Trying to get system info using {api['name']}.{api['method']} v{api['version']}") + result = self._make_api_request(api["name"], api["method"], version=api["version"]) + + if result: + logger.info(f"Successfully retrieved system info using {api['name']}") + + # Формируем расширенный ответ с дополнительной информацией + system_info = { + "status": "online", + "hostname": result.get("hostname", "unknown"), + "model": result.get("model", "unknown"), + "version": result.get("version", "unknown"), + "uptime": result.get("uptime", 0), + "time": current_time, + "is_online": True, + "api_used": api["name"] + } + + # Сохраняем в кэше + self._cache[cache_key] = system_info + self._cache_ttl[cache_key] = current_time + + return system_info + + # Если все запросы не удались, но система онлайн, возвращаем базовую информацию + logger.warning("Failed to retrieve system info with all API methods") + return { + "status": "error", + "error": "Failed to fetch system information", + "is_online": True, + "time": current_time + } + + def shutdown_system(self) -> bool: + """Выключение системы""" + # Проверяем, включено ли устройство перед попыткой его выключить + if not self.is_online(force_check=True): + logger.info("Synology NAS is already offline, no need to shut down") + return True + + logger.info("Attempting to shutdown Synology NAS...") + + # Попробуем сначала использовать предпочтительный API для управления питанием + logger.info(f"Trying shutdown with {SYNOLOGY_POWER_API} v{SYNOLOGY_API_VERSION}") + + # Для SYNO.Core.Hardware.PowerRecovery используется метод setPowerOnState + # Для других API обычно используется метод shutdown или reboot + if SYNOLOGY_POWER_API == "SYNO.Core.Hardware.PowerRecovery": + # Для этого API нужны специальные параметры + params = {"state": "powerbtn"} # powerbtn имитирует нажатие кнопки питания + result = self._make_api_request(SYNOLOGY_POWER_API, "setPowerOnState", version=SYNOLOGY_API_VERSION, params=params) + else: + # Пробуем стандартный метод + result = self._make_api_request(SYNOLOGY_POWER_API, "shutdown", version=SYNOLOGY_API_VERSION) + + if result is not None: + logger.info(f"Successfully initiated system shutdown using {SYNOLOGY_POWER_API}") + + # Даем системе время начать процесс выключения + logger.info("Waiting for shutdown to initialize...") + sleep(5) + + # Проверяем статус + if not self.is_online(force_check=True): + logger.info("System is now offline. Shutdown confirmed successful.") + return True + else: + logger.info("System still appears to be online, but shutdown may be in progress.") + return True + else: + logger.warning(f"Failed to shutdown using {SYNOLOGY_POWER_API}, trying alternative methods") + + # Если не сработал основной метод, пробуем резервные варианты + # Проверка всех доступных методов API для выключения + apis_to_try = [ + {"name": "SYNO.Core.System", "method": "shutdown", "version": 3}, + {"name": "SYNO.Core.System.Power", "method": "shutdown", "version": 1}, + {"name": "SYNO.DSM.Power", "method": "shutdown", "version": 1}, + {"name": "SYNO.System.Power", "method": "shutdown", "version": 1}, + {"name": "SYNO.DSM.System", "method": "shutdown", "version": 1} + ] + + # Проверяем доступные API + try: + api_info_url = f"{self.base_url}/entry.cgi" + api_info_params = { + "api": "SYNO.API.Info", + "version": "1", + "method": "query", + "query": "SYNO.Core.System.Power,SYNO.DSM.Power,SYNO.Core.System,SYNO.System.Power,SYNO.DSM.System" + } + + logger.debug("Checking available shutdown APIs") + api_info_response = self.session.get( + api_info_url, + params=api_info_params, + timeout=self.default_timeout, + verify=False + ) + + if api_info_response.status_code == 200: + api_info_data = api_info_response.json() + if api_info_data.get("success"): + available_apis = api_info_data.get("data", {}) + logger.debug(f"Available APIs: {list(available_apis.keys())}") + + # Фильтруем только доступные API + filtered_apis = [] + for api in apis_to_try: + if api["name"] in available_apis: + api_info = available_apis[api["name"]] + # Проверка версии API + if api_info.get("minVersion", 1) <= api["version"] <= api_info.get("maxVersion", 1): + filtered_apis.append(api) + logger.debug(f"Adding {api['name']} to available shutdown APIs") + else: + logger.debug(f"API {api['name']} version {api['version']} not supported. Supported: {api_info.get('minVersion')}-{api_info.get('maxVersion')}") + + if filtered_apis: + apis_to_try = filtered_apis + else: + logger.warning("No compatible APIs found, trying all methods as fallback") + else: + logger.warning("Failed to query API info, using default methods") + else: + logger.warning(f"API info request failed with status {api_info_response.status_code}, using default methods") + except Exception as e: + logger.warning(f"Error querying API info: {str(e)}, using default methods") + + # Пробуем все доступные методы по порядку + for api in apis_to_try: + logger.info(f"Trying shutdown with {api['name']}.{api['method']} v{api['version']}") + result = self._make_api_request(api["name"], api["method"], version=api["version"]) + + if result is not None: + logger.info(f"Successfully initiated system shutdown using {api['name']}") + + # Даем системе время начать процесс выключения + logger.info("Waiting for shutdown to initialize...") + sleep(5) + + # Проверяем статус + if not self.is_online(force_check=True): + logger.info("System is now offline. Shutdown confirmed successful.") + return True + else: + logger.info("System still appears to be online, but shutdown may be in progress.") + return True + else: + logger.warning(f"Failed to shutdown using {api['name']}.{api['method']} v{api['version']}") + + # Если ни один метод не сработал, но система стала недоступна + if not self.is_online(force_check=True): + logger.info("System appears to be shutting down despite API errors") + return True + + logger.error("Failed to shutdown system after trying multiple APIs") + return False + + def reboot_system(self) -> bool: + """Перезагрузка системы""" + # Проверяем, включена ли система + if not self.is_online(force_check=True): + logger.error("Cannot reboot: System is offline") + return False + + logger.info("Attempting to reboot Synology NAS...") + + # Попробуем сначала использовать предпочтительный API для управления питанием + logger.info(f"Trying reboot with {SYNOLOGY_POWER_API} v{SYNOLOGY_API_VERSION}") + + # Для SYNO.Core.Hardware.PowerRecovery используется метод setPowerOnState или powerButton + # Для других API обычно используется метод restart или reboot + if SYNOLOGY_POWER_API == "SYNO.Core.Hardware.PowerRecovery": + # Для этого API нужны специальные параметры + params = {"reboot": "true"} # Передаем флаг для перезагрузки + result = self._make_api_request(SYNOLOGY_POWER_API, "setPowerOnState", version=SYNOLOGY_API_VERSION, params=params) + else: + # Пробуем стандартный метод + methods_to_try = ["restart", "reboot"] + result = None + for method in methods_to_try: + result = self._make_api_request(SYNOLOGY_POWER_API, method, version=SYNOLOGY_API_VERSION) + if result is not None: + logger.info(f"Successfully used method {method} for {SYNOLOGY_POWER_API}") + break + + if result is not None: + logger.info(f"Successfully initiated system reboot using {SYNOLOGY_POWER_API}") + + # Даем системе время начать процесс перезагрузки + logger.info("Waiting for reboot to initialize...") + sleep(5) + + # Ждем, пока система станет недоступна (признак перезагрузки) + reboot_started = False + for i in range(12): # Проверяем в течение 60 секунд (12 * 5сек) + if not self.is_online(force_check=True): + logger.info(f"System went offline after {i*5} seconds, reboot in progress") + reboot_started = True + break + logger.debug(f"System still online, waiting... ({i+1}/12)") + sleep(5) + + if reboot_started: + # Ждем, пока система снова станет доступна + logger.info("Waiting for system to come back online...") + return self.wait_for_boot(max_attempts=30, delay=10) + else: + logger.warning("System did not go offline after reboot command, but command was accepted") + # Даже если система не ушла оффлайн, команда могла быть принята + return True + else: + logger.warning(f"Failed to reboot using {SYNOLOGY_POWER_API}, trying alternative methods") + + # Если не сработал основной метод, пробуем резервные варианты + # Проверка всех доступных методов API для перезагрузки + apis_to_try = [ + {"name": "SYNO.Core.System.Power", "method": "restart", "version": 1}, + {"name": "SYNO.DSM.Power", "method": "reboot", "version": 1}, + {"name": "SYNO.Core.System", "method": "reboot", "version": 3}, + {"name": "SYNO.System.Power", "method": "reboot", "version": 1}, + {"name": "SYNO.DSM.System", "method": "reboot", "version": 1}, + {"name": "SYNO.Core.Hardware.NeedReboot", "method": "reboot", "version": 1} + ] + + # Получаем список доступных API + try: + api_info_url = f"{self.base_url}/entry.cgi" + api_info_params = { + "api": "SYNO.API.Info", + "version": "1", + "method": "query", + "query": "SYNO.Core.System.Power,SYNO.DSM.Power,SYNO.Core.System,SYNO.System.Power,SYNO.DSM.System,SYNO.Core.Hardware.NeedReboot" + } + + logger.debug("Checking available reboot APIs") + api_info_response = self.session.get( + api_info_url, + params=api_info_params, + timeout=self.default_timeout, + verify=False + ) + + if api_info_response.status_code == 200: + api_info_data = api_info_response.json() + if api_info_data.get("success"): + available_apis = api_info_data.get("data", {}) + logger.debug(f"Available APIs: {list(available_apis.keys())}") + + # Фильтруем только доступные API + filtered_apis = [] + for api in apis_to_try: + if api["name"] in available_apis: + api_info = available_apis[api["name"]] + # Проверка версии API + if api_info.get("minVersion", 1) <= api["version"] <= api_info.get("maxVersion", 1): + filtered_apis.append(api) + logger.debug(f"Adding {api['name']} to available reboot APIs") + else: + logger.debug(f"API {api['name']} version {api['version']} not supported. Supported: {api_info.get('minVersion')}-{api_info.get('maxVersion')}") + + if filtered_apis: + apis_to_try = filtered_apis + else: + logger.warning("No compatible APIs found, trying all methods as fallback") + else: + logger.warning("Failed to query API info, using default methods") + else: + logger.warning(f"API info request failed with status {api_info_response.status_code}, using default methods") + except Exception as e: + logger.warning(f"Error querying API info: {str(e)}, using default methods") + + # Пробуем все доступные методы по порядку + for api in apis_to_try: + logger.info(f"Trying reboot with {api['name']}.{api['method']} v{api['version']}") + result = self._make_api_request(api["name"], api["method"], version=api["version"]) + + if result is not None: + logger.info(f"Successfully initiated system reboot using {api['name']}") + + # Даем системе время начать процесс перезагрузки + logger.info("Waiting for reboot to initialize...") + sleep(5) + + # Ждем, пока система станет недоступна (признак перезагрузки) + reboot_started = False + for i in range(12): # Проверяем в течение 60 секунд (12 * 5сек) + if not self.is_online(force_check=True): + logger.info(f"System went offline after {i*5} seconds, reboot in progress") + reboot_started = True + break + logger.debug(f"System still online, waiting... ({i+1}/12)") + sleep(5) + + if reboot_started: + # Ждем, пока система снова станет доступна + logger.info("Waiting for system to come back online...") + return self.wait_for_boot(max_attempts=30, delay=10) + else: + logger.warning("System did not go offline after reboot command, but command was accepted") + # Даже если система не ушла оффлайн, команда могла быть принята + return True + else: + logger.warning(f"Failed to reboot using {api['name']}.{api['method']} v{api['version']}") + + logger.error("Failed to reboot system after trying multiple APIs") + return False + + def _get_error_description(self, error_code: int) -> str: + """Получение описания ошибки по коду""" + error_descriptions = { + 100: "Unknown error", + 101: "Invalid parameter", + 102: "API does not exist", + 103: "Method does not exist", + 104: "Version does not support", + 105: "Permission denied", + 106: "Session timeout", + 107: "Session interrupted by duplicate login", + 400: "Invalid credentials", + 401: "Account disabled", + 402: "Permission denied", + 403: "2FA required", + 404: "Failed to authenticate with 2FA" + } + return error_descriptions.get(error_code, "Unknown error code") + + def _check_tcp_connection(self) -> bool: + """Проверка базового TCP-соединения с Synology NAS""" + try: + socket_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + socket_obj.settimeout(SYNOLOGY_TIMEOUT) + result = socket_obj.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT)) + socket_obj.close() + + return result == 0 + except socket.error as e: + logger.error(f"Socket error during connection check: {str(e)}") + return False + except Exception as e: + logger.error(f"Unexpected error during connection check: {str(e)}") + return False + + def is_online(self, force_check=False) -> bool: + """Проверка онлайн-статуса Synology NAS""" + # Используем кэшированное значение, если доступно и не устарело + current_time = time.time() + if not force_check and (current_time - self._last_online_check) < self._online_check_interval: + logger.debug(f"Using cached online status: {self._last_online_status}") + return self._last_online_status + + logger.info("Checking if NAS is online...") + + # Проверяем TCP-соединение + online_status = self._check_tcp_connection() + logger.info(f"Detected Synology NAS online status: {online_status}") + + # Если TCP-соединение успешно и у нас есть действующий SID, + # попробуем более детальную проверку через API + if online_status and self.sid: + logger.info("Trying to fetch more detailed online status through API...") + + # Пробуем разные API для проверки онлайн-статуса + api_checks = [ + {"api": "SYNO.DSM.Info", "version": "2", "method": "getinfo"}, + {"api": "SYNO.Core.System", "version": "1", "method": "info"}, + {"api": "SYNO.Core.System.Status", "version": "1", "method": "get"} + ] + + api_success = False + for api_check in api_checks: + try: + url = f"{self.base_url}/entry.cgi" + params = { + "api": api_check["api"], + "version": api_check["version"], + "method": api_check["method"], + "sid": self.sid + } + + logger.debug(f"Trying online status check with {api_check['api']}") + response = self.session.get( + url, + params=params, + timeout=self.default_timeout, + verify=False + ) + + if response.status_code == 200: + data = response.json() + if data.get("success"): + logger.info(f"API request successful for {api_check['api']}") + logger.info("Synology NAS is online with API access") + api_success = True + break + else: + error_code = data.get("error", {}).get("code", -1) + logger.warning(f"API response indicates an error: {error_code}, but NAS is reachable") + else: + logger.warning(f"API returned status code {response.status_code}") + except Exception as e: + logger.warning(f"API check failed for {api_check['api']}, but TCP connection succeeded: {str(e)}") + + if not api_success: + logger.warning("All API checks failed, but TCP connection is successful") + + # Обновляем кэшированное значение + self._last_online_check = current_time + self._last_online_status = online_status + + return online_status + + def wake_on_lan(self) -> bool: + """Отправка Wake-on-LAN пакета для включения Synology NAS""" + if not SYNOLOGY_MAC: + logger.error("MAC address not configured") + return False + + try: + # Преобразование MAC-адреса в байты + mac_address = SYNOLOGY_MAC.replace(':', '').replace('-', '') + if len(mac_address) != 12: + logger.error(f"Invalid MAC address format: {SYNOLOGY_MAC}") + return False + + try: + mac_bytes = bytes.fromhex(mac_address) + except ValueError as e: + logger.error(f"Failed to parse MAC address: {str(e)}") + logger.error(f"MAC address must contain only hex characters: {SYNOLOGY_MAC}") + return False + + # Создание Magic Packet + magic_packet = b'\xff' * 6 + mac_bytes * 16 + + # Отправка пакета на конкретный адрес + try: + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) + sock.sendto(magic_packet, (SYNOLOGY_HOST, WOL_PORT)) + sock.close() + logger.info(f"Wake-on-LAN packet sent to {SYNOLOGY_MAC} at {SYNOLOGY_HOST}:{WOL_PORT}") + except Exception as e: + logger.error(f"Error sending directed WoL packet: {str(e)}") + return False + + # Для надежности отправляем также широковещательный пакет + try: + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) + + # Используем стандартный широковещательный адрес + broadcast_addr = "255.255.255.255" + sock.sendto(magic_packet, (broadcast_addr, WOL_PORT)) + sock.close() + logger.info(f"Wake-on-LAN broadcast packet sent to {broadcast_addr}:{WOL_PORT}") + except Exception as e: + logger.warning(f"Error sending broadcast WoL packet (non-critical): {str(e)}") + # Не считаем ошибкой, т.к. основной пакет уже отправлен + + return True + + except Exception as e: + logger.error(f"Unexpected error in wake_on_lan: {str(e)}") + return False + + def wait_for_boot(self, max_attempts: int = 30, delay: int = 10) -> bool: + """Ожидание загрузки Synology NAS""" + logger.info(f"Waiting for Synology NAS to boot (timeout: {max_attempts * delay}s)...") + + for attempt in range(max_attempts): + # Принудительно проверяем статус без использования кэша + if self.is_online(force_check=True): + logger.info(f"Synology NAS is online after {attempt + 1} attempts ({(attempt + 1) * delay}s)") + + # Проверяем, что не только сеть доступна, но и API загрузился + api_ready = False + logger.info("Waiting for API services to initialize...") + + for api_check in range(5): # Даем еще до 50 секунд для загрузки API + if self.sid or self.login(): + api_ready = True + logger.info(f"API services are ready after {api_check + 1} attempts") + break + logger.debug(f"API not ready yet, waiting... ({api_check + 1}/5)") + sleep(10) + + if not api_ready: + logger.warning("System is online but API services may not be fully initialized") + + # Дадим дополнительное время для полной загрузки всех сервисов + sleep(delay) + return True + + sleep(delay) + logger.info(f"Waiting for boot... Attempt {attempt + 1}/{max_attempts}") + + logger.error(f"Synology NAS didn't come online after {max_attempts * delay} seconds") + return False + + def power_on(self) -> bool: + """Включение Synology NAS""" + # Принудительная проверка статуса + if self.is_online(force_check=True): + logger.info("Synology NAS is already online") + return True + + logger.info("Powering on Synology NAS via Wake-on-LAN...") + + # Проверяем, настроен ли MAC-адрес + if not SYNOLOGY_MAC: + logger.error("Cannot power on: MAC address not configured in settings") + return False + + # Пробуем отправить несколько WoL пакетов для надежности + success = False + for attempt in range(3): + logger.debug(f"Sending WoL packet, attempt {attempt + 1}/3") + if self.wake_on_lan(): + success = True + break + sleep(1) + + if not success: + logger.error("Failed to send Wake-on-LAN packets") + return False + + # Ожидание загрузки + logger.info("WoL packets sent successfully, waiting for system to boot...") + boot_result = self.wait_for_boot(max_attempts=30, delay=10) + + if boot_result: + # Проверяем доступность API после загрузки + system_status = self.get_system_status() + if system_status.get("status") == "online": + logger.info("System booted successfully with API access") + return True + else: + logger.warning("System appears to be online but API may not be fully ready") + return True + else: + logger.error("System did not come online after WoL") + return False + + def power_off(self) -> bool: + """Выключение Synology NAS""" + if not self.is_online(force_check=True): + logger.info("Synology NAS is already offline") + return True + + logger.info("Powering off Synology NAS...") + result = self.shutdown_system() + + if result: + # Дополнительная проверка, что система действительно выключилась + logger.info("Verifying system is offline...") + for attempt in range(12): # Проверяем в течение 2 минут (12 * 10 сек) + if not self.is_online(force_check=True): + logger.info(f"System confirmed offline after {attempt * 10} seconds") + return True + logger.debug(f"System still shutting down, checking again... ({attempt + 1}/12)") + sleep(10) + + logger.warning("System still appears to be online after shutdown command") + return False + else: + logger.error("Failed to initiate shutdown") + return False + + # Заглушки для расширенных методов + def get_shared_folders(self) -> List[Dict[str, Any]]: + """Получение списка общих папок""" + logger.info("Getting list of shared folders") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for shared folders request") + return [] + + try: + # Запрашиваем список общих папок через FileStation API + result = self._make_api_request("SYNO.FileStation.List", "list_share", version=2) + + if not result: + # Пробуем альтернативный API + logger.info("Trying alternative API for shared folders") + alt_result = self._make_api_request("SYNO.FileStation.List", "list_share", version=1) + if alt_result: + return alt_result.get("shares", []) + return [] + + return result.get("shares", []) + + except Exception as e: + logger.error(f"Error getting shared folders: {str(e)}") + return [] + + def get_system_load(self) -> Dict[str, Any]: + """Получение информации о загрузке системы""" + logger.info("Getting system load information") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for system load request") + return {} + + try: + # Запрашиваем информацию о загрузке системы + result = self._make_api_request("SYNO.Core.System.Utilization", "get", version=1) + + if not result: + # Пробуем альтернативный API + logger.info("Trying alternative API for system load") + alt_result = self._make_api_request("SYNO.Core.System", "info", version=3) + + if not alt_result: + return {} + + # Формируем из частичных данных + return { + "cpu_load": alt_result.get("cpu_usage", 0), + "memory": { + "total": alt_result.get("memory_size", 0), + "used": alt_result.get("memory_usage", 0), + "usage_percent": alt_result.get("memory_usage_percent", 0) + } + } + + # Формируем структурированный результат + return { + "cpu_load": result.get("cpu", {}).get("user", 0) + result.get("cpu", {}).get("system", 0), + "memory": result.get("memory", {}), + "network": result.get("network", {}) + } + + except Exception as e: + logger.error(f"Error getting system load: {str(e)}") + return {} + + def get_storage_status(self) -> Dict[str, Any]: + """Получение подробной информации о хранилище""" + logger.info("Getting storage status information") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for storage status request") + return {"error": "authentication_failed"} + + try: + # Пробуем получить информацию о хранилище через SYNO.Storage.CGI.Storage API + result = self._make_api_request("SYNO.Storage.CGI.Storage", "load_info", version=1) + + if not result: + # Пробуем альтернативный API + logger.info("Trying alternative API for storage info") + alt_result = self._make_api_request("SYNO.Core.Storage", "info", version=1) + + if not alt_result: + # Пробуем еще один альтернативный API + logger.info("Trying SYNO.Core.System API for storage info") + sys_result = self._make_api_request("SYNO.Core.System", "info", version=3) + + if not sys_result: + return { + "volumes": [], + "disks": [], + "total_size": 0, + "total_used": 0, + "error": "no_data" + } + + # Извлекаем базовую информацию о хранилище из системной информации + return { + "volumes": [], + "disks": [], + "total_size": sys_result.get("hdd_total_size", 0) * 1024**2, # МБ в байты + "total_used": sys_result.get("hdd_total_size", 0) * sys_result.get("hdd_usage", 0) / 100 * 1024**2, + } + + # Обрабатываем данные из альтернативного API + volumes = alt_result.get("volumes", []) + disks = alt_result.get("disks", []) + + else: + # Обрабатываем данные из основного API + volumes = result.get("volumes", []) + disks = result.get("disks", []) + + # Рассчитываем общие размеры + total_size = 0 + total_used = 0 + + for volume in volumes: + volume_size = volume.get("size", {}).get("total", 0) + volume_used = volume.get("size", {}).get("used", 0) + + total_size += volume_size + total_used += volume_used + + return { + "volumes": volumes, + "disks": disks, + "total_size": total_size, + "total_used": total_used + } + + except Exception as e: + logger.error(f"Error in get_storage_status: {str(e)}") + return {"error": str(e)} + + def get_security_status(self) -> Dict[str, Any]: + """Получение информации о состоянии безопасности""" + logger.info("Getting security status information") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for security status request") + return {"success": False, "error": "authentication_failed"} + + try: + # Пробуем получить информацию о безопасности через API Security Scan + result = self._make_api_request("SYNO.Core.SecurityScan.Status", "get", version=1) + + if not result: + # Пробуем альтернативный API + logger.info("Trying alternative API for security status") + # Проверяем статус брандмауэра + firewall_result = self._make_api_request("SYNO.Core.Security.Firewall.Status", "get", version=1) + + # Проверяем статус автоматических обновлений + update_result = self._make_api_request("SYNO.Core.Upgrade.Server.Setting", "get", version=1) + + # Если ни один из API не отвечает + if not firewall_result and not update_result: + # Получаем общую информацию о системе для базовой проверки безопасности + sys_result = self._make_api_request("SYNO.DSM.Info", "getinfo", version=2) + + if not sys_result: + return { + "success": False, + "status": "unknown", + "last_check": None, + "is_secure": False, + "error": "no_security_api" + } + + # Собираем базовые сведения из системной информации + return { + "success": True, + "status": "basic", + "last_check": None, + "is_secure": True, # Предполагаем, что система в целом безопасна + "firewall_enabled": None, + "auto_update": None, + "version_latest": sys_result.get("version_string", "") + } + + # Собираем информацию из доступных результатов + firewall_enabled = firewall_result.get("enabled", False) if firewall_result else None + auto_update = update_result.get("auto_update", False) if update_result else None + + # Определяем, насколько система безопасна + is_secure = True # По умолчанию предполагаем, что система безопасна + if firewall_enabled is not None and not firewall_enabled: + is_secure = False + + return { + "success": True, + "status": "partial", + "last_check": None, + "is_secure": is_secure, + "firewall_enabled": firewall_enabled, + "auto_update": auto_update + } + + # Если основное API отвечает, возвращаем его данные + return { + "success": True, + "status": result.get("status", "unknown"), + "last_check": result.get("last_check", None), + "is_secure": result.get("is_secure", False), + "details": result.get("details", {}) + } + + except Exception as e: + logger.error(f"Error in get_security_status: {str(e)}") + return {"success": False, "error": str(e)} + + def get_processes(self, limit: int = 20) -> List[Dict[str, Any]]: + """Получение списка активных процессов""" + logger.info(f"Getting list of active processes (limit={limit})") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for processes request") + return [] + + try: + # Получаем список процессов через API + result = self._make_api_request("SYNO.Core.System.Process", "list", version=1, + params={"sort_by": "cpu", "order": "DESC", "limit": limit}) + + if not result: + logger.warning("Failed to get process list") + return [] + + return result.get("processes", []) + + except Exception as e: + logger.error(f"Error getting process list: {str(e)}") + return [] + + def get_network_status(self) -> Dict[str, Any]: + """Получение информации о сетевых подключениях""" + logger.info("Getting network status information") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for network status request") + return {} + + try: + # Получаем информацию о сетевых интерфейсах + interface_result = self._make_api_request("SYNO.Core.Network.Interface", "list", version=1) + + # Получаем статистику использования сети + utilization_result = self._make_api_request("SYNO.Core.System.Utilization", "get", version=1) + + interfaces = [] + if interface_result: + interfaces = interface_result.get("interfaces", []) + + network_stats = {} + if utilization_result and "network" in utilization_result: + network_stats = utilization_result.get("network", {}) + + # Объединяем данные + for interface in interfaces: + iface_id = interface.get("id", "") + if iface_id in network_stats: + interface["rx"] = network_stats[iface_id].get("rx", 0) + interface["tx"] = network_stats[iface_id].get("tx", 0) + + return { + "interfaces": interfaces, + "statistics": network_stats + } + + except Exception as e: + logger.error(f"Error getting network status: {str(e)}") + return {} + + def get_system_logs(self, limit: int = 20) -> List[Dict[str, Any]]: + """Получение журналов системы""" + logger.info(f"Getting system logs (limit={limit})") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for system logs request") + return [] + + try: + # Получаем журналы через API + result = self._make_api_request("SYNO.Core.SyslogClient.Log", "list", version=1, + params={"start": 0, "limit": limit, "sort": "time", "dir": "DESC"}) + + if not result: + # Пробуем альтернативный API + logger.info("Trying alternative API for system logs") + alt_result = self._make_api_request("SYNO.Core.Log", "list", version=1, + params={"start": 0, "limit": limit, "sort": "time", "dir": "DESC"}) + + if alt_result: + return alt_result.get("logs", []) + return [] + + return result.get("logs", []) + + except Exception as e: + logger.error(f"Error getting system logs: {str(e)}") + return [] + + def get_power_schedule(self) -> Dict[str, Any]: + """Получение расписания включения/выключения""" + logger.info("Getting power schedule") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for power schedule request") + return {} + + try: + # Получаем расписание через API + result = self._make_api_request("SYNO.Core.Hardware.PowerSchedule", "get", version=1) + + if not result: + return {} + + return result + + except Exception as e: + logger.error(f"Error getting power schedule: {str(e)}") + return {} diff --git a/.history/src/api/synology_20250830091124.py b/.history/src/api/synology_20250830091124.py new file mode 100644 index 0000000..0517d6f --- /dev/null +++ b/.history/src/api/synology_20250830091124.py @@ -0,0 +1,1773 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Модуль для взаимодействия с API Synology NAS +""" + +import requests +from requests.adapters import HTTPAdapter +import json +import logging +import time +import urllib3 +from urllib3.util import Retry +from typing import Dict, Any, Optional, List +import socket +import struct +from time import sleep + +from src.config.config import ( + SYNOLOGY_HOST, + SYNOLOGY_PORT, + SYNOLOGY_USERNAME, + SYNOLOGY_PASSWORD, + SYNOLOGY_SECURE, + SYNOLOGY_TIMEOUT, + SYNOLOGY_MAC, + WOL_PORT, + SYNOLOGY_API_VERSION, + SYNOLOGY_POWER_API, + SYNOLOGY_INFO_API +) +from src.api.api_discovery import discover_available_apis, find_compatible_api + +# Отключение предупреждений о небезопасных SSL-соединениях +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + +logger = logging.getLogger(__name__) + +class SynologyAPI: + """Класс для взаимодействия с API Synology NAS""" + + def __init__(self): + """Инициализация класса SynologyAPI""" + logger.info("Creating API with auto-retry and connection pool") + logger.debug(f"Connection details: Host={SYNOLOGY_HOST}, Port={SYNOLOGY_PORT}, Secure={SYNOLOGY_SECURE}") + + self.protocol = "https" if SYNOLOGY_SECURE else "http" + self.base_url = f"{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi" + self.sid = None + self.session = requests.Session() + + # Настройка SSL + if self.protocol == "https": + logger.debug("SSL enabled, disabling certificate verification for internal network") + self.session.verify = False # Отключаем проверку SSL для внутренней сети + + # Добавляем пользовательские заголовки для улучшения совместимости с API + custom_headers = { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36', + 'Accept': 'application/json, text/javascript, */*; q=0.01', + 'Accept-Language': 'ru-RU,ru;q=0.9,en-US;q=0.8,en;q=0.7', + 'X-Requested-With': 'XMLHttpRequest', + 'Connection': 'keep-alive', + 'Referer': f'{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/' + } + self.session.headers.update(custom_headers) + logger.debug("Added browser-like headers for API compatibility") + + # Добавляем повторные попытки для HTTP-запросов + retry_strategy = Retry( + total=5, # Увеличиваем количество попыток + status_forcelist=[429, 500, 502, 503, 504, 404], + allowed_methods=["GET", "POST"], + backoff_factor=1.5, # Увеличиваем задержку между попытками + respect_retry_after_header=True + ) + adapter = HTTPAdapter( + max_retries=retry_strategy, + pool_connections=3, + pool_maxsize=10 + ) + self.session.mount("http://", adapter) + self.session.mount("https://", adapter) + + # Таймауты будут указаны в запросах + self.default_timeout = (SYNOLOGY_TIMEOUT, SYNOLOGY_TIMEOUT*2) + logger.debug(f"Setting default request timeout: {self.default_timeout}") + + # Кэш для хранения результатов запросов + self._cache = {} + self._cache_ttl = {} + self._last_online_check = 0 + self._last_online_status = False + self._online_check_interval = 30 # Интервал для кэширования проверок онлайн-статуса + + # Время последней успешной аутентификации и срок действия сессии + self._last_auth_time = 0 + self._auth_expiry = 3600 # По умолчанию 1 час + + # Информация о доступных API + self._available_apis = {} + self._api_info_ttl = 0 + + # Инициализируем API version resolver для автоматического определения совместимых API + self.api_resolver = None # Будет создан при необходимости + + def login(self) -> bool: + """Авторизация в API Synology NAS""" + # Сбрасываем SID для новой сессии + self.sid = None + + logger.info("Attempting to authenticate with Synology NAS...") + logger.debug(f"Base URL: {self.base_url}") + + # Проверяем доступность NAS перед запросом с помощью прямой TCP-проверки + # Избегаем вызова is_online(), чтобы не создавать рекурсию + online_status = self._check_tcp_connection() + if not online_status: + logger.error("Cannot login: Synology NAS is not reachable") + return False + + # Пробуем различные версии API для аутентификации + # Начинаем с версии 3, которая показала лучшую совместимость в тестах + auth_versions_to_try = [3, 2, 1, 6] # Пробуем сначала более стабильные версии + + for auth_version in auth_versions_to_try: + try: + # Определяем путь к API аутентификации + auth_path = "entry.cgi" # Используем entry.cgi вместо auth.cgi по умолчанию + + # Проверка информации API для определения доступных версий API + api_info_url = f"{self.base_url}/entry.cgi" + api_info_params = { + "api": "SYNO.API.Info", + "version": "1", + "method": "query", + "query": "SYNO.API.Auth" + } + + logger.debug(f"Querying API info for auth version {auth_version}") + try: + api_info_response = self.session.get( + api_info_url, + params=api_info_params, + timeout=self.default_timeout, + verify=False + ) + + if api_info_response.status_code == 200: + api_info_data = api_info_response.json() + if api_info_data.get("success"): + auth_info = api_info_data.get("data", {}).get("SYNO.API.Auth", {}) + max_version = auth_info.get("maxVersion", 6) + min_version = auth_info.get("minVersion", 1) + auth_path = auth_info.get("path", "entry.cgi") + logger.debug(f"Auth API versions: min={min_version}, max={max_version}, path={auth_path}") + + # Проверяем поддержку текущей версии + if auth_version < min_version or auth_version > max_version: + logger.warning(f"Auth API version {auth_version} not supported (min={min_version}, max={max_version}), trying next version") + continue + else: + logger.warning("Failed to query API info, using default auth path") + else: + logger.warning(f"API info request failed with status {api_info_response.status_code}, using default auth path") + except Exception as e: + logger.warning(f"Error querying API info: {str(e)}, using default auth path") + + # Основной запрос авторизации + url = f"{self.base_url}/{auth_path}" + params = { + "api": "SYNO.API.Auth", + "version": str(auth_version), + "method": "login", + "account": SYNOLOGY_USERNAME, + "passwd": SYNOLOGY_PASSWORD, + "session": "SynologyPowerControlBot", + "format": "cookie" + } + + # Для версии 6+ используем немного другой формат + if auth_version >= 6: + params["enable_syno_token"] = "yes" + + logger.debug(f"Sending auth request to {url} with API version {auth_version}") + start_time = time.time() + response = self.session.get( + url, + params=params, + timeout=self.default_timeout, + verify=False + ) + elapsed_time = time.time() - start_time + logger.debug(f"Auth request completed in {elapsed_time:.2f}s with status code {response.status_code}") + + # Проверка статуса HTTP + if response.status_code != 200: + logger.error(f"HTTP error: {response.status_code}") + logger.debug(f"Response content: {response.text[:200]}") + continue # Пробуем следующую версию + + try: + data = response.json() + except json.JSONDecodeError: + logger.error("Failed to decode JSON response") + logger.debug(f"Response content: {response.text[:200]}") + continue # Пробуем следующую версию + + if data.get("success"): + self.sid = data.get("data", {}).get("sid") + self._last_auth_time = time.time() + logger.info(f"Successfully logged in to Synology NAS using API version {auth_version}") + logger.debug(f"Session ID obtained: {self.sid[:5] if self.sid else 'None'}...") + + # Получаем и сохраняем токен SYNO, если он есть + syno_token = data.get("data", {}).get("synotoken") + if syno_token: + self.session.headers.update({'X-SYNO-TOKEN': syno_token}) + logger.debug("Added X-SYNO-TOKEN header for improved API compatibility") + + # Также добавляем SID в cookies для улучшения совместимости + self.session.cookies.update({ + 'id': self.sid, + 'sid': self.sid + }) + logger.debug("Added SID to session cookies for improved compatibility") + + # Проверка валидности полученной сессии с помощью простого запроса + # Будем использовать SYNO.API.Info без проверки сложных методов + + # Даем системе немного времени для инициализации сессии + time.sleep(0.5) + + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to log in with version {auth_version}: Error code {error_code}") + + # Если ошибка связана с версией API, пробуем следующую версию + if error_code in [104, 105]: + logger.warning(f"Auth version {auth_version} not supported, trying next version") + continue + + # Дополнительная диагностика + if error_code == 400: + logger.error("Authentication error: Invalid credentials") + elif error_code == 401: + logger.error("Authentication error: Account disabled") + elif error_code == 402: + logger.error("Authentication error: Permission denied") + elif error_code == 403: + logger.error("Authentication error: 2-factor authentication required") + elif error_code == 404: + logger.error("Authentication error: Failed to authenticate with 2-factor authentication") + + # Если ошибка связана с неверными учетными данными, нет смысла пробовать другие версии API + if error_code in [400, 401, 402, 403, 404]: + return False + + except requests.exceptions.Timeout: + logger.error(f"Connection timeout during auth with version {auth_version}") + continue # Пробуем следующую версию + except requests.exceptions.ConnectionError as e: + logger.error(f"Connection error during auth with version {auth_version}: {str(e)}") + continue # Пробуем следующую версию + except requests.RequestException as e: + logger.error(f"Request error during auth with version {auth_version}: {str(e)}") + continue # Пробуем следующую версию + except Exception as e: + logger.error(f"Unexpected error during login with version {auth_version}: {str(e)}", exc_info=True) + continue # Пробуем следующую версию + + # Если все версии не сработали + logger.error("Failed to authenticate with any API version") + return False + + def _validate_session(self) -> bool: + """Проверяет валидность сессии после авторизации""" + if not self.sid: + return False + + # Попробуем сделать простой запрос для проверки сессии + test_apis = [ + {"api": "SYNO.Core.System", "method": "info", "version": 1}, + {"api": "SYNO.DSM.Info", "method": "getinfo", "version": 1} + ] + + for test_api in test_apis: + try: + url = f"{self.base_url}/entry.cgi" + params = { + "api": test_api["api"], + "version": str(test_api["version"]), + "method": test_api["method"], + "_sid": self.sid + } + + response = self.session.get(url, params=params, timeout=self.default_timeout, verify=False) + + if response.status_code == 200: + data = response.json() + if data.get("success"): + logger.debug(f"Session validation successful using {test_api['api']}") + return True + else: + error_code = data.get("error", {}).get("code", -1) + if error_code != 119: # Не сессия истекла + logger.debug(f"Session validation with {test_api['api']} returned error code {error_code}") + return True # Считаем сессию валидной, если ошибка не связана с истечением сессии + except Exception as e: + logger.warning(f"Error during session validation with {test_api['api']}: {str(e)}") + + logger.warning("Session validation failed with all test APIs") + return False + + def logout(self) -> bool: + """Выход из API Synology NAS""" + if not self.sid: + return True + + try: + url = f"{self.base_url}/auth.cgi" + params = { + "api": "SYNO.API.Auth", + "version": "1", + "method": "logout", + "session": "SynologyPowerControlBot", + "_sid": self.sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + self.sid = None + logger.info("Successfully logged out from Synology NAS") + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to log out from Synology NAS: Error code {error_code}") + return False + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + return False + + def _make_api_request(self, api_name: str, method: str, version: int = 1, + params: Optional[Dict[str, Any]] = None, retry_auth: bool = True, retry_count: int = 0) -> Optional[Dict[str, Any]]: + """Обобщенный метод для выполнения API запросов с обработкой ошибок""" + # Ограничение на количество повторных попыток + if retry_count >= 3: + logger.error(f"Too many retries for {api_name}.{method}, giving up") + return None + + # Проверка наличия авторизации + if not self.sid and not self.login(): + logger.error(f"Not authenticated for API request: {api_name}.{method}") + return None + + # Проверка информации API для определения пути и поддерживаемой версии + api_info_url = f"{self.base_url}/entry.cgi" + api_info_params = { + "api": "SYNO.API.Info", + "version": "1", + "method": "query", + "query": api_name + } + + api_path = "entry.cgi" + try: + logger.debug(f"Querying API info for {api_name}") + api_info_response = self.session.get( + api_info_url, + params=api_info_params, + timeout=self.default_timeout, + verify=False + ) + + if api_info_response.status_code == 200: + api_info_data = api_info_response.json() + if api_info_data.get("success"): + api_info = api_info_data.get("data", {}).get(api_name, {}) + if api_info: + max_version = api_info.get("maxVersion", version) + min_version = api_info.get("minVersion", version) + api_path = api_info.get("path", "entry.cgi") + + # Проверка, поддерживается ли запрошенная версия + if version < min_version: + logger.warning(f"API version {version} for {api_name} is below minimum {min_version}, using {min_version}") + version = min_version + elif version > max_version: + logger.warning(f"API version {version} for {api_name} exceeds maximum {max_version}, using {max_version}") + version = max_version + + logger.debug(f"Using API path: {api_path}, version: {version}") + else: + logger.warning(f"API {api_name} not found in API info, using defaults") + except Exception as e: + logger.warning(f"Error querying API info for {api_name}: {str(e)}, using defaults") + + # Подготовка базовых параметров запроса + base_params = { + "api": api_name, + "version": str(version), + "method": method, + "_sid": self.sid # Используем _sid вместо sid для лучшей совместимости + } + + # Добавление дополнительных параметров, если они заданы + if params: + base_params.update(params) + + url = f"{self.base_url}/{api_path}" + logger.debug(f"API request: {api_name}.{method} v{version} to {url}") + logger.debug(f"Full request params: {base_params}") + + try: + start_time = time.time() + response = self.session.get( + url, + params=base_params, + timeout=self.default_timeout, + verify=False + ) + elapsed_time = time.time() - start_time + logger.debug(f"API request completed in {elapsed_time:.2f}s with status {response.status_code}") + + # Проверка статуса HTTP + if response.status_code != 200: + logger.error(f"HTTP error: {response.status_code} for {api_name}.{method}") + + # Повторная попытка при ошибках соединения + if response.status_code in [500, 502, 503, 504]: + logger.info(f"Server error, retrying request for {api_name}.{method}") + sleep(2) + return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1) + + return None + + try: + data = response.json() + except json.JSONDecodeError: + logger.error(f"Failed to decode JSON response for {api_name}.{method}") + logger.debug(f"Response content: {response.text[:200]}") + + # Повторная попытка при ошибках декодирования + logger.info(f"JSON decode error, retrying request for {api_name}.{method}") + sleep(2) + return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1) + + if data.get("success"): + logger.info(f"API request successful for {api_name}.{method}") + return data.get("data", {}) + else: + error_code = data.get("error", {}).get("code", -1) + error_desc = self._get_error_description(error_code) + logger.error(f"API error for {api_name}.{method}: {error_code} - {error_desc}") + + # Ошибки доступа или прав часто встречаются, но они не критичные + # Например, ошибка 102 означает, что нет прав, но NAS доступен + if error_code in [102, 103, 104, 105]: + logger.warning(f"Permission-related error for {api_name}.{method}: {error_code}") + # Возвращаем пустой словарь вместо None, + # чтобы вызывающий код мог понять, что запрос выполнен + return {} + + # Если ошибка связана с авторизацией и нам разрешено повторить попытку + if error_code in [106, 107, 119] and retry_auth: + logger.info(f"Session error (code {error_code}), creating fresh session...") + self.sid = None # Сбрасываем SID + + # Для ошибки 119 (Session timeout) дадим системе немного времени + if error_code == 119: + logger.info("Session timeout detected, waiting before retry...") + sleep(3) + + if self.login(): + logger.info("Re-authenticated with fresh session, retrying API request...") + # Рекурсивный вызов, но со счетчиком повторов + return self._make_api_request(api_name, method, version, params, False, retry_count + 1) + + # Для некоторых ошибок можно автоматически повторить запрос + if error_code in [408, 429, 500, 502, 503, 504]: + logger.info(f"Temporary error {error_code}, retrying request for {api_name}.{method}") + sleep(2) + return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1) + + return None + + except requests.exceptions.Timeout: + logger.error(f"Request timeout for {api_name}.{method}") + + # Повторная попытка при таймауте + if retry_count < 2: + logger.info(f"Timeout, retrying request for {api_name}.{method}") + sleep(2) + return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1) + + return None + except requests.exceptions.ConnectionError as e: + logger.error(f"Connection error for {api_name}.{method}: {str(e)}") + + # Повторная попытка при ошибке соединения + if retry_count < 2: + logger.info(f"Connection error, retrying request for {api_name}.{method}") + sleep(2) + return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1) + + return None + except Exception as e: + logger.error(f"Unexpected error during {api_name}.{method}: {str(e)}") + return None + + def get_system_status(self) -> Dict[str, Any]: + """Получение статуса системы""" + # Проверяем доступность системы + if not self.is_online(): + logger.info("Device is offline, skipping API request") + return {"status": "offline"} + + # Проверяем, есть ли кэшированный результат + cache_key = "system_status" + current_time = time.time() + if cache_key in self._cache and current_time - self._cache_ttl.get(cache_key, 0) < 60: + logger.debug("Using cached system status") + return self._cache[cache_key] + + # Используем рекомендованный API для получения информации о системе + logger.info(f"Getting system info using {SYNOLOGY_INFO_API} v{SYNOLOGY_API_VERSION}") + + # Для SYNO.DSM.Info метод getinfo, для других обычно используется get или info + if SYNOLOGY_INFO_API == "SYNO.DSM.Info": + method = "getinfo" + else: + method = "get" + + result = self._make_api_request(SYNOLOGY_INFO_API, method, version=SYNOLOGY_API_VERSION) + + if result: + logger.info(f"Successfully retrieved system info using {SYNOLOGY_INFO_API}") + + # Формируем расширенный ответ с дополнительной информацией + system_info = { + "status": "online", + "hostname": result.get("hostname", "unknown"), + "model": result.get("model", "unknown"), + "version": result.get("version", "unknown"), + "uptime": result.get("uptime", 0), + "time": current_time, + "is_online": True, + "api_used": SYNOLOGY_INFO_API + } + + # Сохраняем в кэше + self._cache[cache_key] = system_info + self._cache_ttl[cache_key] = current_time + + return system_info + + # Если основной API не сработал, пробуем резервные варианты + logger.warning(f"Failed to retrieve system info with {SYNOLOGY_INFO_API}, trying fallback APIs") + + # Пробуем резервные API + apis_to_try = [ + {"name": "SYNO.DSM.Info", "method": "getinfo", "version": 2}, + {"name": "SYNO.Core.System", "method": "info", "version": 1}, + {"name": "SYNO.Core.System.Status", "method": "get", "version": 1}, + {"name": "SYNO.Core.System.Info", "method": "get", "version": 1}, + ] + + for api in apis_to_try: + if api["name"] == SYNOLOGY_INFO_API: + continue # Пропускаем уже проверенный API + + logger.info(f"Trying to get system info using {api['name']}.{api['method']} v{api['version']}") + result = self._make_api_request(api["name"], api["method"], version=api["version"]) + + if result: + logger.info(f"Successfully retrieved system info using {api['name']}") + + # Формируем расширенный ответ с дополнительной информацией + system_info = { + "status": "online", + "hostname": result.get("hostname", "unknown"), + "model": result.get("model", "unknown"), + "version": result.get("version", "unknown"), + "uptime": result.get("uptime", 0), + "time": current_time, + "is_online": True, + "api_used": api["name"] + } + + # Сохраняем в кэше + self._cache[cache_key] = system_info + self._cache_ttl[cache_key] = current_time + + return system_info + + # Если все запросы не удались, но система онлайн, возвращаем базовую информацию + logger.warning("Failed to retrieve system info with all API methods") + return { + "status": "error", + "error": "Failed to fetch system information", + "is_online": True, + "time": current_time + } + + def shutdown_system(self) -> bool: + """Выключение системы""" + # Проверяем, включено ли устройство перед попыткой его выключить + if not self.is_online(force_check=True): + logger.info("Synology NAS is already offline, no need to shut down") + return True + + logger.info("Attempting to shutdown Synology NAS...") + + # Попробуем сначала использовать предпочтительный API для управления питанием + logger.info(f"Trying shutdown with {SYNOLOGY_POWER_API} v{SYNOLOGY_API_VERSION}") + + # Для SYNO.Core.Hardware.PowerRecovery используется метод setPowerOnState + # Для других API обычно используется метод shutdown или reboot + if SYNOLOGY_POWER_API == "SYNO.Core.Hardware.PowerRecovery": + # Для этого API нужны специальные параметры + params = {"state": "powerbtn"} # powerbtn имитирует нажатие кнопки питания + result = self._make_api_request(SYNOLOGY_POWER_API, "setPowerOnState", version=SYNOLOGY_API_VERSION, params=params) + else: + # Пробуем стандартный метод + result = self._make_api_request(SYNOLOGY_POWER_API, "shutdown", version=SYNOLOGY_API_VERSION) + + if result is not None: + logger.info(f"Successfully initiated system shutdown using {SYNOLOGY_POWER_API}") + + # Даем системе время начать процесс выключения + logger.info("Waiting for shutdown to initialize...") + sleep(5) + + # Проверяем статус + if not self.is_online(force_check=True): + logger.info("System is now offline. Shutdown confirmed successful.") + return True + else: + logger.info("System still appears to be online, but shutdown may be in progress.") + return True + else: + logger.warning(f"Failed to shutdown using {SYNOLOGY_POWER_API}, trying alternative methods") + + # Если не сработал основной метод, пробуем резервные варианты + # Проверка всех доступных методов API для выключения + apis_to_try = [ + {"name": "SYNO.Core.System", "method": "shutdown", "version": 3}, + {"name": "SYNO.Core.System.Power", "method": "shutdown", "version": 1}, + {"name": "SYNO.DSM.Power", "method": "shutdown", "version": 1}, + {"name": "SYNO.System.Power", "method": "shutdown", "version": 1}, + {"name": "SYNO.DSM.System", "method": "shutdown", "version": 1} + ] + + # Проверяем доступные API + try: + api_info_url = f"{self.base_url}/entry.cgi" + api_info_params = { + "api": "SYNO.API.Info", + "version": "1", + "method": "query", + "query": "SYNO.Core.System.Power,SYNO.DSM.Power,SYNO.Core.System,SYNO.System.Power,SYNO.DSM.System" + } + + logger.debug("Checking available shutdown APIs") + api_info_response = self.session.get( + api_info_url, + params=api_info_params, + timeout=self.default_timeout, + verify=False + ) + + if api_info_response.status_code == 200: + api_info_data = api_info_response.json() + if api_info_data.get("success"): + available_apis = api_info_data.get("data", {}) + logger.debug(f"Available APIs: {list(available_apis.keys())}") + + # Фильтруем только доступные API + filtered_apis = [] + for api in apis_to_try: + if api["name"] in available_apis: + api_info = available_apis[api["name"]] + # Проверка версии API + if api_info.get("minVersion", 1) <= api["version"] <= api_info.get("maxVersion", 1): + filtered_apis.append(api) + logger.debug(f"Adding {api['name']} to available shutdown APIs") + else: + logger.debug(f"API {api['name']} version {api['version']} not supported. Supported: {api_info.get('minVersion')}-{api_info.get('maxVersion')}") + + if filtered_apis: + apis_to_try = filtered_apis + else: + logger.warning("No compatible APIs found, trying all methods as fallback") + else: + logger.warning("Failed to query API info, using default methods") + else: + logger.warning(f"API info request failed with status {api_info_response.status_code}, using default methods") + except Exception as e: + logger.warning(f"Error querying API info: {str(e)}, using default methods") + + # Пробуем все доступные методы по порядку + for api in apis_to_try: + logger.info(f"Trying shutdown with {api['name']}.{api['method']} v{api['version']}") + result = self._make_api_request(api["name"], api["method"], version=api["version"]) + + if result is not None: + logger.info(f"Successfully initiated system shutdown using {api['name']}") + + # Даем системе время начать процесс выключения + logger.info("Waiting for shutdown to initialize...") + sleep(5) + + # Проверяем статус + if not self.is_online(force_check=True): + logger.info("System is now offline. Shutdown confirmed successful.") + return True + else: + logger.info("System still appears to be online, but shutdown may be in progress.") + return True + else: + logger.warning(f"Failed to shutdown using {api['name']}.{api['method']} v{api['version']}") + + # Если ни один метод не сработал, но система стала недоступна + if not self.is_online(force_check=True): + logger.info("System appears to be shutting down despite API errors") + return True + + logger.error("Failed to shutdown system after trying multiple APIs") + return False + + def reboot_system(self) -> bool: + """Перезагрузка системы""" + # Проверяем, включена ли система + if not self.is_online(force_check=True): + logger.error("Cannot reboot: System is offline") + return False + + logger.info("Attempting to reboot Synology NAS...") + + # Попробуем сначала использовать предпочтительный API для управления питанием + logger.info(f"Trying reboot with {SYNOLOGY_POWER_API} v{SYNOLOGY_API_VERSION}") + + # Для SYNO.Core.Hardware.PowerRecovery используется метод setPowerOnState или powerButton + # Для других API обычно используется метод restart или reboot + if SYNOLOGY_POWER_API == "SYNO.Core.Hardware.PowerRecovery": + # Для этого API нужны специальные параметры + params = {"reboot": "true"} # Передаем флаг для перезагрузки + result = self._make_api_request(SYNOLOGY_POWER_API, "setPowerOnState", version=SYNOLOGY_API_VERSION, params=params) + else: + # Пробуем стандартный метод + methods_to_try = ["restart", "reboot"] + result = None + for method in methods_to_try: + result = self._make_api_request(SYNOLOGY_POWER_API, method, version=SYNOLOGY_API_VERSION) + if result is not None: + logger.info(f"Successfully used method {method} for {SYNOLOGY_POWER_API}") + break + + if result is not None: + logger.info(f"Successfully initiated system reboot using {SYNOLOGY_POWER_API}") + + # Даем системе время начать процесс перезагрузки + logger.info("Waiting for reboot to initialize...") + sleep(5) + + # Ждем, пока система станет недоступна (признак перезагрузки) + reboot_started = False + for i in range(12): # Проверяем в течение 60 секунд (12 * 5сек) + if not self.is_online(force_check=True): + logger.info(f"System went offline after {i*5} seconds, reboot in progress") + reboot_started = True + break + logger.debug(f"System still online, waiting... ({i+1}/12)") + sleep(5) + + if reboot_started: + # Ждем, пока система снова станет доступна + logger.info("Waiting for system to come back online...") + return self.wait_for_boot(max_attempts=30, delay=10) + else: + logger.warning("System did not go offline after reboot command, but command was accepted") + # Даже если система не ушла оффлайн, команда могла быть принята + return True + else: + logger.warning(f"Failed to reboot using {SYNOLOGY_POWER_API}, trying alternative methods") + + # Если не сработал основной метод, пробуем резервные варианты + # Проверка всех доступных методов API для перезагрузки + apis_to_try = [ + {"name": "SYNO.Core.System.Power", "method": "restart", "version": 1}, + {"name": "SYNO.DSM.Power", "method": "reboot", "version": 1}, + {"name": "SYNO.Core.System", "method": "reboot", "version": 3}, + {"name": "SYNO.System.Power", "method": "reboot", "version": 1}, + {"name": "SYNO.DSM.System", "method": "reboot", "version": 1}, + {"name": "SYNO.Core.Hardware.NeedReboot", "method": "reboot", "version": 1} + ] + + # Получаем список доступных API + try: + api_info_url = f"{self.base_url}/entry.cgi" + api_info_params = { + "api": "SYNO.API.Info", + "version": "1", + "method": "query", + "query": "SYNO.Core.System.Power,SYNO.DSM.Power,SYNO.Core.System,SYNO.System.Power,SYNO.DSM.System,SYNO.Core.Hardware.NeedReboot" + } + + logger.debug("Checking available reboot APIs") + api_info_response = self.session.get( + api_info_url, + params=api_info_params, + timeout=self.default_timeout, + verify=False + ) + + if api_info_response.status_code == 200: + api_info_data = api_info_response.json() + if api_info_data.get("success"): + available_apis = api_info_data.get("data", {}) + logger.debug(f"Available APIs: {list(available_apis.keys())}") + + # Фильтруем только доступные API + filtered_apis = [] + for api in apis_to_try: + if api["name"] in available_apis: + api_info = available_apis[api["name"]] + # Проверка версии API + if api_info.get("minVersion", 1) <= api["version"] <= api_info.get("maxVersion", 1): + filtered_apis.append(api) + logger.debug(f"Adding {api['name']} to available reboot APIs") + else: + logger.debug(f"API {api['name']} version {api['version']} not supported. Supported: {api_info.get('minVersion')}-{api_info.get('maxVersion')}") + + if filtered_apis: + apis_to_try = filtered_apis + else: + logger.warning("No compatible APIs found, trying all methods as fallback") + else: + logger.warning("Failed to query API info, using default methods") + else: + logger.warning(f"API info request failed with status {api_info_response.status_code}, using default methods") + except Exception as e: + logger.warning(f"Error querying API info: {str(e)}, using default methods") + + # Пробуем все доступные методы по порядку + for api in apis_to_try: + logger.info(f"Trying reboot with {api['name']}.{api['method']} v{api['version']}") + result = self._make_api_request(api["name"], api["method"], version=api["version"]) + + if result is not None: + logger.info(f"Successfully initiated system reboot using {api['name']}") + + # Даем системе время начать процесс перезагрузки + logger.info("Waiting for reboot to initialize...") + sleep(5) + + # Ждем, пока система станет недоступна (признак перезагрузки) + reboot_started = False + for i in range(12): # Проверяем в течение 60 секунд (12 * 5сек) + if not self.is_online(force_check=True): + logger.info(f"System went offline after {i*5} seconds, reboot in progress") + reboot_started = True + break + logger.debug(f"System still online, waiting... ({i+1}/12)") + sleep(5) + + if reboot_started: + # Ждем, пока система снова станет доступна + logger.info("Waiting for system to come back online...") + return self.wait_for_boot(max_attempts=30, delay=10) + else: + logger.warning("System did not go offline after reboot command, but command was accepted") + # Даже если система не ушла оффлайн, команда могла быть принята + return True + else: + logger.warning(f"Failed to reboot using {api['name']}.{api['method']} v{api['version']}") + + logger.error("Failed to reboot system after trying multiple APIs") + return False + + def _get_error_description(self, error_code: int) -> str: + """Получение описания ошибки по коду""" + error_descriptions = { + 100: "Unknown error", + 101: "Invalid parameter", + 102: "API does not exist", + 103: "Method does not exist", + 104: "Version does not support", + 105: "Permission denied", + 106: "Session timeout", + 107: "Session interrupted by duplicate login", + 400: "Invalid credentials", + 401: "Account disabled", + 402: "Permission denied", + 403: "2FA required", + 404: "Failed to authenticate with 2FA" + } + return error_descriptions.get(error_code, "Unknown error code") + + def _check_tcp_connection(self) -> bool: + """Проверка базового TCP-соединения с Synology NAS""" + try: + socket_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + socket_obj.settimeout(SYNOLOGY_TIMEOUT) + result = socket_obj.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT)) + socket_obj.close() + + return result == 0 + except socket.error as e: + logger.error(f"Socket error during connection check: {str(e)}") + return False + except Exception as e: + logger.error(f"Unexpected error during connection check: {str(e)}") + return False + + def is_online(self, force_check=False) -> bool: + """Проверка онлайн-статуса Synology NAS""" + # Используем кэшированное значение, если доступно и не устарело + current_time = time.time() + if not force_check and (current_time - self._last_online_check) < self._online_check_interval: + logger.debug(f"Using cached online status: {self._last_online_status}") + return self._last_online_status + + logger.info("Checking if NAS is online...") + + # Проверяем TCP-соединение + online_status = self._check_tcp_connection() + logger.info(f"Detected Synology NAS online status: {online_status}") + + # Если TCP-соединение успешно и у нас есть действующий SID, + # попробуем более детальную проверку через API + if online_status and self.sid: + logger.info("Trying to fetch more detailed online status through API...") + + # Пробуем разные API для проверки онлайн-статуса + api_checks = [ + {"api": "SYNO.DSM.Info", "version": "2", "method": "getinfo"}, + {"api": "SYNO.Core.System", "version": "1", "method": "info"}, + {"api": "SYNO.Core.System.Status", "version": "1", "method": "get"} + ] + + api_success = False + for api_check in api_checks: + try: + url = f"{self.base_url}/entry.cgi" + params = { + "api": api_check["api"], + "version": api_check["version"], + "method": api_check["method"], + "sid": self.sid + } + + logger.debug(f"Trying online status check with {api_check['api']}") + response = self.session.get( + url, + params=params, + timeout=self.default_timeout, + verify=False + ) + + if response.status_code == 200: + data = response.json() + if data.get("success"): + logger.info(f"API request successful for {api_check['api']}") + logger.info("Synology NAS is online with API access") + api_success = True + break + else: + error_code = data.get("error", {}).get("code", -1) + logger.warning(f"API response indicates an error: {error_code}, but NAS is reachable") + else: + logger.warning(f"API returned status code {response.status_code}") + except Exception as e: + logger.warning(f"API check failed for {api_check['api']}, but TCP connection succeeded: {str(e)}") + + if not api_success: + logger.warning("All API checks failed, but TCP connection is successful") + + # Обновляем кэшированное значение + self._last_online_check = current_time + self._last_online_status = online_status + + return online_status + + def wake_on_lan(self) -> bool: + """Отправка Wake-on-LAN пакета для включения Synology NAS""" + if not SYNOLOGY_MAC: + logger.error("MAC address not configured") + return False + + try: + # Преобразование MAC-адреса в байты + mac_address = SYNOLOGY_MAC.replace(':', '').replace('-', '') + if len(mac_address) != 12: + logger.error(f"Invalid MAC address format: {SYNOLOGY_MAC}") + return False + + try: + mac_bytes = bytes.fromhex(mac_address) + except ValueError as e: + logger.error(f"Failed to parse MAC address: {str(e)}") + logger.error(f"MAC address must contain only hex characters: {SYNOLOGY_MAC}") + return False + + # Создание Magic Packet + magic_packet = b'\xff' * 6 + mac_bytes * 16 + + # Отправка пакета на конкретный адрес + try: + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) + sock.sendto(magic_packet, (SYNOLOGY_HOST, WOL_PORT)) + sock.close() + logger.info(f"Wake-on-LAN packet sent to {SYNOLOGY_MAC} at {SYNOLOGY_HOST}:{WOL_PORT}") + except Exception as e: + logger.error(f"Error sending directed WoL packet: {str(e)}") + return False + + # Для надежности отправляем также широковещательный пакет + try: + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) + + # Используем стандартный широковещательный адрес + broadcast_addr = "255.255.255.255" + sock.sendto(magic_packet, (broadcast_addr, WOL_PORT)) + sock.close() + logger.info(f"Wake-on-LAN broadcast packet sent to {broadcast_addr}:{WOL_PORT}") + except Exception as e: + logger.warning(f"Error sending broadcast WoL packet (non-critical): {str(e)}") + # Не считаем ошибкой, т.к. основной пакет уже отправлен + + return True + + except Exception as e: + logger.error(f"Unexpected error in wake_on_lan: {str(e)}") + return False + + def wait_for_boot(self, max_attempts: int = 30, delay: int = 10) -> bool: + """Ожидание загрузки Synology NAS""" + logger.info(f"Waiting for Synology NAS to boot (timeout: {max_attempts * delay}s)...") + + for attempt in range(max_attempts): + # Принудительно проверяем статус без использования кэша + if self.is_online(force_check=True): + logger.info(f"Synology NAS is online after {attempt + 1} attempts ({(attempt + 1) * delay}s)") + + # Проверяем, что не только сеть доступна, но и API загрузился + api_ready = False + logger.info("Waiting for API services to initialize...") + + for api_check in range(5): # Даем еще до 50 секунд для загрузки API + if self.sid or self.login(): + api_ready = True + logger.info(f"API services are ready after {api_check + 1} attempts") + break + logger.debug(f"API not ready yet, waiting... ({api_check + 1}/5)") + sleep(10) + + if not api_ready: + logger.warning("System is online but API services may not be fully initialized") + + # Дадим дополнительное время для полной загрузки всех сервисов + sleep(delay) + return True + + sleep(delay) + logger.info(f"Waiting for boot... Attempt {attempt + 1}/{max_attempts}") + + logger.error(f"Synology NAS didn't come online after {max_attempts * delay} seconds") + return False + + def power_on(self) -> bool: + """Включение Synology NAS""" + # Принудительная проверка статуса + if self.is_online(force_check=True): + logger.info("Synology NAS is already online") + return True + + logger.info("Powering on Synology NAS via Wake-on-LAN...") + + # Проверяем, настроен ли MAC-адрес + if not SYNOLOGY_MAC: + logger.error("Cannot power on: MAC address not configured in settings") + return False + + # Пробуем отправить несколько WoL пакетов для надежности + success = False + for attempt in range(3): + logger.debug(f"Sending WoL packet, attempt {attempt + 1}/3") + if self.wake_on_lan(): + success = True + break + sleep(1) + + if not success: + logger.error("Failed to send Wake-on-LAN packets") + return False + + # Ожидание загрузки + logger.info("WoL packets sent successfully, waiting for system to boot...") + boot_result = self.wait_for_boot(max_attempts=30, delay=10) + + if boot_result: + # Проверяем доступность API после загрузки + system_status = self.get_system_status() + if system_status.get("status") == "online": + logger.info("System booted successfully with API access") + return True + else: + logger.warning("System appears to be online but API may not be fully ready") + return True + else: + logger.error("System did not come online after WoL") + return False + + def power_off(self) -> bool: + """Выключение Synology NAS""" + if not self.is_online(force_check=True): + logger.info("Synology NAS is already offline") + return True + + logger.info("Powering off Synology NAS...") + result = self.shutdown_system() + + if result: + # Дополнительная проверка, что система действительно выключилась + logger.info("Verifying system is offline...") + for attempt in range(12): # Проверяем в течение 2 минут (12 * 10 сек) + if not self.is_online(force_check=True): + logger.info(f"System confirmed offline after {attempt * 10} seconds") + return True + logger.debug(f"System still shutting down, checking again... ({attempt + 1}/12)") + sleep(10) + + logger.warning("System still appears to be online after shutdown command") + return False + else: + logger.error("Failed to initiate shutdown") + return False + + # Заглушки для расширенных методов + def get_shared_folders(self) -> List[Dict[str, Any]]: + """Получение списка общих папок""" + logger.info("Getting list of shared folders") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for shared folders request") + return [] + + try: + # Запрашиваем список общих папок через FileStation API + result = self._make_api_request("SYNO.FileStation.List", "list_share", version=2) + + if not result: + # Пробуем альтернативный API + logger.info("Trying alternative API for shared folders") + alt_result = self._make_api_request("SYNO.FileStation.List", "list_share", version=1) + if alt_result: + return alt_result.get("shares", []) + return [] + + return result.get("shares", []) + + except Exception as e: + logger.error(f"Error getting shared folders: {str(e)}") + return [] + + def get_system_load(self) -> Dict[str, Any]: + """Получение информации о загрузке системы""" + logger.info("Getting system load information") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for system load request") + return {} + + try: + # Запрашиваем информацию о загрузке системы + result = self._make_api_request("SYNO.Core.System.Utilization", "get", version=1) + + if not result: + # Пробуем альтернативный API + logger.info("Trying alternative API for system load") + alt_result = self._make_api_request("SYNO.Core.System", "info", version=3) + + if not alt_result: + return {} + + # Формируем из частичных данных + return { + "cpu_load": alt_result.get("cpu_usage", 0), + "memory": { + "total": alt_result.get("memory_size", 0), + "used": alt_result.get("memory_usage", 0), + "usage_percent": alt_result.get("memory_usage_percent", 0) + } + } + + # Формируем структурированный результат + return { + "cpu_load": result.get("cpu", {}).get("user", 0) + result.get("cpu", {}).get("system", 0), + "memory": result.get("memory", {}), + "network": result.get("network", {}) + } + + except Exception as e: + logger.error(f"Error getting system load: {str(e)}") + return {} + + def get_storage_status(self) -> Dict[str, Any]: + """Получение подробной информации о хранилище""" + logger.info("Getting storage status information") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for storage status request") + return {"error": "authentication_failed"} + + try: + # Пробуем получить информацию о хранилище через SYNO.Storage.CGI.Storage API + result = self._make_api_request("SYNO.Storage.CGI.Storage", "load_info", version=1) + + if not result: + # Пробуем альтернативный API + logger.info("Trying alternative API for storage info") + alt_result = self._make_api_request("SYNO.Core.Storage", "info", version=1) + + if not alt_result: + # Пробуем еще один альтернативный API + logger.info("Trying SYNO.Core.System API for storage info") + sys_result = self._make_api_request("SYNO.Core.System", "info", version=3) + + if not sys_result: + return { + "volumes": [], + "disks": [], + "total_size": 0, + "total_used": 0, + "error": "no_data" + } + + # Извлекаем базовую информацию о хранилище из системной информации + return { + "volumes": [], + "disks": [], + "total_size": sys_result.get("hdd_total_size", 0) * 1024**2, # МБ в байты + "total_used": sys_result.get("hdd_total_size", 0) * sys_result.get("hdd_usage", 0) / 100 * 1024**2, + } + + # Обрабатываем данные из альтернативного API + volumes = alt_result.get("volumes", []) + disks = alt_result.get("disks", []) + + else: + # Обрабатываем данные из основного API + volumes = result.get("volumes", []) + disks = result.get("disks", []) + + # Рассчитываем общие размеры + total_size = 0 + total_used = 0 + + for volume in volumes: + volume_size = volume.get("size", {}).get("total", 0) + volume_used = volume.get("size", {}).get("used", 0) + + total_size += volume_size + total_used += volume_used + + return { + "volumes": volumes, + "disks": disks, + "total_size": total_size, + "total_used": total_used + } + + except Exception as e: + logger.error(f"Error in get_storage_status: {str(e)}") + return {"error": str(e)} + + def get_security_status(self) -> Dict[str, Any]: + """Получение информации о состоянии безопасности""" + logger.info("Getting security status information") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for security status request") + return {"success": False, "error": "authentication_failed"} + + try: + # Пробуем получить информацию о безопасности через API Security Scan + result = self._make_api_request("SYNO.Core.SecurityScan.Status", "get", version=1) + + if not result: + # Пробуем альтернативный API + logger.info("Trying alternative API for security status") + # Проверяем статус брандмауэра + firewall_result = self._make_api_request("SYNO.Core.Security.Firewall.Status", "get", version=1) + + # Проверяем статус автоматических обновлений + update_result = self._make_api_request("SYNO.Core.Upgrade.Server.Setting", "get", version=1) + + # Если ни один из API не отвечает + if not firewall_result and not update_result: + # Получаем общую информацию о системе для базовой проверки безопасности + sys_result = self._make_api_request("SYNO.DSM.Info", "getinfo", version=2) + + if not sys_result: + return { + "success": False, + "status": "unknown", + "last_check": None, + "is_secure": False, + "error": "no_security_api" + } + + # Собираем базовые сведения из системной информации + return { + "success": True, + "status": "basic", + "last_check": None, + "is_secure": True, # Предполагаем, что система в целом безопасна + "firewall_enabled": None, + "auto_update": None, + "version_latest": sys_result.get("version_string", "") + } + + # Собираем информацию из доступных результатов + firewall_enabled = firewall_result.get("enabled", False) if firewall_result else None + auto_update = update_result.get("auto_update", False) if update_result else None + + # Определяем, насколько система безопасна + is_secure = True # По умолчанию предполагаем, что система безопасна + if firewall_enabled is not None and not firewall_enabled: + is_secure = False + + return { + "success": True, + "status": "partial", + "last_check": None, + "is_secure": is_secure, + "firewall_enabled": firewall_enabled, + "auto_update": auto_update + } + + # Если основное API отвечает, возвращаем его данные + return { + "success": True, + "status": result.get("status", "unknown"), + "last_check": result.get("last_check", None), + "is_secure": result.get("is_secure", False), + "details": result.get("details", {}) + } + + except Exception as e: + logger.error(f"Error in get_security_status: {str(e)}") + return {"success": False, "error": str(e)} + + def get_processes(self, limit: int = 20) -> List[Dict[str, Any]]: + """Получение списка активных процессов""" + logger.info(f"Getting list of active processes (limit={limit})") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for processes request") + return [] + + try: + # Получаем список процессов через API + result = self._make_api_request("SYNO.Core.System.Process", "list", version=1, + params={"sort_by": "cpu", "order": "DESC", "limit": limit}) + + if not result: + logger.warning("Failed to get process list") + return [] + + return result.get("processes", []) + + except Exception as e: + logger.error(f"Error getting process list: {str(e)}") + return [] + + def get_network_status(self) -> Dict[str, Any]: + """Получение информации о сетевых подключениях""" + logger.info("Getting network status information") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for network status request") + return {} + + try: + # Получаем информацию о сетевых интерфейсах + interface_result = self._make_api_request("SYNO.Core.Network.Interface", "list", version=1) + + # Получаем статистику использования сети + utilization_result = self._make_api_request("SYNO.Core.System.Utilization", "get", version=1) + + interfaces = [] + if interface_result: + interfaces = interface_result.get("interfaces", []) + + network_stats = {} + if utilization_result and "network" in utilization_result: + network_stats = utilization_result.get("network", {}) + + # Объединяем данные + for interface in interfaces: + iface_id = interface.get("id", "") + if iface_id in network_stats: + interface["rx"] = network_stats[iface_id].get("rx", 0) + interface["tx"] = network_stats[iface_id].get("tx", 0) + + return { + "interfaces": interfaces, + "statistics": network_stats + } + + except Exception as e: + logger.error(f"Error getting network status: {str(e)}") + return {} + + def get_system_logs(self, limit: int = 20) -> List[Dict[str, Any]]: + """Получение журналов системы""" + logger.info(f"Getting system logs (limit={limit})") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for system logs request") + return [] + + try: + # Получаем журналы через API + result = self._make_api_request("SYNO.Core.SyslogClient.Log", "list", version=1, + params={"start": 0, "limit": limit, "sort": "time", "dir": "DESC"}) + + if not result: + # Пробуем альтернативный API + logger.info("Trying alternative API for system logs") + alt_result = self._make_api_request("SYNO.Core.Log", "list", version=1, + params={"start": 0, "limit": limit, "sort": "time", "dir": "DESC"}) + + if alt_result: + return alt_result.get("logs", []) + return [] + + return result.get("logs", []) + + except Exception as e: + logger.error(f"Error getting system logs: {str(e)}") + return [] + + def get_power_schedule(self) -> Dict[str, Any]: + """Получение расписания включения/выключения""" + logger.info("Getting power schedule") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for power schedule request") + return {} + + try: + # Получаем расписание через API + result = self._make_api_request("SYNO.Core.Hardware.PowerSchedule", "get", version=1) + + if not result: + return {} + + return result + + except Exception as e: + logger.error(f"Error getting power schedule: {str(e)}") + return {} + + def set_power_schedule(self, schedule_type: str, days: List[int], time: str, enabled: bool = True) -> bool: + """Настройка расписания включения/выключения + + Args: + schedule_type: Тип расписания ('boot' или 'shutdown') + days: Список дней недели (0-6, где 0 - понедельник) + time: Время в формате 'HH:MM' + enabled: Включить или выключить расписание + + Returns: + True если успешно, False в противном случае + """ + logger.info(f"Setting power schedule for {schedule_type} at {time} on days {days}") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for setting power schedule") + return False + + try: + # Получаем текущее расписание + current_schedule = self._make_api_request("SYNO.Core.Hardware.PowerSchedule", "get", version=1) + + if not current_schedule: + logger.error("Failed to get current power schedule") + return False + + # Подготавливаем новое расписание + params = { + "enabled": enabled, + "type": schedule_type, + "day": days, + "time": time + } + + # Устанавливаем новое расписание + result = self._make_api_request("SYNO.Core.Hardware.PowerSchedule", "set", version=1, params=params) + + if not result: + logger.error("Failed to set power schedule") + return False + + logger.info(f"Power schedule for {schedule_type} set successfully") + return True + + except Exception as e: + logger.error(f"Error setting power schedule: {str(e)}") + return False + + def get_temperature_status(self) -> Dict[str, Any]: + """Получение информации о температуре системы и дисков""" + logger.info("Getting temperature status") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for temperature status request") + return {} + + try: + # Получаем информацию о системе для общей температуры + system_info = self._make_api_request("SYNO.DSM.Info", "getinfo", version=2) + + # Получаем информацию о дисках для их температуры + storage_info = self._make_api_request("SYNO.Storage.CGI.Storage", "load_info", version=1) + + system_temp = None + disk_temps = [] + + if system_info: + system_temp = system_info.get("temperature") + + if storage_info: + disks = storage_info.get("disks", []) + for disk in disks: + name = disk.get("name", "unknown") + model = disk.get("model", "unknown") + temp = disk.get("temp", None) + if temp is not None: + disk_temps.append({ + "name": name, + "model": model, + "temperature": temp + }) + + return { + "system_temperature": system_temp, + "disk_temperatures": disk_temps, + "warning": system_info.get("temperature_warn", False) if system_info else False + } + + except Exception as e: + logger.error(f"Error getting temperature status: {str(e)}") + return {} + + def browse_files(self, folder_path: str = "", limit: int = 100) -> Dict[str, Any]: + """Просмотр файлов в указанной директории + + Args: + folder_path: Путь к папке (пустая строка для корневых общих папок) + limit: Максимальное количество элементов для возврата + + Returns: + Словарь с информацией о файлах и папках + """ + logger.info(f"Browsing files in {folder_path or 'root'}") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for file browsing") + return {"success": False, "error": "authentication_failed"} + + try: + if not folder_path: + # Если путь не указан, получаем список общих папок + result = self._make_api_request("SYNO.FileStation.List", "list_share", version=2) + + if not result: + logger.error("Failed to list shared folders") + return {"success": False, "error": "api_error"} + + return { + "success": True, + "items": result.get("shares", []), + "path": "", + "is_root": True + } + else: + # Получаем список файлов в указанной директории + params = { + "folder_path": folder_path, + "limit": limit, + "offset": 0, + "sort_by": "name", + "sort_direction": "ASC" + } + + result = self._make_api_request("SYNO.FileStation.List", "list", version=2, params=params) + + if not result: + logger.error(f"Failed to list files in {folder_path}") + return {"success": False, "error": "api_error"} + + return { + "success": True, + "items": result.get("files", []), + "path": folder_path, + "is_root": False, + "total": result.get("total", 0) + } + + except Exception as e: + logger.error(f"Error browsing files: {str(e)}") + return {"success": False, "error": str(e)} + + def manage_service(self, service_name: str, action: str = "status") -> Dict[str, Any]: + """Управление системным сервисом + + Args: + service_name: Имя сервиса + action: Действие (status/start/stop/restart) + + Returns: + Словарь с результатом операции + """ + logger.info(f"Managing service {service_name}, action: {action}") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for service management") + return {"success": False, "error": "authentication_failed"} + + try: + # Проверяем доступное API для управления сервисами + if action == "status": + result = self._make_api_request("SYNO.Core.Service", "get", version=1, + params={"service": service_name}) + else: + result = self._make_api_request("SYNO.Core.Service", action, version=1, + params={"service": service_name}) + + if not result: + logger.error(f"Failed to {action} service {service_name}") + return {"success": False, "error": "api_error"} + + return { + "success": True, + "service": service_name, + "action": action, + "result": result, + "status": result.get("status") if action == "status" else "completed" + } + + except Exception as e: + logger.error(f"Error managing service {service_name}: {str(e)}") + return {"success": False, "error": str(e)} + + def search_files(self, pattern: str, folder_path: str = "", limit: int = 100) -> Dict[str, Any]: + """Поиск файлов по шаблону + + Args: + pattern: Шаблон для поиска + folder_path: Путь к папке для поиска (пустая строка для всех общих папок) + limit: Максимальное количество результатов + + Returns: + Словарь с найденными файлами + """ + logger.info(f"Searching for {pattern} in {folder_path or 'all shared folders'}") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for file search") + return {"success": False, "error": "authentication_failed"} + + try: + if not folder_path: + # Получаем список всех общих папок для поиска + shares_result = self._make_api_request("SYNO.FileStation.List", "list_share", version=2) + + if not shares_result: + logger.error("Failed to list shared folders for search") + return {"success": False, "error": "api_error"} + + # Формируем список путей для поиска + folder_paths = [share.get("path") for share in shares_result.get("shares", [])] + else: + folder_paths = [folder_path] + + # Запускаем поиск + params = { + "folder_path": folder_paths, + "pattern": pattern, + "limit": limit, + "offset": 0 + } + + result = self._make_api_request("SYNO.FileStation.Search", "start", version=2, params=params) + + if not result: + logger.error(f"Failed to start search for {pattern}") + return {"success": False, "error": "api_error"} + + # Получаем taskid для проверки результатов + taskid = result.get("taskid") + if not taskid: + logger.error("No taskid received for search") + return {"success": False, "error": "no_task_id"} + + # Ожидаем завершения поиска + search_result = {"finished": False, "progress": 0} + for _ in range(10): # Максимум 10 попыток + search_status = self._make_api_request("SYNO.FileStation.Search", "status", version=2, + params={"taskid": taskid}) + + if not search_status: + break + + search_result["progress"] = search_status.get("progress", 0) + + if search_status.get("finished", False): + search_result["finished"] = True + break + + time.sleep(0.5) # Пауза между запросами + + # Получаем результаты поиска + if search_result["finished"]: + list_result = self._make_api_request("SYNO.FileStation.Search", "list", version=2, + params={"taskid": taskid, "limit": limit}) + + if list_result: + files = list_result.get("files", []) + return { + "success": True, + "pattern": pattern, + "results": files, + "total": list_result.get("total", len(files)) + } + + # Если не удалось получить результаты, останавливаем поиск + self._make_api_request("SYNO.FileStation.Search", "stop", version=2, + params={"taskid": taskid}) + + return { + "success": False, + "error": "search_timeout", + "progress": search_result["progress"] + } + + except Exception as e: + logger.error(f"Error searching files: {str(e)}") + return {"success": False, "error": str(e)} diff --git a/.history/src/api/synology_20250830091218.py b/.history/src/api/synology_20250830091218.py new file mode 100644 index 0000000..25d086c --- /dev/null +++ b/.history/src/api/synology_20250830091218.py @@ -0,0 +1,1903 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Модуль для взаимодействия с API Synology NAS +""" + +import requests +from requests.adapters import HTTPAdapter +import json +import logging +import time +import urllib3 +from urllib3.util import Retry +from typing import Dict, Any, Optional, List +import socket +import struct +from time import sleep + +from src.config.config import ( + SYNOLOGY_HOST, + SYNOLOGY_PORT, + SYNOLOGY_USERNAME, + SYNOLOGY_PASSWORD, + SYNOLOGY_SECURE, + SYNOLOGY_TIMEOUT, + SYNOLOGY_MAC, + WOL_PORT, + SYNOLOGY_API_VERSION, + SYNOLOGY_POWER_API, + SYNOLOGY_INFO_API +) +from src.api.api_discovery import discover_available_apis, find_compatible_api + +# Отключение предупреждений о небезопасных SSL-соединениях +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + +logger = logging.getLogger(__name__) + +class SynologyAPI: + """Класс для взаимодействия с API Synology NAS""" + + def __init__(self): + """Инициализация класса SynologyAPI""" + logger.info("Creating API with auto-retry and connection pool") + logger.debug(f"Connection details: Host={SYNOLOGY_HOST}, Port={SYNOLOGY_PORT}, Secure={SYNOLOGY_SECURE}") + + self.protocol = "https" if SYNOLOGY_SECURE else "http" + self.base_url = f"{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi" + self.sid = None + self.session = requests.Session() + + # Настройка SSL + if self.protocol == "https": + logger.debug("SSL enabled, disabling certificate verification for internal network") + self.session.verify = False # Отключаем проверку SSL для внутренней сети + + # Добавляем пользовательские заголовки для улучшения совместимости с API + custom_headers = { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36', + 'Accept': 'application/json, text/javascript, */*; q=0.01', + 'Accept-Language': 'ru-RU,ru;q=0.9,en-US;q=0.8,en;q=0.7', + 'X-Requested-With': 'XMLHttpRequest', + 'Connection': 'keep-alive', + 'Referer': f'{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/' + } + self.session.headers.update(custom_headers) + logger.debug("Added browser-like headers for API compatibility") + + # Добавляем повторные попытки для HTTP-запросов + retry_strategy = Retry( + total=5, # Увеличиваем количество попыток + status_forcelist=[429, 500, 502, 503, 504, 404], + allowed_methods=["GET", "POST"], + backoff_factor=1.5, # Увеличиваем задержку между попытками + respect_retry_after_header=True + ) + adapter = HTTPAdapter( + max_retries=retry_strategy, + pool_connections=3, + pool_maxsize=10 + ) + self.session.mount("http://", adapter) + self.session.mount("https://", adapter) + + # Таймауты будут указаны в запросах + self.default_timeout = (SYNOLOGY_TIMEOUT, SYNOLOGY_TIMEOUT*2) + logger.debug(f"Setting default request timeout: {self.default_timeout}") + + # Кэш для хранения результатов запросов + self._cache = {} + self._cache_ttl = {} + self._last_online_check = 0 + self._last_online_status = False + self._online_check_interval = 30 # Интервал для кэширования проверок онлайн-статуса + + # Время последней успешной аутентификации и срок действия сессии + self._last_auth_time = 0 + self._auth_expiry = 3600 # По умолчанию 1 час + + # Информация о доступных API + self._available_apis = {} + self._api_info_ttl = 0 + + # Инициализируем API version resolver для автоматического определения совместимых API + self.api_resolver = None # Будет создан при необходимости + + def login(self) -> bool: + """Авторизация в API Synology NAS""" + # Сбрасываем SID для новой сессии + self.sid = None + + logger.info("Attempting to authenticate with Synology NAS...") + logger.debug(f"Base URL: {self.base_url}") + + # Проверяем доступность NAS перед запросом с помощью прямой TCP-проверки + # Избегаем вызова is_online(), чтобы не создавать рекурсию + online_status = self._check_tcp_connection() + if not online_status: + logger.error("Cannot login: Synology NAS is not reachable") + return False + + # Пробуем различные версии API для аутентификации + # Начинаем с версии 3, которая показала лучшую совместимость в тестах + auth_versions_to_try = [3, 2, 1, 6] # Пробуем сначала более стабильные версии + + for auth_version in auth_versions_to_try: + try: + # Определяем путь к API аутентификации + auth_path = "entry.cgi" # Используем entry.cgi вместо auth.cgi по умолчанию + + # Проверка информации API для определения доступных версий API + api_info_url = f"{self.base_url}/entry.cgi" + api_info_params = { + "api": "SYNO.API.Info", + "version": "1", + "method": "query", + "query": "SYNO.API.Auth" + } + + logger.debug(f"Querying API info for auth version {auth_version}") + try: + api_info_response = self.session.get( + api_info_url, + params=api_info_params, + timeout=self.default_timeout, + verify=False + ) + + if api_info_response.status_code == 200: + api_info_data = api_info_response.json() + if api_info_data.get("success"): + auth_info = api_info_data.get("data", {}).get("SYNO.API.Auth", {}) + max_version = auth_info.get("maxVersion", 6) + min_version = auth_info.get("minVersion", 1) + auth_path = auth_info.get("path", "entry.cgi") + logger.debug(f"Auth API versions: min={min_version}, max={max_version}, path={auth_path}") + + # Проверяем поддержку текущей версии + if auth_version < min_version or auth_version > max_version: + logger.warning(f"Auth API version {auth_version} not supported (min={min_version}, max={max_version}), trying next version") + continue + else: + logger.warning("Failed to query API info, using default auth path") + else: + logger.warning(f"API info request failed with status {api_info_response.status_code}, using default auth path") + except Exception as e: + logger.warning(f"Error querying API info: {str(e)}, using default auth path") + + # Основной запрос авторизации + url = f"{self.base_url}/{auth_path}" + params = { + "api": "SYNO.API.Auth", + "version": str(auth_version), + "method": "login", + "account": SYNOLOGY_USERNAME, + "passwd": SYNOLOGY_PASSWORD, + "session": "SynologyPowerControlBot", + "format": "cookie" + } + + # Для версии 6+ используем немного другой формат + if auth_version >= 6: + params["enable_syno_token"] = "yes" + + logger.debug(f"Sending auth request to {url} with API version {auth_version}") + start_time = time.time() + response = self.session.get( + url, + params=params, + timeout=self.default_timeout, + verify=False + ) + elapsed_time = time.time() - start_time + logger.debug(f"Auth request completed in {elapsed_time:.2f}s with status code {response.status_code}") + + # Проверка статуса HTTP + if response.status_code != 200: + logger.error(f"HTTP error: {response.status_code}") + logger.debug(f"Response content: {response.text[:200]}") + continue # Пробуем следующую версию + + try: + data = response.json() + except json.JSONDecodeError: + logger.error("Failed to decode JSON response") + logger.debug(f"Response content: {response.text[:200]}") + continue # Пробуем следующую версию + + if data.get("success"): + self.sid = data.get("data", {}).get("sid") + self._last_auth_time = time.time() + logger.info(f"Successfully logged in to Synology NAS using API version {auth_version}") + logger.debug(f"Session ID obtained: {self.sid[:5] if self.sid else 'None'}...") + + # Получаем и сохраняем токен SYNO, если он есть + syno_token = data.get("data", {}).get("synotoken") + if syno_token: + self.session.headers.update({'X-SYNO-TOKEN': syno_token}) + logger.debug("Added X-SYNO-TOKEN header for improved API compatibility") + + # Также добавляем SID в cookies для улучшения совместимости + self.session.cookies.update({ + 'id': self.sid, + 'sid': self.sid + }) + logger.debug("Added SID to session cookies for improved compatibility") + + # Проверка валидности полученной сессии с помощью простого запроса + # Будем использовать SYNO.API.Info без проверки сложных методов + + # Даем системе немного времени для инициализации сессии + time.sleep(0.5) + + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to log in with version {auth_version}: Error code {error_code}") + + # Если ошибка связана с версией API, пробуем следующую версию + if error_code in [104, 105]: + logger.warning(f"Auth version {auth_version} not supported, trying next version") + continue + + # Дополнительная диагностика + if error_code == 400: + logger.error("Authentication error: Invalid credentials") + elif error_code == 401: + logger.error("Authentication error: Account disabled") + elif error_code == 402: + logger.error("Authentication error: Permission denied") + elif error_code == 403: + logger.error("Authentication error: 2-factor authentication required") + elif error_code == 404: + logger.error("Authentication error: Failed to authenticate with 2-factor authentication") + + # Если ошибка связана с неверными учетными данными, нет смысла пробовать другие версии API + if error_code in [400, 401, 402, 403, 404]: + return False + + except requests.exceptions.Timeout: + logger.error(f"Connection timeout during auth with version {auth_version}") + continue # Пробуем следующую версию + except requests.exceptions.ConnectionError as e: + logger.error(f"Connection error during auth with version {auth_version}: {str(e)}") + continue # Пробуем следующую версию + except requests.RequestException as e: + logger.error(f"Request error during auth with version {auth_version}: {str(e)}") + continue # Пробуем следующую версию + except Exception as e: + logger.error(f"Unexpected error during login with version {auth_version}: {str(e)}", exc_info=True) + continue # Пробуем следующую версию + + # Если все версии не сработали + logger.error("Failed to authenticate with any API version") + return False + + def _validate_session(self) -> bool: + """Проверяет валидность сессии после авторизации""" + if not self.sid: + return False + + # Попробуем сделать простой запрос для проверки сессии + test_apis = [ + {"api": "SYNO.Core.System", "method": "info", "version": 1}, + {"api": "SYNO.DSM.Info", "method": "getinfo", "version": 1} + ] + + for test_api in test_apis: + try: + url = f"{self.base_url}/entry.cgi" + params = { + "api": test_api["api"], + "version": str(test_api["version"]), + "method": test_api["method"], + "_sid": self.sid + } + + response = self.session.get(url, params=params, timeout=self.default_timeout, verify=False) + + if response.status_code == 200: + data = response.json() + if data.get("success"): + logger.debug(f"Session validation successful using {test_api['api']}") + return True + else: + error_code = data.get("error", {}).get("code", -1) + if error_code != 119: # Не сессия истекла + logger.debug(f"Session validation with {test_api['api']} returned error code {error_code}") + return True # Считаем сессию валидной, если ошибка не связана с истечением сессии + except Exception as e: + logger.warning(f"Error during session validation with {test_api['api']}: {str(e)}") + + logger.warning("Session validation failed with all test APIs") + return False + + def logout(self) -> bool: + """Выход из API Synology NAS""" + if not self.sid: + return True + + try: + url = f"{self.base_url}/auth.cgi" + params = { + "api": "SYNO.API.Auth", + "version": "1", + "method": "logout", + "session": "SynologyPowerControlBot", + "_sid": self.sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + self.sid = None + logger.info("Successfully logged out from Synology NAS") + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to log out from Synology NAS: Error code {error_code}") + return False + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + return False + + def _make_api_request(self, api_name: str, method: str, version: int = 1, + params: Optional[Dict[str, Any]] = None, retry_auth: bool = True, retry_count: int = 0) -> Optional[Dict[str, Any]]: + """Обобщенный метод для выполнения API запросов с обработкой ошибок""" + # Ограничение на количество повторных попыток + if retry_count >= 3: + logger.error(f"Too many retries for {api_name}.{method}, giving up") + return None + + # Проверка наличия авторизации + if not self.sid and not self.login(): + logger.error(f"Not authenticated for API request: {api_name}.{method}") + return None + + # Проверка информации API для определения пути и поддерживаемой версии + api_info_url = f"{self.base_url}/entry.cgi" + api_info_params = { + "api": "SYNO.API.Info", + "version": "1", + "method": "query", + "query": api_name + } + + api_path = "entry.cgi" + try: + logger.debug(f"Querying API info for {api_name}") + api_info_response = self.session.get( + api_info_url, + params=api_info_params, + timeout=self.default_timeout, + verify=False + ) + + if api_info_response.status_code == 200: + api_info_data = api_info_response.json() + if api_info_data.get("success"): + api_info = api_info_data.get("data", {}).get(api_name, {}) + if api_info: + max_version = api_info.get("maxVersion", version) + min_version = api_info.get("minVersion", version) + api_path = api_info.get("path", "entry.cgi") + + # Проверка, поддерживается ли запрошенная версия + if version < min_version: + logger.warning(f"API version {version} for {api_name} is below minimum {min_version}, using {min_version}") + version = min_version + elif version > max_version: + logger.warning(f"API version {version} for {api_name} exceeds maximum {max_version}, using {max_version}") + version = max_version + + logger.debug(f"Using API path: {api_path}, version: {version}") + else: + logger.warning(f"API {api_name} not found in API info, using defaults") + except Exception as e: + logger.warning(f"Error querying API info for {api_name}: {str(e)}, using defaults") + + # Подготовка базовых параметров запроса + base_params = { + "api": api_name, + "version": str(version), + "method": method, + "_sid": self.sid # Используем _sid вместо sid для лучшей совместимости + } + + # Добавление дополнительных параметров, если они заданы + if params: + base_params.update(params) + + url = f"{self.base_url}/{api_path}" + logger.debug(f"API request: {api_name}.{method} v{version} to {url}") + logger.debug(f"Full request params: {base_params}") + + try: + start_time = time.time() + response = self.session.get( + url, + params=base_params, + timeout=self.default_timeout, + verify=False + ) + elapsed_time = time.time() - start_time + logger.debug(f"API request completed in {elapsed_time:.2f}s with status {response.status_code}") + + # Проверка статуса HTTP + if response.status_code != 200: + logger.error(f"HTTP error: {response.status_code} for {api_name}.{method}") + + # Повторная попытка при ошибках соединения + if response.status_code in [500, 502, 503, 504]: + logger.info(f"Server error, retrying request for {api_name}.{method}") + sleep(2) + return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1) + + return None + + try: + data = response.json() + except json.JSONDecodeError: + logger.error(f"Failed to decode JSON response for {api_name}.{method}") + logger.debug(f"Response content: {response.text[:200]}") + + # Повторная попытка при ошибках декодирования + logger.info(f"JSON decode error, retrying request for {api_name}.{method}") + sleep(2) + return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1) + + if data.get("success"): + logger.info(f"API request successful for {api_name}.{method}") + return data.get("data", {}) + else: + error_code = data.get("error", {}).get("code", -1) + error_desc = self._get_error_description(error_code) + logger.error(f"API error for {api_name}.{method}: {error_code} - {error_desc}") + + # Ошибки доступа или прав часто встречаются, но они не критичные + # Например, ошибка 102 означает, что нет прав, но NAS доступен + if error_code in [102, 103, 104, 105]: + logger.warning(f"Permission-related error for {api_name}.{method}: {error_code}") + # Возвращаем пустой словарь вместо None, + # чтобы вызывающий код мог понять, что запрос выполнен + return {} + + # Если ошибка связана с авторизацией и нам разрешено повторить попытку + if error_code in [106, 107, 119] and retry_auth: + logger.info(f"Session error (code {error_code}), creating fresh session...") + self.sid = None # Сбрасываем SID + + # Для ошибки 119 (Session timeout) дадим системе немного времени + if error_code == 119: + logger.info("Session timeout detected, waiting before retry...") + sleep(3) + + if self.login(): + logger.info("Re-authenticated with fresh session, retrying API request...") + # Рекурсивный вызов, но со счетчиком повторов + return self._make_api_request(api_name, method, version, params, False, retry_count + 1) + + # Для некоторых ошибок можно автоматически повторить запрос + if error_code in [408, 429, 500, 502, 503, 504]: + logger.info(f"Temporary error {error_code}, retrying request for {api_name}.{method}") + sleep(2) + return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1) + + return None + + except requests.exceptions.Timeout: + logger.error(f"Request timeout for {api_name}.{method}") + + # Повторная попытка при таймауте + if retry_count < 2: + logger.info(f"Timeout, retrying request for {api_name}.{method}") + sleep(2) + return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1) + + return None + except requests.exceptions.ConnectionError as e: + logger.error(f"Connection error for {api_name}.{method}: {str(e)}") + + # Повторная попытка при ошибке соединения + if retry_count < 2: + logger.info(f"Connection error, retrying request for {api_name}.{method}") + sleep(2) + return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1) + + return None + except Exception as e: + logger.error(f"Unexpected error during {api_name}.{method}: {str(e)}") + return None + + def get_system_status(self) -> Dict[str, Any]: + """Получение статуса системы""" + # Проверяем доступность системы + if not self.is_online(): + logger.info("Device is offline, skipping API request") + return {"status": "offline"} + + # Проверяем, есть ли кэшированный результат + cache_key = "system_status" + current_time = time.time() + if cache_key in self._cache and current_time - self._cache_ttl.get(cache_key, 0) < 60: + logger.debug("Using cached system status") + return self._cache[cache_key] + + # Используем рекомендованный API для получения информации о системе + logger.info(f"Getting system info using {SYNOLOGY_INFO_API} v{SYNOLOGY_API_VERSION}") + + # Для SYNO.DSM.Info метод getinfo, для других обычно используется get или info + if SYNOLOGY_INFO_API == "SYNO.DSM.Info": + method = "getinfo" + else: + method = "get" + + result = self._make_api_request(SYNOLOGY_INFO_API, method, version=SYNOLOGY_API_VERSION) + + if result: + logger.info(f"Successfully retrieved system info using {SYNOLOGY_INFO_API}") + + # Формируем расширенный ответ с дополнительной информацией + system_info = { + "status": "online", + "hostname": result.get("hostname", "unknown"), + "model": result.get("model", "unknown"), + "version": result.get("version", "unknown"), + "uptime": result.get("uptime", 0), + "time": current_time, + "is_online": True, + "api_used": SYNOLOGY_INFO_API + } + + # Сохраняем в кэше + self._cache[cache_key] = system_info + self._cache_ttl[cache_key] = current_time + + return system_info + + # Если основной API не сработал, пробуем резервные варианты + logger.warning(f"Failed to retrieve system info with {SYNOLOGY_INFO_API}, trying fallback APIs") + + # Пробуем резервные API + apis_to_try = [ + {"name": "SYNO.DSM.Info", "method": "getinfo", "version": 2}, + {"name": "SYNO.Core.System", "method": "info", "version": 1}, + {"name": "SYNO.Core.System.Status", "method": "get", "version": 1}, + {"name": "SYNO.Core.System.Info", "method": "get", "version": 1}, + ] + + for api in apis_to_try: + if api["name"] == SYNOLOGY_INFO_API: + continue # Пропускаем уже проверенный API + + logger.info(f"Trying to get system info using {api['name']}.{api['method']} v{api['version']}") + result = self._make_api_request(api["name"], api["method"], version=api["version"]) + + if result: + logger.info(f"Successfully retrieved system info using {api['name']}") + + # Формируем расширенный ответ с дополнительной информацией + system_info = { + "status": "online", + "hostname": result.get("hostname", "unknown"), + "model": result.get("model", "unknown"), + "version": result.get("version", "unknown"), + "uptime": result.get("uptime", 0), + "time": current_time, + "is_online": True, + "api_used": api["name"] + } + + # Сохраняем в кэше + self._cache[cache_key] = system_info + self._cache_ttl[cache_key] = current_time + + return system_info + + # Если все запросы не удались, но система онлайн, возвращаем базовую информацию + logger.warning("Failed to retrieve system info with all API methods") + return { + "status": "error", + "error": "Failed to fetch system information", + "is_online": True, + "time": current_time + } + + def shutdown_system(self) -> bool: + """Выключение системы""" + # Проверяем, включено ли устройство перед попыткой его выключить + if not self.is_online(force_check=True): + logger.info("Synology NAS is already offline, no need to shut down") + return True + + logger.info("Attempting to shutdown Synology NAS...") + + # Попробуем сначала использовать предпочтительный API для управления питанием + logger.info(f"Trying shutdown with {SYNOLOGY_POWER_API} v{SYNOLOGY_API_VERSION}") + + # Для SYNO.Core.Hardware.PowerRecovery используется метод setPowerOnState + # Для других API обычно используется метод shutdown или reboot + if SYNOLOGY_POWER_API == "SYNO.Core.Hardware.PowerRecovery": + # Для этого API нужны специальные параметры + params = {"state": "powerbtn"} # powerbtn имитирует нажатие кнопки питания + result = self._make_api_request(SYNOLOGY_POWER_API, "setPowerOnState", version=SYNOLOGY_API_VERSION, params=params) + else: + # Пробуем стандартный метод + result = self._make_api_request(SYNOLOGY_POWER_API, "shutdown", version=SYNOLOGY_API_VERSION) + + if result is not None: + logger.info(f"Successfully initiated system shutdown using {SYNOLOGY_POWER_API}") + + # Даем системе время начать процесс выключения + logger.info("Waiting for shutdown to initialize...") + sleep(5) + + # Проверяем статус + if not self.is_online(force_check=True): + logger.info("System is now offline. Shutdown confirmed successful.") + return True + else: + logger.info("System still appears to be online, but shutdown may be in progress.") + return True + else: + logger.warning(f"Failed to shutdown using {SYNOLOGY_POWER_API}, trying alternative methods") + + # Если не сработал основной метод, пробуем резервные варианты + # Проверка всех доступных методов API для выключения + apis_to_try = [ + {"name": "SYNO.Core.System", "method": "shutdown", "version": 3}, + {"name": "SYNO.Core.System.Power", "method": "shutdown", "version": 1}, + {"name": "SYNO.DSM.Power", "method": "shutdown", "version": 1}, + {"name": "SYNO.System.Power", "method": "shutdown", "version": 1}, + {"name": "SYNO.DSM.System", "method": "shutdown", "version": 1} + ] + + # Проверяем доступные API + try: + api_info_url = f"{self.base_url}/entry.cgi" + api_info_params = { + "api": "SYNO.API.Info", + "version": "1", + "method": "query", + "query": "SYNO.Core.System.Power,SYNO.DSM.Power,SYNO.Core.System,SYNO.System.Power,SYNO.DSM.System" + } + + logger.debug("Checking available shutdown APIs") + api_info_response = self.session.get( + api_info_url, + params=api_info_params, + timeout=self.default_timeout, + verify=False + ) + + if api_info_response.status_code == 200: + api_info_data = api_info_response.json() + if api_info_data.get("success"): + available_apis = api_info_data.get("data", {}) + logger.debug(f"Available APIs: {list(available_apis.keys())}") + + # Фильтруем только доступные API + filtered_apis = [] + for api in apis_to_try: + if api["name"] in available_apis: + api_info = available_apis[api["name"]] + # Проверка версии API + if api_info.get("minVersion", 1) <= api["version"] <= api_info.get("maxVersion", 1): + filtered_apis.append(api) + logger.debug(f"Adding {api['name']} to available shutdown APIs") + else: + logger.debug(f"API {api['name']} version {api['version']} not supported. Supported: {api_info.get('minVersion')}-{api_info.get('maxVersion')}") + + if filtered_apis: + apis_to_try = filtered_apis + else: + logger.warning("No compatible APIs found, trying all methods as fallback") + else: + logger.warning("Failed to query API info, using default methods") + else: + logger.warning(f"API info request failed with status {api_info_response.status_code}, using default methods") + except Exception as e: + logger.warning(f"Error querying API info: {str(e)}, using default methods") + + # Пробуем все доступные методы по порядку + for api in apis_to_try: + logger.info(f"Trying shutdown with {api['name']}.{api['method']} v{api['version']}") + result = self._make_api_request(api["name"], api["method"], version=api["version"]) + + if result is not None: + logger.info(f"Successfully initiated system shutdown using {api['name']}") + + # Даем системе время начать процесс выключения + logger.info("Waiting for shutdown to initialize...") + sleep(5) + + # Проверяем статус + if not self.is_online(force_check=True): + logger.info("System is now offline. Shutdown confirmed successful.") + return True + else: + logger.info("System still appears to be online, but shutdown may be in progress.") + return True + else: + logger.warning(f"Failed to shutdown using {api['name']}.{api['method']} v{api['version']}") + + # Если ни один метод не сработал, но система стала недоступна + if not self.is_online(force_check=True): + logger.info("System appears to be shutting down despite API errors") + return True + + logger.error("Failed to shutdown system after trying multiple APIs") + return False + + def reboot_system(self) -> bool: + """Перезагрузка системы""" + # Проверяем, включена ли система + if not self.is_online(force_check=True): + logger.error("Cannot reboot: System is offline") + return False + + logger.info("Attempting to reboot Synology NAS...") + + # Попробуем сначала использовать предпочтительный API для управления питанием + logger.info(f"Trying reboot with {SYNOLOGY_POWER_API} v{SYNOLOGY_API_VERSION}") + + # Для SYNO.Core.Hardware.PowerRecovery используется метод setPowerOnState или powerButton + # Для других API обычно используется метод restart или reboot + if SYNOLOGY_POWER_API == "SYNO.Core.Hardware.PowerRecovery": + # Для этого API нужны специальные параметры + params = {"reboot": "true"} # Передаем флаг для перезагрузки + result = self._make_api_request(SYNOLOGY_POWER_API, "setPowerOnState", version=SYNOLOGY_API_VERSION, params=params) + else: + # Пробуем стандартный метод + methods_to_try = ["restart", "reboot"] + result = None + for method in methods_to_try: + result = self._make_api_request(SYNOLOGY_POWER_API, method, version=SYNOLOGY_API_VERSION) + if result is not None: + logger.info(f"Successfully used method {method} for {SYNOLOGY_POWER_API}") + break + + if result is not None: + logger.info(f"Successfully initiated system reboot using {SYNOLOGY_POWER_API}") + + # Даем системе время начать процесс перезагрузки + logger.info("Waiting for reboot to initialize...") + sleep(5) + + # Ждем, пока система станет недоступна (признак перезагрузки) + reboot_started = False + for i in range(12): # Проверяем в течение 60 секунд (12 * 5сек) + if not self.is_online(force_check=True): + logger.info(f"System went offline after {i*5} seconds, reboot in progress") + reboot_started = True + break + logger.debug(f"System still online, waiting... ({i+1}/12)") + sleep(5) + + if reboot_started: + # Ждем, пока система снова станет доступна + logger.info("Waiting for system to come back online...") + return self.wait_for_boot(max_attempts=30, delay=10) + else: + logger.warning("System did not go offline after reboot command, but command was accepted") + # Даже если система не ушла оффлайн, команда могла быть принята + return True + else: + logger.warning(f"Failed to reboot using {SYNOLOGY_POWER_API}, trying alternative methods") + + # Если не сработал основной метод, пробуем резервные варианты + # Проверка всех доступных методов API для перезагрузки + apis_to_try = [ + {"name": "SYNO.Core.System.Power", "method": "restart", "version": 1}, + {"name": "SYNO.DSM.Power", "method": "reboot", "version": 1}, + {"name": "SYNO.Core.System", "method": "reboot", "version": 3}, + {"name": "SYNO.System.Power", "method": "reboot", "version": 1}, + {"name": "SYNO.DSM.System", "method": "reboot", "version": 1}, + {"name": "SYNO.Core.Hardware.NeedReboot", "method": "reboot", "version": 1} + ] + + # Получаем список доступных API + try: + api_info_url = f"{self.base_url}/entry.cgi" + api_info_params = { + "api": "SYNO.API.Info", + "version": "1", + "method": "query", + "query": "SYNO.Core.System.Power,SYNO.DSM.Power,SYNO.Core.System,SYNO.System.Power,SYNO.DSM.System,SYNO.Core.Hardware.NeedReboot" + } + + logger.debug("Checking available reboot APIs") + api_info_response = self.session.get( + api_info_url, + params=api_info_params, + timeout=self.default_timeout, + verify=False + ) + + if api_info_response.status_code == 200: + api_info_data = api_info_response.json() + if api_info_data.get("success"): + available_apis = api_info_data.get("data", {}) + logger.debug(f"Available APIs: {list(available_apis.keys())}") + + # Фильтруем только доступные API + filtered_apis = [] + for api in apis_to_try: + if api["name"] in available_apis: + api_info = available_apis[api["name"]] + # Проверка версии API + if api_info.get("minVersion", 1) <= api["version"] <= api_info.get("maxVersion", 1): + filtered_apis.append(api) + logger.debug(f"Adding {api['name']} to available reboot APIs") + else: + logger.debug(f"API {api['name']} version {api['version']} not supported. Supported: {api_info.get('minVersion')}-{api_info.get('maxVersion')}") + + if filtered_apis: + apis_to_try = filtered_apis + else: + logger.warning("No compatible APIs found, trying all methods as fallback") + else: + logger.warning("Failed to query API info, using default methods") + else: + logger.warning(f"API info request failed with status {api_info_response.status_code}, using default methods") + except Exception as e: + logger.warning(f"Error querying API info: {str(e)}, using default methods") + + # Пробуем все доступные методы по порядку + for api in apis_to_try: + logger.info(f"Trying reboot with {api['name']}.{api['method']} v{api['version']}") + result = self._make_api_request(api["name"], api["method"], version=api["version"]) + + if result is not None: + logger.info(f"Successfully initiated system reboot using {api['name']}") + + # Даем системе время начать процесс перезагрузки + logger.info("Waiting for reboot to initialize...") + sleep(5) + + # Ждем, пока система станет недоступна (признак перезагрузки) + reboot_started = False + for i in range(12): # Проверяем в течение 60 секунд (12 * 5сек) + if not self.is_online(force_check=True): + logger.info(f"System went offline after {i*5} seconds, reboot in progress") + reboot_started = True + break + logger.debug(f"System still online, waiting... ({i+1}/12)") + sleep(5) + + if reboot_started: + # Ждем, пока система снова станет доступна + logger.info("Waiting for system to come back online...") + return self.wait_for_boot(max_attempts=30, delay=10) + else: + logger.warning("System did not go offline after reboot command, but command was accepted") + # Даже если система не ушла оффлайн, команда могла быть принята + return True + else: + logger.warning(f"Failed to reboot using {api['name']}.{api['method']} v{api['version']}") + + logger.error("Failed to reboot system after trying multiple APIs") + return False + + def _get_error_description(self, error_code: int) -> str: + """Получение описания ошибки по коду""" + error_descriptions = { + 100: "Unknown error", + 101: "Invalid parameter", + 102: "API does not exist", + 103: "Method does not exist", + 104: "Version does not support", + 105: "Permission denied", + 106: "Session timeout", + 107: "Session interrupted by duplicate login", + 400: "Invalid credentials", + 401: "Account disabled", + 402: "Permission denied", + 403: "2FA required", + 404: "Failed to authenticate with 2FA" + } + return error_descriptions.get(error_code, "Unknown error code") + + def _check_tcp_connection(self) -> bool: + """Проверка базового TCP-соединения с Synology NAS""" + try: + socket_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + socket_obj.settimeout(SYNOLOGY_TIMEOUT) + result = socket_obj.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT)) + socket_obj.close() + + return result == 0 + except socket.error as e: + logger.error(f"Socket error during connection check: {str(e)}") + return False + except Exception as e: + logger.error(f"Unexpected error during connection check: {str(e)}") + return False + + def is_online(self, force_check=False) -> bool: + """Проверка онлайн-статуса Synology NAS""" + # Используем кэшированное значение, если доступно и не устарело + current_time = time.time() + if not force_check and (current_time - self._last_online_check) < self._online_check_interval: + logger.debug(f"Using cached online status: {self._last_online_status}") + return self._last_online_status + + logger.info("Checking if NAS is online...") + + # Проверяем TCP-соединение + online_status = self._check_tcp_connection() + logger.info(f"Detected Synology NAS online status: {online_status}") + + # Если TCP-соединение успешно и у нас есть действующий SID, + # попробуем более детальную проверку через API + if online_status and self.sid: + logger.info("Trying to fetch more detailed online status through API...") + + # Пробуем разные API для проверки онлайн-статуса + api_checks = [ + {"api": "SYNO.DSM.Info", "version": "2", "method": "getinfo"}, + {"api": "SYNO.Core.System", "version": "1", "method": "info"}, + {"api": "SYNO.Core.System.Status", "version": "1", "method": "get"} + ] + + api_success = False + for api_check in api_checks: + try: + url = f"{self.base_url}/entry.cgi" + params = { + "api": api_check["api"], + "version": api_check["version"], + "method": api_check["method"], + "sid": self.sid + } + + logger.debug(f"Trying online status check with {api_check['api']}") + response = self.session.get( + url, + params=params, + timeout=self.default_timeout, + verify=False + ) + + if response.status_code == 200: + data = response.json() + if data.get("success"): + logger.info(f"API request successful for {api_check['api']}") + logger.info("Synology NAS is online with API access") + api_success = True + break + else: + error_code = data.get("error", {}).get("code", -1) + logger.warning(f"API response indicates an error: {error_code}, but NAS is reachable") + else: + logger.warning(f"API returned status code {response.status_code}") + except Exception as e: + logger.warning(f"API check failed for {api_check['api']}, but TCP connection succeeded: {str(e)}") + + if not api_success: + logger.warning("All API checks failed, but TCP connection is successful") + + # Обновляем кэшированное значение + self._last_online_check = current_time + self._last_online_status = online_status + + return online_status + + def wake_on_lan(self) -> bool: + """Отправка Wake-on-LAN пакета для включения Synology NAS""" + if not SYNOLOGY_MAC: + logger.error("MAC address not configured") + return False + + try: + # Преобразование MAC-адреса в байты + mac_address = SYNOLOGY_MAC.replace(':', '').replace('-', '') + if len(mac_address) != 12: + logger.error(f"Invalid MAC address format: {SYNOLOGY_MAC}") + return False + + try: + mac_bytes = bytes.fromhex(mac_address) + except ValueError as e: + logger.error(f"Failed to parse MAC address: {str(e)}") + logger.error(f"MAC address must contain only hex characters: {SYNOLOGY_MAC}") + return False + + # Создание Magic Packet + magic_packet = b'\xff' * 6 + mac_bytes * 16 + + # Отправка пакета на конкретный адрес + try: + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) + sock.sendto(magic_packet, (SYNOLOGY_HOST, WOL_PORT)) + sock.close() + logger.info(f"Wake-on-LAN packet sent to {SYNOLOGY_MAC} at {SYNOLOGY_HOST}:{WOL_PORT}") + except Exception as e: + logger.error(f"Error sending directed WoL packet: {str(e)}") + return False + + # Для надежности отправляем также широковещательный пакет + try: + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) + + # Используем стандартный широковещательный адрес + broadcast_addr = "255.255.255.255" + sock.sendto(magic_packet, (broadcast_addr, WOL_PORT)) + sock.close() + logger.info(f"Wake-on-LAN broadcast packet sent to {broadcast_addr}:{WOL_PORT}") + except Exception as e: + logger.warning(f"Error sending broadcast WoL packet (non-critical): {str(e)}") + # Не считаем ошибкой, т.к. основной пакет уже отправлен + + return True + + except Exception as e: + logger.error(f"Unexpected error in wake_on_lan: {str(e)}") + return False + + def wait_for_boot(self, max_attempts: int = 30, delay: int = 10) -> bool: + """Ожидание загрузки Synology NAS""" + logger.info(f"Waiting for Synology NAS to boot (timeout: {max_attempts * delay}s)...") + + for attempt in range(max_attempts): + # Принудительно проверяем статус без использования кэша + if self.is_online(force_check=True): + logger.info(f"Synology NAS is online after {attempt + 1} attempts ({(attempt + 1) * delay}s)") + + # Проверяем, что не только сеть доступна, но и API загрузился + api_ready = False + logger.info("Waiting for API services to initialize...") + + for api_check in range(5): # Даем еще до 50 секунд для загрузки API + if self.sid or self.login(): + api_ready = True + logger.info(f"API services are ready after {api_check + 1} attempts") + break + logger.debug(f"API not ready yet, waiting... ({api_check + 1}/5)") + sleep(10) + + if not api_ready: + logger.warning("System is online but API services may not be fully initialized") + + # Дадим дополнительное время для полной загрузки всех сервисов + sleep(delay) + return True + + sleep(delay) + logger.info(f"Waiting for boot... Attempt {attempt + 1}/{max_attempts}") + + logger.error(f"Synology NAS didn't come online after {max_attempts * delay} seconds") + return False + + def power_on(self) -> bool: + """Включение Synology NAS""" + # Принудительная проверка статуса + if self.is_online(force_check=True): + logger.info("Synology NAS is already online") + return True + + logger.info("Powering on Synology NAS via Wake-on-LAN...") + + # Проверяем, настроен ли MAC-адрес + if not SYNOLOGY_MAC: + logger.error("Cannot power on: MAC address not configured in settings") + return False + + # Пробуем отправить несколько WoL пакетов для надежности + success = False + for attempt in range(3): + logger.debug(f"Sending WoL packet, attempt {attempt + 1}/3") + if self.wake_on_lan(): + success = True + break + sleep(1) + + if not success: + logger.error("Failed to send Wake-on-LAN packets") + return False + + # Ожидание загрузки + logger.info("WoL packets sent successfully, waiting for system to boot...") + boot_result = self.wait_for_boot(max_attempts=30, delay=10) + + if boot_result: + # Проверяем доступность API после загрузки + system_status = self.get_system_status() + if system_status.get("status") == "online": + logger.info("System booted successfully with API access") + return True + else: + logger.warning("System appears to be online but API may not be fully ready") + return True + else: + logger.error("System did not come online after WoL") + return False + + def power_off(self) -> bool: + """Выключение Synology NAS""" + if not self.is_online(force_check=True): + logger.info("Synology NAS is already offline") + return True + + logger.info("Powering off Synology NAS...") + result = self.shutdown_system() + + if result: + # Дополнительная проверка, что система действительно выключилась + logger.info("Verifying system is offline...") + for attempt in range(12): # Проверяем в течение 2 минут (12 * 10 сек) + if not self.is_online(force_check=True): + logger.info(f"System confirmed offline after {attempt * 10} seconds") + return True + logger.debug(f"System still shutting down, checking again... ({attempt + 1}/12)") + sleep(10) + + logger.warning("System still appears to be online after shutdown command") + return False + else: + logger.error("Failed to initiate shutdown") + return False + + # Заглушки для расширенных методов + def get_shared_folders(self) -> List[Dict[str, Any]]: + """Получение списка общих папок""" + logger.info("Getting list of shared folders") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for shared folders request") + return [] + + try: + # Запрашиваем список общих папок через FileStation API + result = self._make_api_request("SYNO.FileStation.List", "list_share", version=2) + + if not result: + # Пробуем альтернативный API + logger.info("Trying alternative API for shared folders") + alt_result = self._make_api_request("SYNO.FileStation.List", "list_share", version=1) + if alt_result: + return alt_result.get("shares", []) + return [] + + return result.get("shares", []) + + except Exception as e: + logger.error(f"Error getting shared folders: {str(e)}") + return [] + + def get_system_load(self) -> Dict[str, Any]: + """Получение информации о загрузке системы""" + logger.info("Getting system load information") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for system load request") + return {} + + try: + # Запрашиваем информацию о загрузке системы + result = self._make_api_request("SYNO.Core.System.Utilization", "get", version=1) + + if not result: + # Пробуем альтернативный API + logger.info("Trying alternative API for system load") + alt_result = self._make_api_request("SYNO.Core.System", "info", version=3) + + if not alt_result: + return {} + + # Формируем из частичных данных + return { + "cpu_load": alt_result.get("cpu_usage", 0), + "memory": { + "total": alt_result.get("memory_size", 0), + "used": alt_result.get("memory_usage", 0), + "usage_percent": alt_result.get("memory_usage_percent", 0) + } + } + + # Формируем структурированный результат + return { + "cpu_load": result.get("cpu", {}).get("user", 0) + result.get("cpu", {}).get("system", 0), + "memory": result.get("memory", {}), + "network": result.get("network", {}) + } + + except Exception as e: + logger.error(f"Error getting system load: {str(e)}") + return {} + + def get_storage_status(self) -> Dict[str, Any]: + """Получение подробной информации о хранилище""" + logger.info("Getting storage status information") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for storage status request") + return {"error": "authentication_failed"} + + try: + # Пробуем получить информацию о хранилище через SYNO.Storage.CGI.Storage API + result = self._make_api_request("SYNO.Storage.CGI.Storage", "load_info", version=1) + + if not result: + # Пробуем альтернативный API + logger.info("Trying alternative API for storage info") + alt_result = self._make_api_request("SYNO.Core.Storage", "info", version=1) + + if not alt_result: + # Пробуем еще один альтернативный API + logger.info("Trying SYNO.Core.System API for storage info") + sys_result = self._make_api_request("SYNO.Core.System", "info", version=3) + + if not sys_result: + return { + "volumes": [], + "disks": [], + "total_size": 0, + "total_used": 0, + "error": "no_data" + } + + # Извлекаем базовую информацию о хранилище из системной информации + return { + "volumes": [], + "disks": [], + "total_size": sys_result.get("hdd_total_size", 0) * 1024**2, # МБ в байты + "total_used": sys_result.get("hdd_total_size", 0) * sys_result.get("hdd_usage", 0) / 100 * 1024**2, + } + + # Обрабатываем данные из альтернативного API + volumes = alt_result.get("volumes", []) + disks = alt_result.get("disks", []) + + else: + # Обрабатываем данные из основного API + volumes = result.get("volumes", []) + disks = result.get("disks", []) + + # Рассчитываем общие размеры + total_size = 0 + total_used = 0 + + for volume in volumes: + volume_size = volume.get("size", {}).get("total", 0) + volume_used = volume.get("size", {}).get("used", 0) + + total_size += volume_size + total_used += volume_used + + return { + "volumes": volumes, + "disks": disks, + "total_size": total_size, + "total_used": total_used + } + + except Exception as e: + logger.error(f"Error in get_storage_status: {str(e)}") + return {"error": str(e)} + + def get_security_status(self) -> Dict[str, Any]: + """Получение информации о состоянии безопасности""" + logger.info("Getting security status information") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for security status request") + return {"success": False, "error": "authentication_failed"} + + try: + # Пробуем получить информацию о безопасности через API Security Scan + result = self._make_api_request("SYNO.Core.SecurityScan.Status", "get", version=1) + + if not result: + # Пробуем альтернативный API + logger.info("Trying alternative API for security status") + # Проверяем статус брандмауэра + firewall_result = self._make_api_request("SYNO.Core.Security.Firewall.Status", "get", version=1) + + # Проверяем статус автоматических обновлений + update_result = self._make_api_request("SYNO.Core.Upgrade.Server.Setting", "get", version=1) + + # Если ни один из API не отвечает + if not firewall_result and not update_result: + # Получаем общую информацию о системе для базовой проверки безопасности + sys_result = self._make_api_request("SYNO.DSM.Info", "getinfo", version=2) + + if not sys_result: + return { + "success": False, + "status": "unknown", + "last_check": None, + "is_secure": False, + "error": "no_security_api" + } + + # Собираем базовые сведения из системной информации + return { + "success": True, + "status": "basic", + "last_check": None, + "is_secure": True, # Предполагаем, что система в целом безопасна + "firewall_enabled": None, + "auto_update": None, + "version_latest": sys_result.get("version_string", "") + } + + # Собираем информацию из доступных результатов + firewall_enabled = firewall_result.get("enabled", False) if firewall_result else None + auto_update = update_result.get("auto_update", False) if update_result else None + + # Определяем, насколько система безопасна + is_secure = True # По умолчанию предполагаем, что система безопасна + if firewall_enabled is not None and not firewall_enabled: + is_secure = False + + return { + "success": True, + "status": "partial", + "last_check": None, + "is_secure": is_secure, + "firewall_enabled": firewall_enabled, + "auto_update": auto_update + } + + # Если основное API отвечает, возвращаем его данные + return { + "success": True, + "status": result.get("status", "unknown"), + "last_check": result.get("last_check", None), + "is_secure": result.get("is_secure", False), + "details": result.get("details", {}) + } + + except Exception as e: + logger.error(f"Error in get_security_status: {str(e)}") + return {"success": False, "error": str(e)} + + def get_processes(self, limit: int = 20) -> List[Dict[str, Any]]: + """Получение списка активных процессов""" + logger.info(f"Getting list of active processes (limit={limit})") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for processes request") + return [] + + try: + # Получаем список процессов через API + result = self._make_api_request("SYNO.Core.System.Process", "list", version=1, + params={"sort_by": "cpu", "order": "DESC", "limit": limit}) + + if not result: + logger.warning("Failed to get process list") + return [] + + return result.get("processes", []) + + except Exception as e: + logger.error(f"Error getting process list: {str(e)}") + return [] + + def get_network_status(self) -> Dict[str, Any]: + """Получение информации о сетевых подключениях""" + logger.info("Getting network status information") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for network status request") + return {} + + try: + # Получаем информацию о сетевых интерфейсах + interface_result = self._make_api_request("SYNO.Core.Network.Interface", "list", version=1) + + # Получаем статистику использования сети + utilization_result = self._make_api_request("SYNO.Core.System.Utilization", "get", version=1) + + interfaces = [] + if interface_result: + interfaces = interface_result.get("interfaces", []) + + network_stats = {} + if utilization_result and "network" in utilization_result: + network_stats = utilization_result.get("network", {}) + + # Объединяем данные + for interface in interfaces: + iface_id = interface.get("id", "") + if iface_id in network_stats: + interface["rx"] = network_stats[iface_id].get("rx", 0) + interface["tx"] = network_stats[iface_id].get("tx", 0) + + return { + "interfaces": interfaces, + "statistics": network_stats + } + + except Exception as e: + logger.error(f"Error getting network status: {str(e)}") + return {} + + def get_system_logs(self, limit: int = 20) -> List[Dict[str, Any]]: + """Получение журналов системы""" + logger.info(f"Getting system logs (limit={limit})") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for system logs request") + return [] + + try: + # Получаем журналы через API + result = self._make_api_request("SYNO.Core.SyslogClient.Log", "list", version=1, + params={"start": 0, "limit": limit, "sort": "time", "dir": "DESC"}) + + if not result: + # Пробуем альтернативный API + logger.info("Trying alternative API for system logs") + alt_result = self._make_api_request("SYNO.Core.Log", "list", version=1, + params={"start": 0, "limit": limit, "sort": "time", "dir": "DESC"}) + + if alt_result: + return alt_result.get("logs", []) + return [] + + return result.get("logs", []) + + except Exception as e: + logger.error(f"Error getting system logs: {str(e)}") + return [] + + def get_power_schedule(self) -> Dict[str, Any]: + """Получение расписания включения/выключения""" + logger.info("Getting power schedule") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for power schedule request") + return {} + + try: + # Получаем расписание через API + result = self._make_api_request("SYNO.Core.Hardware.PowerSchedule", "get", version=1) + + if not result: + return {} + + return result + + except Exception as e: + logger.error(f"Error getting power schedule: {str(e)}") + return {} + + def set_power_schedule(self, schedule_type: str, days: List[int], time: str, enabled: bool = True) -> bool: + """Настройка расписания включения/выключения + + Args: + schedule_type: Тип расписания ('boot' или 'shutdown') + days: Список дней недели (0-6, где 0 - понедельник) + time: Время в формате 'HH:MM' + enabled: Включить или выключить расписание + + Returns: + True если успешно, False в противном случае + """ + logger.info(f"Setting power schedule for {schedule_type} at {time} on days {days}") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for setting power schedule") + return False + + try: + # Получаем текущее расписание + current_schedule = self._make_api_request("SYNO.Core.Hardware.PowerSchedule", "get", version=1) + + if not current_schedule: + logger.error("Failed to get current power schedule") + return False + + # Подготавливаем новое расписание + params = { + "enabled": enabled, + "type": schedule_type, + "day": days, + "time": time + } + + # Устанавливаем новое расписание + result = self._make_api_request("SYNO.Core.Hardware.PowerSchedule", "set", version=1, params=params) + + if not result: + logger.error("Failed to set power schedule") + return False + + logger.info(f"Power schedule for {schedule_type} set successfully") + return True + + except Exception as e: + logger.error(f"Error setting power schedule: {str(e)}") + return False + + def get_temperature_status(self) -> Dict[str, Any]: + """Получение информации о температуре системы и дисков""" + logger.info("Getting temperature status") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for temperature status request") + return {} + + try: + # Получаем информацию о системе для общей температуры + system_info = self._make_api_request("SYNO.DSM.Info", "getinfo", version=2) + + # Получаем информацию о дисках для их температуры + storage_info = self._make_api_request("SYNO.Storage.CGI.Storage", "load_info", version=1) + + system_temp = None + disk_temps = [] + + if system_info: + system_temp = system_info.get("temperature") + + if storage_info: + disks = storage_info.get("disks", []) + for disk in disks: + name = disk.get("name", "unknown") + model = disk.get("model", "unknown") + temp = disk.get("temp", None) + if temp is not None: + disk_temps.append({ + "name": name, + "model": model, + "temperature": temp + }) + + return { + "system_temperature": system_temp, + "disk_temperatures": disk_temps, + "warning": system_info.get("temperature_warn", False) if system_info else False + } + + except Exception as e: + logger.error(f"Error getting temperature status: {str(e)}") + return {} + + def browse_files(self, folder_path: str = "", limit: int = 100) -> Dict[str, Any]: + """Просмотр файлов в указанной директории + + Args: + folder_path: Путь к папке (пустая строка для корневых общих папок) + limit: Максимальное количество элементов для возврата + + Returns: + Словарь с информацией о файлах и папках + """ + logger.info(f"Browsing files in {folder_path or 'root'}") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for file browsing") + return {"success": False, "error": "authentication_failed"} + + try: + if not folder_path: + # Если путь не указан, получаем список общих папок + result = self._make_api_request("SYNO.FileStation.List", "list_share", version=2) + + if not result: + logger.error("Failed to list shared folders") + return {"success": False, "error": "api_error"} + + return { + "success": True, + "items": result.get("shares", []), + "path": "", + "is_root": True + } + else: + # Получаем список файлов в указанной директории + params = { + "folder_path": folder_path, + "limit": limit, + "offset": 0, + "sort_by": "name", + "sort_direction": "ASC" + } + + result = self._make_api_request("SYNO.FileStation.List", "list", version=2, params=params) + + if not result: + logger.error(f"Failed to list files in {folder_path}") + return {"success": False, "error": "api_error"} + + return { + "success": True, + "items": result.get("files", []), + "path": folder_path, + "is_root": False, + "total": result.get("total", 0) + } + + except Exception as e: + logger.error(f"Error browsing files: {str(e)}") + return {"success": False, "error": str(e)} + + def manage_service(self, service_name: str, action: str = "status") -> Dict[str, Any]: + """Управление системным сервисом + + Args: + service_name: Имя сервиса + action: Действие (status/start/stop/restart) + + Returns: + Словарь с результатом операции + """ + logger.info(f"Managing service {service_name}, action: {action}") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for service management") + return {"success": False, "error": "authentication_failed"} + + try: + # Проверяем доступное API для управления сервисами + if action == "status": + result = self._make_api_request("SYNO.Core.Service", "get", version=1, + params={"service": service_name}) + else: + result = self._make_api_request("SYNO.Core.Service", action, version=1, + params={"service": service_name}) + + if not result: + logger.error(f"Failed to {action} service {service_name}") + return {"success": False, "error": "api_error"} + + return { + "success": True, + "service": service_name, + "action": action, + "result": result, + "status": result.get("status") if action == "status" else "completed" + } + + except Exception as e: + logger.error(f"Error managing service {service_name}: {str(e)}") + return {"success": False, "error": str(e)} + + def search_files(self, pattern: str, folder_path: str = "", limit: int = 100) -> Dict[str, Any]: + """Поиск файлов по шаблону + + Args: + pattern: Шаблон для поиска + folder_path: Путь к папке для поиска (пустая строка для всех общих папок) + limit: Максимальное количество результатов + + Returns: + Словарь с найденными файлами + """ + logger.info(f"Searching for {pattern} in {folder_path or 'all shared folders'}") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for file search") + return {"success": False, "error": "authentication_failed"} + + try: + if not folder_path: + # Получаем список всех общих папок для поиска + shares_result = self._make_api_request("SYNO.FileStation.List", "list_share", version=2) + + if not shares_result: + logger.error("Failed to list shared folders for search") + return {"success": False, "error": "api_error"} + + # Формируем список путей для поиска + folder_paths = [share.get("path") for share in shares_result.get("shares", [])] + else: + folder_paths = [folder_path] + + # Запускаем поиск + params = { + "folder_path": folder_paths, + "pattern": pattern, + "limit": limit, + "offset": 0 + } + + result = self._make_api_request("SYNO.FileStation.Search", "start", version=2, params=params) + + if not result: + logger.error(f"Failed to start search for {pattern}") + return {"success": False, "error": "api_error"} + + # Получаем taskid для проверки результатов + taskid = result.get("taskid") + if not taskid: + logger.error("No taskid received for search") + return {"success": False, "error": "no_task_id"} + + # Ожидаем завершения поиска + search_result = {"finished": False, "progress": 0} + for _ in range(10): # Максимум 10 попыток + search_status = self._make_api_request("SYNO.FileStation.Search", "status", version=2, + params={"taskid": taskid}) + + if not search_status: + break + + search_result["progress"] = search_status.get("progress", 0) + + if search_status.get("finished", False): + search_result["finished"] = True + break + + time.sleep(0.5) # Пауза между запросами + + # Получаем результаты поиска + if search_result["finished"]: + list_result = self._make_api_request("SYNO.FileStation.Search", "list", version=2, + params={"taskid": taskid, "limit": limit}) + + if list_result: + files = list_result.get("files", []) + return { + "success": True, + "pattern": pattern, + "results": files, + "total": list_result.get("total", len(files)) + } + + # Если не удалось получить результаты, останавливаем поиск + self._make_api_request("SYNO.FileStation.Search", "stop", version=2, + params={"taskid": taskid}) + + return { + "success": False, + "error": "search_timeout", + "progress": search_result["progress"] + } + + except Exception as e: + logger.error(f"Error searching files: {str(e)}") + return {"success": False, "error": str(e)} + + def get_backup_status(self) -> Dict[str, Any]: + """Получение информации о резервном копировании""" + logger.info("Getting backup status information") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for backup status request") + return {"success": False, "error": "authentication_failed"} + + try: + # Пробуем получить информацию о Hyper Backup + hyper_result = self._make_api_request("SYNO.Backup.Repository", "list", version=1) + + # Пробуем получить информацию о задачах Time Backup + time_result = self._make_api_request("SYNO.BackupService.Task", "list", version=1) + + # Проверяем статус резервного копирования USB + usb_result = self._make_api_request("SYNO.USBCopy", "status", version=1) + + backups = { + "hyper_backup": hyper_result.get("backups", []) if hyper_result else [], + "time_backup": time_result.get("tasks", []) if time_result else [], + "usb_copy": {"enabled": usb_result.get("enable", False) if usb_result else False} + } + + return { + "success": True, + "backups": backups, + "available_apis": { + "hyper_backup": hyper_result is not None, + "time_backup": time_result is not None, + "usb_copy": usb_result is not None + } + } + + except Exception as e: + logger.error(f"Error getting backup status: {str(e)}") + return {"success": False, "error": str(e)} + + def check_for_updates(self) -> Dict[str, Any]: + """Проверка наличия обновлений системы""" + logger.info("Checking for system updates") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for update check") + return {"success": False, "error": "authentication_failed"} + + try: + # Получаем текущую информацию о системе + system_info = self._make_api_request("SYNO.DSM.Info", "getinfo", version=2) + + if not system_info: + logger.error("Failed to get system info for update check") + return {"success": False, "error": "api_error"} + + # Проверяем наличие обновлений + update_result = self._make_api_request("SYNO.Core.Upgrade.Server", "check", version=1) + + # Получаем настройки автоматического обновления + settings_result = self._make_api_request("SYNO.Core.Upgrade.Server.Setting", "get", version=1) + + # Получаем информацию о доступных обновлениях + update_info = self._make_api_request("SYNO.Core.Upgrade.Server.Download", "list", version=1) + + current_version = system_info.get("version_string", "unknown") + auto_update_enabled = settings_result.get("auto_update", False) if settings_result else False + + updates = [] + if update_info and "updates" in update_info: + updates = update_info.get("updates", []) + + update_available = len(updates) > 0 + + return { + "success": True, + "current_version": current_version, + "update_available": update_available, + "auto_update_enabled": auto_update_enabled, + "updates": updates + } + + except Exception as e: + logger.error(f"Error checking for updates: {str(e)}") + return {"success": False, "error": str(e)} + + def get_quota_info(self) -> Dict[str, Any]: + """Получение информации о квотах пользователей""" + logger.info("Getting user quota information") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for quota info request") + return {"success": False, "error": "authentication_failed"} + + try: + # Получаем список пользователей + users_result = self._make_api_request("SYNO.Core.User", "list", version=1) + + if not users_result: + logger.error("Failed to get user list for quota info") + return {"success": False, "error": "api_error"} + + users = users_result.get("users", []) + user_quotas = [] + + # Получаем квоты для каждого пользователя + for user in users: + user_name = user.get("name") + if not user_name: + continue + + quota_result = self._make_api_request("SYNO.Core.Quota", "get", version=1, + params={"user_name": user_name}) + + if quota_result and "quotas" in quota_result: + user_quotas.append({ + "user": user_name, + "quotas": quota_result.get("quotas", []) + }) + + return { + "success": True, + "user_quotas": user_quotas + } + + except Exception as e: + logger.error(f"Error getting quota info: {str(e)}") + return {"success": False, "error": str(e)} diff --git a/.history/src/api/synology_20250830092441.py b/.history/src/api/synology_20250830092441.py new file mode 100644 index 0000000..25d086c --- /dev/null +++ b/.history/src/api/synology_20250830092441.py @@ -0,0 +1,1903 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Модуль для взаимодействия с API Synology NAS +""" + +import requests +from requests.adapters import HTTPAdapter +import json +import logging +import time +import urllib3 +from urllib3.util import Retry +from typing import Dict, Any, Optional, List +import socket +import struct +from time import sleep + +from src.config.config import ( + SYNOLOGY_HOST, + SYNOLOGY_PORT, + SYNOLOGY_USERNAME, + SYNOLOGY_PASSWORD, + SYNOLOGY_SECURE, + SYNOLOGY_TIMEOUT, + SYNOLOGY_MAC, + WOL_PORT, + SYNOLOGY_API_VERSION, + SYNOLOGY_POWER_API, + SYNOLOGY_INFO_API +) +from src.api.api_discovery import discover_available_apis, find_compatible_api + +# Отключение предупреждений о небезопасных SSL-соединениях +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + +logger = logging.getLogger(__name__) + +class SynologyAPI: + """Класс для взаимодействия с API Synology NAS""" + + def __init__(self): + """Инициализация класса SynologyAPI""" + logger.info("Creating API with auto-retry and connection pool") + logger.debug(f"Connection details: Host={SYNOLOGY_HOST}, Port={SYNOLOGY_PORT}, Secure={SYNOLOGY_SECURE}") + + self.protocol = "https" if SYNOLOGY_SECURE else "http" + self.base_url = f"{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi" + self.sid = None + self.session = requests.Session() + + # Настройка SSL + if self.protocol == "https": + logger.debug("SSL enabled, disabling certificate verification for internal network") + self.session.verify = False # Отключаем проверку SSL для внутренней сети + + # Добавляем пользовательские заголовки для улучшения совместимости с API + custom_headers = { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36', + 'Accept': 'application/json, text/javascript, */*; q=0.01', + 'Accept-Language': 'ru-RU,ru;q=0.9,en-US;q=0.8,en;q=0.7', + 'X-Requested-With': 'XMLHttpRequest', + 'Connection': 'keep-alive', + 'Referer': f'{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/' + } + self.session.headers.update(custom_headers) + logger.debug("Added browser-like headers for API compatibility") + + # Добавляем повторные попытки для HTTP-запросов + retry_strategy = Retry( + total=5, # Увеличиваем количество попыток + status_forcelist=[429, 500, 502, 503, 504, 404], + allowed_methods=["GET", "POST"], + backoff_factor=1.5, # Увеличиваем задержку между попытками + respect_retry_after_header=True + ) + adapter = HTTPAdapter( + max_retries=retry_strategy, + pool_connections=3, + pool_maxsize=10 + ) + self.session.mount("http://", adapter) + self.session.mount("https://", adapter) + + # Таймауты будут указаны в запросах + self.default_timeout = (SYNOLOGY_TIMEOUT, SYNOLOGY_TIMEOUT*2) + logger.debug(f"Setting default request timeout: {self.default_timeout}") + + # Кэш для хранения результатов запросов + self._cache = {} + self._cache_ttl = {} + self._last_online_check = 0 + self._last_online_status = False + self._online_check_interval = 30 # Интервал для кэширования проверок онлайн-статуса + + # Время последней успешной аутентификации и срок действия сессии + self._last_auth_time = 0 + self._auth_expiry = 3600 # По умолчанию 1 час + + # Информация о доступных API + self._available_apis = {} + self._api_info_ttl = 0 + + # Инициализируем API version resolver для автоматического определения совместимых API + self.api_resolver = None # Будет создан при необходимости + + def login(self) -> bool: + """Авторизация в API Synology NAS""" + # Сбрасываем SID для новой сессии + self.sid = None + + logger.info("Attempting to authenticate with Synology NAS...") + logger.debug(f"Base URL: {self.base_url}") + + # Проверяем доступность NAS перед запросом с помощью прямой TCP-проверки + # Избегаем вызова is_online(), чтобы не создавать рекурсию + online_status = self._check_tcp_connection() + if not online_status: + logger.error("Cannot login: Synology NAS is not reachable") + return False + + # Пробуем различные версии API для аутентификации + # Начинаем с версии 3, которая показала лучшую совместимость в тестах + auth_versions_to_try = [3, 2, 1, 6] # Пробуем сначала более стабильные версии + + for auth_version in auth_versions_to_try: + try: + # Определяем путь к API аутентификации + auth_path = "entry.cgi" # Используем entry.cgi вместо auth.cgi по умолчанию + + # Проверка информации API для определения доступных версий API + api_info_url = f"{self.base_url}/entry.cgi" + api_info_params = { + "api": "SYNO.API.Info", + "version": "1", + "method": "query", + "query": "SYNO.API.Auth" + } + + logger.debug(f"Querying API info for auth version {auth_version}") + try: + api_info_response = self.session.get( + api_info_url, + params=api_info_params, + timeout=self.default_timeout, + verify=False + ) + + if api_info_response.status_code == 200: + api_info_data = api_info_response.json() + if api_info_data.get("success"): + auth_info = api_info_data.get("data", {}).get("SYNO.API.Auth", {}) + max_version = auth_info.get("maxVersion", 6) + min_version = auth_info.get("minVersion", 1) + auth_path = auth_info.get("path", "entry.cgi") + logger.debug(f"Auth API versions: min={min_version}, max={max_version}, path={auth_path}") + + # Проверяем поддержку текущей версии + if auth_version < min_version or auth_version > max_version: + logger.warning(f"Auth API version {auth_version} not supported (min={min_version}, max={max_version}), trying next version") + continue + else: + logger.warning("Failed to query API info, using default auth path") + else: + logger.warning(f"API info request failed with status {api_info_response.status_code}, using default auth path") + except Exception as e: + logger.warning(f"Error querying API info: {str(e)}, using default auth path") + + # Основной запрос авторизации + url = f"{self.base_url}/{auth_path}" + params = { + "api": "SYNO.API.Auth", + "version": str(auth_version), + "method": "login", + "account": SYNOLOGY_USERNAME, + "passwd": SYNOLOGY_PASSWORD, + "session": "SynologyPowerControlBot", + "format": "cookie" + } + + # Для версии 6+ используем немного другой формат + if auth_version >= 6: + params["enable_syno_token"] = "yes" + + logger.debug(f"Sending auth request to {url} with API version {auth_version}") + start_time = time.time() + response = self.session.get( + url, + params=params, + timeout=self.default_timeout, + verify=False + ) + elapsed_time = time.time() - start_time + logger.debug(f"Auth request completed in {elapsed_time:.2f}s with status code {response.status_code}") + + # Проверка статуса HTTP + if response.status_code != 200: + logger.error(f"HTTP error: {response.status_code}") + logger.debug(f"Response content: {response.text[:200]}") + continue # Пробуем следующую версию + + try: + data = response.json() + except json.JSONDecodeError: + logger.error("Failed to decode JSON response") + logger.debug(f"Response content: {response.text[:200]}") + continue # Пробуем следующую версию + + if data.get("success"): + self.sid = data.get("data", {}).get("sid") + self._last_auth_time = time.time() + logger.info(f"Successfully logged in to Synology NAS using API version {auth_version}") + logger.debug(f"Session ID obtained: {self.sid[:5] if self.sid else 'None'}...") + + # Получаем и сохраняем токен SYNO, если он есть + syno_token = data.get("data", {}).get("synotoken") + if syno_token: + self.session.headers.update({'X-SYNO-TOKEN': syno_token}) + logger.debug("Added X-SYNO-TOKEN header for improved API compatibility") + + # Также добавляем SID в cookies для улучшения совместимости + self.session.cookies.update({ + 'id': self.sid, + 'sid': self.sid + }) + logger.debug("Added SID to session cookies for improved compatibility") + + # Проверка валидности полученной сессии с помощью простого запроса + # Будем использовать SYNO.API.Info без проверки сложных методов + + # Даем системе немного времени для инициализации сессии + time.sleep(0.5) + + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to log in with version {auth_version}: Error code {error_code}") + + # Если ошибка связана с версией API, пробуем следующую версию + if error_code in [104, 105]: + logger.warning(f"Auth version {auth_version} not supported, trying next version") + continue + + # Дополнительная диагностика + if error_code == 400: + logger.error("Authentication error: Invalid credentials") + elif error_code == 401: + logger.error("Authentication error: Account disabled") + elif error_code == 402: + logger.error("Authentication error: Permission denied") + elif error_code == 403: + logger.error("Authentication error: 2-factor authentication required") + elif error_code == 404: + logger.error("Authentication error: Failed to authenticate with 2-factor authentication") + + # Если ошибка связана с неверными учетными данными, нет смысла пробовать другие версии API + if error_code in [400, 401, 402, 403, 404]: + return False + + except requests.exceptions.Timeout: + logger.error(f"Connection timeout during auth with version {auth_version}") + continue # Пробуем следующую версию + except requests.exceptions.ConnectionError as e: + logger.error(f"Connection error during auth with version {auth_version}: {str(e)}") + continue # Пробуем следующую версию + except requests.RequestException as e: + logger.error(f"Request error during auth with version {auth_version}: {str(e)}") + continue # Пробуем следующую версию + except Exception as e: + logger.error(f"Unexpected error during login with version {auth_version}: {str(e)}", exc_info=True) + continue # Пробуем следующую версию + + # Если все версии не сработали + logger.error("Failed to authenticate with any API version") + return False + + def _validate_session(self) -> bool: + """Проверяет валидность сессии после авторизации""" + if not self.sid: + return False + + # Попробуем сделать простой запрос для проверки сессии + test_apis = [ + {"api": "SYNO.Core.System", "method": "info", "version": 1}, + {"api": "SYNO.DSM.Info", "method": "getinfo", "version": 1} + ] + + for test_api in test_apis: + try: + url = f"{self.base_url}/entry.cgi" + params = { + "api": test_api["api"], + "version": str(test_api["version"]), + "method": test_api["method"], + "_sid": self.sid + } + + response = self.session.get(url, params=params, timeout=self.default_timeout, verify=False) + + if response.status_code == 200: + data = response.json() + if data.get("success"): + logger.debug(f"Session validation successful using {test_api['api']}") + return True + else: + error_code = data.get("error", {}).get("code", -1) + if error_code != 119: # Не сессия истекла + logger.debug(f"Session validation with {test_api['api']} returned error code {error_code}") + return True # Считаем сессию валидной, если ошибка не связана с истечением сессии + except Exception as e: + logger.warning(f"Error during session validation with {test_api['api']}: {str(e)}") + + logger.warning("Session validation failed with all test APIs") + return False + + def logout(self) -> bool: + """Выход из API Synology NAS""" + if not self.sid: + return True + + try: + url = f"{self.base_url}/auth.cgi" + params = { + "api": "SYNO.API.Auth", + "version": "1", + "method": "logout", + "session": "SynologyPowerControlBot", + "_sid": self.sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + self.sid = None + logger.info("Successfully logged out from Synology NAS") + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to log out from Synology NAS: Error code {error_code}") + return False + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + return False + + def _make_api_request(self, api_name: str, method: str, version: int = 1, + params: Optional[Dict[str, Any]] = None, retry_auth: bool = True, retry_count: int = 0) -> Optional[Dict[str, Any]]: + """Обобщенный метод для выполнения API запросов с обработкой ошибок""" + # Ограничение на количество повторных попыток + if retry_count >= 3: + logger.error(f"Too many retries for {api_name}.{method}, giving up") + return None + + # Проверка наличия авторизации + if not self.sid and not self.login(): + logger.error(f"Not authenticated for API request: {api_name}.{method}") + return None + + # Проверка информации API для определения пути и поддерживаемой версии + api_info_url = f"{self.base_url}/entry.cgi" + api_info_params = { + "api": "SYNO.API.Info", + "version": "1", + "method": "query", + "query": api_name + } + + api_path = "entry.cgi" + try: + logger.debug(f"Querying API info for {api_name}") + api_info_response = self.session.get( + api_info_url, + params=api_info_params, + timeout=self.default_timeout, + verify=False + ) + + if api_info_response.status_code == 200: + api_info_data = api_info_response.json() + if api_info_data.get("success"): + api_info = api_info_data.get("data", {}).get(api_name, {}) + if api_info: + max_version = api_info.get("maxVersion", version) + min_version = api_info.get("minVersion", version) + api_path = api_info.get("path", "entry.cgi") + + # Проверка, поддерживается ли запрошенная версия + if version < min_version: + logger.warning(f"API version {version} for {api_name} is below minimum {min_version}, using {min_version}") + version = min_version + elif version > max_version: + logger.warning(f"API version {version} for {api_name} exceeds maximum {max_version}, using {max_version}") + version = max_version + + logger.debug(f"Using API path: {api_path}, version: {version}") + else: + logger.warning(f"API {api_name} not found in API info, using defaults") + except Exception as e: + logger.warning(f"Error querying API info for {api_name}: {str(e)}, using defaults") + + # Подготовка базовых параметров запроса + base_params = { + "api": api_name, + "version": str(version), + "method": method, + "_sid": self.sid # Используем _sid вместо sid для лучшей совместимости + } + + # Добавление дополнительных параметров, если они заданы + if params: + base_params.update(params) + + url = f"{self.base_url}/{api_path}" + logger.debug(f"API request: {api_name}.{method} v{version} to {url}") + logger.debug(f"Full request params: {base_params}") + + try: + start_time = time.time() + response = self.session.get( + url, + params=base_params, + timeout=self.default_timeout, + verify=False + ) + elapsed_time = time.time() - start_time + logger.debug(f"API request completed in {elapsed_time:.2f}s with status {response.status_code}") + + # Проверка статуса HTTP + if response.status_code != 200: + logger.error(f"HTTP error: {response.status_code} for {api_name}.{method}") + + # Повторная попытка при ошибках соединения + if response.status_code in [500, 502, 503, 504]: + logger.info(f"Server error, retrying request for {api_name}.{method}") + sleep(2) + return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1) + + return None + + try: + data = response.json() + except json.JSONDecodeError: + logger.error(f"Failed to decode JSON response for {api_name}.{method}") + logger.debug(f"Response content: {response.text[:200]}") + + # Повторная попытка при ошибках декодирования + logger.info(f"JSON decode error, retrying request for {api_name}.{method}") + sleep(2) + return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1) + + if data.get("success"): + logger.info(f"API request successful for {api_name}.{method}") + return data.get("data", {}) + else: + error_code = data.get("error", {}).get("code", -1) + error_desc = self._get_error_description(error_code) + logger.error(f"API error for {api_name}.{method}: {error_code} - {error_desc}") + + # Ошибки доступа или прав часто встречаются, но они не критичные + # Например, ошибка 102 означает, что нет прав, но NAS доступен + if error_code in [102, 103, 104, 105]: + logger.warning(f"Permission-related error for {api_name}.{method}: {error_code}") + # Возвращаем пустой словарь вместо None, + # чтобы вызывающий код мог понять, что запрос выполнен + return {} + + # Если ошибка связана с авторизацией и нам разрешено повторить попытку + if error_code in [106, 107, 119] and retry_auth: + logger.info(f"Session error (code {error_code}), creating fresh session...") + self.sid = None # Сбрасываем SID + + # Для ошибки 119 (Session timeout) дадим системе немного времени + if error_code == 119: + logger.info("Session timeout detected, waiting before retry...") + sleep(3) + + if self.login(): + logger.info("Re-authenticated with fresh session, retrying API request...") + # Рекурсивный вызов, но со счетчиком повторов + return self._make_api_request(api_name, method, version, params, False, retry_count + 1) + + # Для некоторых ошибок можно автоматически повторить запрос + if error_code in [408, 429, 500, 502, 503, 504]: + logger.info(f"Temporary error {error_code}, retrying request for {api_name}.{method}") + sleep(2) + return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1) + + return None + + except requests.exceptions.Timeout: + logger.error(f"Request timeout for {api_name}.{method}") + + # Повторная попытка при таймауте + if retry_count < 2: + logger.info(f"Timeout, retrying request for {api_name}.{method}") + sleep(2) + return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1) + + return None + except requests.exceptions.ConnectionError as e: + logger.error(f"Connection error for {api_name}.{method}: {str(e)}") + + # Повторная попытка при ошибке соединения + if retry_count < 2: + logger.info(f"Connection error, retrying request for {api_name}.{method}") + sleep(2) + return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1) + + return None + except Exception as e: + logger.error(f"Unexpected error during {api_name}.{method}: {str(e)}") + return None + + def get_system_status(self) -> Dict[str, Any]: + """Получение статуса системы""" + # Проверяем доступность системы + if not self.is_online(): + logger.info("Device is offline, skipping API request") + return {"status": "offline"} + + # Проверяем, есть ли кэшированный результат + cache_key = "system_status" + current_time = time.time() + if cache_key in self._cache and current_time - self._cache_ttl.get(cache_key, 0) < 60: + logger.debug("Using cached system status") + return self._cache[cache_key] + + # Используем рекомендованный API для получения информации о системе + logger.info(f"Getting system info using {SYNOLOGY_INFO_API} v{SYNOLOGY_API_VERSION}") + + # Для SYNO.DSM.Info метод getinfo, для других обычно используется get или info + if SYNOLOGY_INFO_API == "SYNO.DSM.Info": + method = "getinfo" + else: + method = "get" + + result = self._make_api_request(SYNOLOGY_INFO_API, method, version=SYNOLOGY_API_VERSION) + + if result: + logger.info(f"Successfully retrieved system info using {SYNOLOGY_INFO_API}") + + # Формируем расширенный ответ с дополнительной информацией + system_info = { + "status": "online", + "hostname": result.get("hostname", "unknown"), + "model": result.get("model", "unknown"), + "version": result.get("version", "unknown"), + "uptime": result.get("uptime", 0), + "time": current_time, + "is_online": True, + "api_used": SYNOLOGY_INFO_API + } + + # Сохраняем в кэше + self._cache[cache_key] = system_info + self._cache_ttl[cache_key] = current_time + + return system_info + + # Если основной API не сработал, пробуем резервные варианты + logger.warning(f"Failed to retrieve system info with {SYNOLOGY_INFO_API}, trying fallback APIs") + + # Пробуем резервные API + apis_to_try = [ + {"name": "SYNO.DSM.Info", "method": "getinfo", "version": 2}, + {"name": "SYNO.Core.System", "method": "info", "version": 1}, + {"name": "SYNO.Core.System.Status", "method": "get", "version": 1}, + {"name": "SYNO.Core.System.Info", "method": "get", "version": 1}, + ] + + for api in apis_to_try: + if api["name"] == SYNOLOGY_INFO_API: + continue # Пропускаем уже проверенный API + + logger.info(f"Trying to get system info using {api['name']}.{api['method']} v{api['version']}") + result = self._make_api_request(api["name"], api["method"], version=api["version"]) + + if result: + logger.info(f"Successfully retrieved system info using {api['name']}") + + # Формируем расширенный ответ с дополнительной информацией + system_info = { + "status": "online", + "hostname": result.get("hostname", "unknown"), + "model": result.get("model", "unknown"), + "version": result.get("version", "unknown"), + "uptime": result.get("uptime", 0), + "time": current_time, + "is_online": True, + "api_used": api["name"] + } + + # Сохраняем в кэше + self._cache[cache_key] = system_info + self._cache_ttl[cache_key] = current_time + + return system_info + + # Если все запросы не удались, но система онлайн, возвращаем базовую информацию + logger.warning("Failed to retrieve system info with all API methods") + return { + "status": "error", + "error": "Failed to fetch system information", + "is_online": True, + "time": current_time + } + + def shutdown_system(self) -> bool: + """Выключение системы""" + # Проверяем, включено ли устройство перед попыткой его выключить + if not self.is_online(force_check=True): + logger.info("Synology NAS is already offline, no need to shut down") + return True + + logger.info("Attempting to shutdown Synology NAS...") + + # Попробуем сначала использовать предпочтительный API для управления питанием + logger.info(f"Trying shutdown with {SYNOLOGY_POWER_API} v{SYNOLOGY_API_VERSION}") + + # Для SYNO.Core.Hardware.PowerRecovery используется метод setPowerOnState + # Для других API обычно используется метод shutdown или reboot + if SYNOLOGY_POWER_API == "SYNO.Core.Hardware.PowerRecovery": + # Для этого API нужны специальные параметры + params = {"state": "powerbtn"} # powerbtn имитирует нажатие кнопки питания + result = self._make_api_request(SYNOLOGY_POWER_API, "setPowerOnState", version=SYNOLOGY_API_VERSION, params=params) + else: + # Пробуем стандартный метод + result = self._make_api_request(SYNOLOGY_POWER_API, "shutdown", version=SYNOLOGY_API_VERSION) + + if result is not None: + logger.info(f"Successfully initiated system shutdown using {SYNOLOGY_POWER_API}") + + # Даем системе время начать процесс выключения + logger.info("Waiting for shutdown to initialize...") + sleep(5) + + # Проверяем статус + if not self.is_online(force_check=True): + logger.info("System is now offline. Shutdown confirmed successful.") + return True + else: + logger.info("System still appears to be online, but shutdown may be in progress.") + return True + else: + logger.warning(f"Failed to shutdown using {SYNOLOGY_POWER_API}, trying alternative methods") + + # Если не сработал основной метод, пробуем резервные варианты + # Проверка всех доступных методов API для выключения + apis_to_try = [ + {"name": "SYNO.Core.System", "method": "shutdown", "version": 3}, + {"name": "SYNO.Core.System.Power", "method": "shutdown", "version": 1}, + {"name": "SYNO.DSM.Power", "method": "shutdown", "version": 1}, + {"name": "SYNO.System.Power", "method": "shutdown", "version": 1}, + {"name": "SYNO.DSM.System", "method": "shutdown", "version": 1} + ] + + # Проверяем доступные API + try: + api_info_url = f"{self.base_url}/entry.cgi" + api_info_params = { + "api": "SYNO.API.Info", + "version": "1", + "method": "query", + "query": "SYNO.Core.System.Power,SYNO.DSM.Power,SYNO.Core.System,SYNO.System.Power,SYNO.DSM.System" + } + + logger.debug("Checking available shutdown APIs") + api_info_response = self.session.get( + api_info_url, + params=api_info_params, + timeout=self.default_timeout, + verify=False + ) + + if api_info_response.status_code == 200: + api_info_data = api_info_response.json() + if api_info_data.get("success"): + available_apis = api_info_data.get("data", {}) + logger.debug(f"Available APIs: {list(available_apis.keys())}") + + # Фильтруем только доступные API + filtered_apis = [] + for api in apis_to_try: + if api["name"] in available_apis: + api_info = available_apis[api["name"]] + # Проверка версии API + if api_info.get("minVersion", 1) <= api["version"] <= api_info.get("maxVersion", 1): + filtered_apis.append(api) + logger.debug(f"Adding {api['name']} to available shutdown APIs") + else: + logger.debug(f"API {api['name']} version {api['version']} not supported. Supported: {api_info.get('minVersion')}-{api_info.get('maxVersion')}") + + if filtered_apis: + apis_to_try = filtered_apis + else: + logger.warning("No compatible APIs found, trying all methods as fallback") + else: + logger.warning("Failed to query API info, using default methods") + else: + logger.warning(f"API info request failed with status {api_info_response.status_code}, using default methods") + except Exception as e: + logger.warning(f"Error querying API info: {str(e)}, using default methods") + + # Пробуем все доступные методы по порядку + for api in apis_to_try: + logger.info(f"Trying shutdown with {api['name']}.{api['method']} v{api['version']}") + result = self._make_api_request(api["name"], api["method"], version=api["version"]) + + if result is not None: + logger.info(f"Successfully initiated system shutdown using {api['name']}") + + # Даем системе время начать процесс выключения + logger.info("Waiting for shutdown to initialize...") + sleep(5) + + # Проверяем статус + if not self.is_online(force_check=True): + logger.info("System is now offline. Shutdown confirmed successful.") + return True + else: + logger.info("System still appears to be online, but shutdown may be in progress.") + return True + else: + logger.warning(f"Failed to shutdown using {api['name']}.{api['method']} v{api['version']}") + + # Если ни один метод не сработал, но система стала недоступна + if not self.is_online(force_check=True): + logger.info("System appears to be shutting down despite API errors") + return True + + logger.error("Failed to shutdown system after trying multiple APIs") + return False + + def reboot_system(self) -> bool: + """Перезагрузка системы""" + # Проверяем, включена ли система + if not self.is_online(force_check=True): + logger.error("Cannot reboot: System is offline") + return False + + logger.info("Attempting to reboot Synology NAS...") + + # Попробуем сначала использовать предпочтительный API для управления питанием + logger.info(f"Trying reboot with {SYNOLOGY_POWER_API} v{SYNOLOGY_API_VERSION}") + + # Для SYNO.Core.Hardware.PowerRecovery используется метод setPowerOnState или powerButton + # Для других API обычно используется метод restart или reboot + if SYNOLOGY_POWER_API == "SYNO.Core.Hardware.PowerRecovery": + # Для этого API нужны специальные параметры + params = {"reboot": "true"} # Передаем флаг для перезагрузки + result = self._make_api_request(SYNOLOGY_POWER_API, "setPowerOnState", version=SYNOLOGY_API_VERSION, params=params) + else: + # Пробуем стандартный метод + methods_to_try = ["restart", "reboot"] + result = None + for method in methods_to_try: + result = self._make_api_request(SYNOLOGY_POWER_API, method, version=SYNOLOGY_API_VERSION) + if result is not None: + logger.info(f"Successfully used method {method} for {SYNOLOGY_POWER_API}") + break + + if result is not None: + logger.info(f"Successfully initiated system reboot using {SYNOLOGY_POWER_API}") + + # Даем системе время начать процесс перезагрузки + logger.info("Waiting for reboot to initialize...") + sleep(5) + + # Ждем, пока система станет недоступна (признак перезагрузки) + reboot_started = False + for i in range(12): # Проверяем в течение 60 секунд (12 * 5сек) + if not self.is_online(force_check=True): + logger.info(f"System went offline after {i*5} seconds, reboot in progress") + reboot_started = True + break + logger.debug(f"System still online, waiting... ({i+1}/12)") + sleep(5) + + if reboot_started: + # Ждем, пока система снова станет доступна + logger.info("Waiting for system to come back online...") + return self.wait_for_boot(max_attempts=30, delay=10) + else: + logger.warning("System did not go offline after reboot command, but command was accepted") + # Даже если система не ушла оффлайн, команда могла быть принята + return True + else: + logger.warning(f"Failed to reboot using {SYNOLOGY_POWER_API}, trying alternative methods") + + # Если не сработал основной метод, пробуем резервные варианты + # Проверка всех доступных методов API для перезагрузки + apis_to_try = [ + {"name": "SYNO.Core.System.Power", "method": "restart", "version": 1}, + {"name": "SYNO.DSM.Power", "method": "reboot", "version": 1}, + {"name": "SYNO.Core.System", "method": "reboot", "version": 3}, + {"name": "SYNO.System.Power", "method": "reboot", "version": 1}, + {"name": "SYNO.DSM.System", "method": "reboot", "version": 1}, + {"name": "SYNO.Core.Hardware.NeedReboot", "method": "reboot", "version": 1} + ] + + # Получаем список доступных API + try: + api_info_url = f"{self.base_url}/entry.cgi" + api_info_params = { + "api": "SYNO.API.Info", + "version": "1", + "method": "query", + "query": "SYNO.Core.System.Power,SYNO.DSM.Power,SYNO.Core.System,SYNO.System.Power,SYNO.DSM.System,SYNO.Core.Hardware.NeedReboot" + } + + logger.debug("Checking available reboot APIs") + api_info_response = self.session.get( + api_info_url, + params=api_info_params, + timeout=self.default_timeout, + verify=False + ) + + if api_info_response.status_code == 200: + api_info_data = api_info_response.json() + if api_info_data.get("success"): + available_apis = api_info_data.get("data", {}) + logger.debug(f"Available APIs: {list(available_apis.keys())}") + + # Фильтруем только доступные API + filtered_apis = [] + for api in apis_to_try: + if api["name"] in available_apis: + api_info = available_apis[api["name"]] + # Проверка версии API + if api_info.get("minVersion", 1) <= api["version"] <= api_info.get("maxVersion", 1): + filtered_apis.append(api) + logger.debug(f"Adding {api['name']} to available reboot APIs") + else: + logger.debug(f"API {api['name']} version {api['version']} not supported. Supported: {api_info.get('minVersion')}-{api_info.get('maxVersion')}") + + if filtered_apis: + apis_to_try = filtered_apis + else: + logger.warning("No compatible APIs found, trying all methods as fallback") + else: + logger.warning("Failed to query API info, using default methods") + else: + logger.warning(f"API info request failed with status {api_info_response.status_code}, using default methods") + except Exception as e: + logger.warning(f"Error querying API info: {str(e)}, using default methods") + + # Пробуем все доступные методы по порядку + for api in apis_to_try: + logger.info(f"Trying reboot with {api['name']}.{api['method']} v{api['version']}") + result = self._make_api_request(api["name"], api["method"], version=api["version"]) + + if result is not None: + logger.info(f"Successfully initiated system reboot using {api['name']}") + + # Даем системе время начать процесс перезагрузки + logger.info("Waiting for reboot to initialize...") + sleep(5) + + # Ждем, пока система станет недоступна (признак перезагрузки) + reboot_started = False + for i in range(12): # Проверяем в течение 60 секунд (12 * 5сек) + if not self.is_online(force_check=True): + logger.info(f"System went offline after {i*5} seconds, reboot in progress") + reboot_started = True + break + logger.debug(f"System still online, waiting... ({i+1}/12)") + sleep(5) + + if reboot_started: + # Ждем, пока система снова станет доступна + logger.info("Waiting for system to come back online...") + return self.wait_for_boot(max_attempts=30, delay=10) + else: + logger.warning("System did not go offline after reboot command, but command was accepted") + # Даже если система не ушла оффлайн, команда могла быть принята + return True + else: + logger.warning(f"Failed to reboot using {api['name']}.{api['method']} v{api['version']}") + + logger.error("Failed to reboot system after trying multiple APIs") + return False + + def _get_error_description(self, error_code: int) -> str: + """Получение описания ошибки по коду""" + error_descriptions = { + 100: "Unknown error", + 101: "Invalid parameter", + 102: "API does not exist", + 103: "Method does not exist", + 104: "Version does not support", + 105: "Permission denied", + 106: "Session timeout", + 107: "Session interrupted by duplicate login", + 400: "Invalid credentials", + 401: "Account disabled", + 402: "Permission denied", + 403: "2FA required", + 404: "Failed to authenticate with 2FA" + } + return error_descriptions.get(error_code, "Unknown error code") + + def _check_tcp_connection(self) -> bool: + """Проверка базового TCP-соединения с Synology NAS""" + try: + socket_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + socket_obj.settimeout(SYNOLOGY_TIMEOUT) + result = socket_obj.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT)) + socket_obj.close() + + return result == 0 + except socket.error as e: + logger.error(f"Socket error during connection check: {str(e)}") + return False + except Exception as e: + logger.error(f"Unexpected error during connection check: {str(e)}") + return False + + def is_online(self, force_check=False) -> bool: + """Проверка онлайн-статуса Synology NAS""" + # Используем кэшированное значение, если доступно и не устарело + current_time = time.time() + if not force_check and (current_time - self._last_online_check) < self._online_check_interval: + logger.debug(f"Using cached online status: {self._last_online_status}") + return self._last_online_status + + logger.info("Checking if NAS is online...") + + # Проверяем TCP-соединение + online_status = self._check_tcp_connection() + logger.info(f"Detected Synology NAS online status: {online_status}") + + # Если TCP-соединение успешно и у нас есть действующий SID, + # попробуем более детальную проверку через API + if online_status and self.sid: + logger.info("Trying to fetch more detailed online status through API...") + + # Пробуем разные API для проверки онлайн-статуса + api_checks = [ + {"api": "SYNO.DSM.Info", "version": "2", "method": "getinfo"}, + {"api": "SYNO.Core.System", "version": "1", "method": "info"}, + {"api": "SYNO.Core.System.Status", "version": "1", "method": "get"} + ] + + api_success = False + for api_check in api_checks: + try: + url = f"{self.base_url}/entry.cgi" + params = { + "api": api_check["api"], + "version": api_check["version"], + "method": api_check["method"], + "sid": self.sid + } + + logger.debug(f"Trying online status check with {api_check['api']}") + response = self.session.get( + url, + params=params, + timeout=self.default_timeout, + verify=False + ) + + if response.status_code == 200: + data = response.json() + if data.get("success"): + logger.info(f"API request successful for {api_check['api']}") + logger.info("Synology NAS is online with API access") + api_success = True + break + else: + error_code = data.get("error", {}).get("code", -1) + logger.warning(f"API response indicates an error: {error_code}, but NAS is reachable") + else: + logger.warning(f"API returned status code {response.status_code}") + except Exception as e: + logger.warning(f"API check failed for {api_check['api']}, but TCP connection succeeded: {str(e)}") + + if not api_success: + logger.warning("All API checks failed, but TCP connection is successful") + + # Обновляем кэшированное значение + self._last_online_check = current_time + self._last_online_status = online_status + + return online_status + + def wake_on_lan(self) -> bool: + """Отправка Wake-on-LAN пакета для включения Synology NAS""" + if not SYNOLOGY_MAC: + logger.error("MAC address not configured") + return False + + try: + # Преобразование MAC-адреса в байты + mac_address = SYNOLOGY_MAC.replace(':', '').replace('-', '') + if len(mac_address) != 12: + logger.error(f"Invalid MAC address format: {SYNOLOGY_MAC}") + return False + + try: + mac_bytes = bytes.fromhex(mac_address) + except ValueError as e: + logger.error(f"Failed to parse MAC address: {str(e)}") + logger.error(f"MAC address must contain only hex characters: {SYNOLOGY_MAC}") + return False + + # Создание Magic Packet + magic_packet = b'\xff' * 6 + mac_bytes * 16 + + # Отправка пакета на конкретный адрес + try: + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) + sock.sendto(magic_packet, (SYNOLOGY_HOST, WOL_PORT)) + sock.close() + logger.info(f"Wake-on-LAN packet sent to {SYNOLOGY_MAC} at {SYNOLOGY_HOST}:{WOL_PORT}") + except Exception as e: + logger.error(f"Error sending directed WoL packet: {str(e)}") + return False + + # Для надежности отправляем также широковещательный пакет + try: + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) + + # Используем стандартный широковещательный адрес + broadcast_addr = "255.255.255.255" + sock.sendto(magic_packet, (broadcast_addr, WOL_PORT)) + sock.close() + logger.info(f"Wake-on-LAN broadcast packet sent to {broadcast_addr}:{WOL_PORT}") + except Exception as e: + logger.warning(f"Error sending broadcast WoL packet (non-critical): {str(e)}") + # Не считаем ошибкой, т.к. основной пакет уже отправлен + + return True + + except Exception as e: + logger.error(f"Unexpected error in wake_on_lan: {str(e)}") + return False + + def wait_for_boot(self, max_attempts: int = 30, delay: int = 10) -> bool: + """Ожидание загрузки Synology NAS""" + logger.info(f"Waiting for Synology NAS to boot (timeout: {max_attempts * delay}s)...") + + for attempt in range(max_attempts): + # Принудительно проверяем статус без использования кэша + if self.is_online(force_check=True): + logger.info(f"Synology NAS is online after {attempt + 1} attempts ({(attempt + 1) * delay}s)") + + # Проверяем, что не только сеть доступна, но и API загрузился + api_ready = False + logger.info("Waiting for API services to initialize...") + + for api_check in range(5): # Даем еще до 50 секунд для загрузки API + if self.sid or self.login(): + api_ready = True + logger.info(f"API services are ready after {api_check + 1} attempts") + break + logger.debug(f"API not ready yet, waiting... ({api_check + 1}/5)") + sleep(10) + + if not api_ready: + logger.warning("System is online but API services may not be fully initialized") + + # Дадим дополнительное время для полной загрузки всех сервисов + sleep(delay) + return True + + sleep(delay) + logger.info(f"Waiting for boot... Attempt {attempt + 1}/{max_attempts}") + + logger.error(f"Synology NAS didn't come online after {max_attempts * delay} seconds") + return False + + def power_on(self) -> bool: + """Включение Synology NAS""" + # Принудительная проверка статуса + if self.is_online(force_check=True): + logger.info("Synology NAS is already online") + return True + + logger.info("Powering on Synology NAS via Wake-on-LAN...") + + # Проверяем, настроен ли MAC-адрес + if not SYNOLOGY_MAC: + logger.error("Cannot power on: MAC address not configured in settings") + return False + + # Пробуем отправить несколько WoL пакетов для надежности + success = False + for attempt in range(3): + logger.debug(f"Sending WoL packet, attempt {attempt + 1}/3") + if self.wake_on_lan(): + success = True + break + sleep(1) + + if not success: + logger.error("Failed to send Wake-on-LAN packets") + return False + + # Ожидание загрузки + logger.info("WoL packets sent successfully, waiting for system to boot...") + boot_result = self.wait_for_boot(max_attempts=30, delay=10) + + if boot_result: + # Проверяем доступность API после загрузки + system_status = self.get_system_status() + if system_status.get("status") == "online": + logger.info("System booted successfully with API access") + return True + else: + logger.warning("System appears to be online but API may not be fully ready") + return True + else: + logger.error("System did not come online after WoL") + return False + + def power_off(self) -> bool: + """Выключение Synology NAS""" + if not self.is_online(force_check=True): + logger.info("Synology NAS is already offline") + return True + + logger.info("Powering off Synology NAS...") + result = self.shutdown_system() + + if result: + # Дополнительная проверка, что система действительно выключилась + logger.info("Verifying system is offline...") + for attempt in range(12): # Проверяем в течение 2 минут (12 * 10 сек) + if not self.is_online(force_check=True): + logger.info(f"System confirmed offline after {attempt * 10} seconds") + return True + logger.debug(f"System still shutting down, checking again... ({attempt + 1}/12)") + sleep(10) + + logger.warning("System still appears to be online after shutdown command") + return False + else: + logger.error("Failed to initiate shutdown") + return False + + # Заглушки для расширенных методов + def get_shared_folders(self) -> List[Dict[str, Any]]: + """Получение списка общих папок""" + logger.info("Getting list of shared folders") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for shared folders request") + return [] + + try: + # Запрашиваем список общих папок через FileStation API + result = self._make_api_request("SYNO.FileStation.List", "list_share", version=2) + + if not result: + # Пробуем альтернативный API + logger.info("Trying alternative API for shared folders") + alt_result = self._make_api_request("SYNO.FileStation.List", "list_share", version=1) + if alt_result: + return alt_result.get("shares", []) + return [] + + return result.get("shares", []) + + except Exception as e: + logger.error(f"Error getting shared folders: {str(e)}") + return [] + + def get_system_load(self) -> Dict[str, Any]: + """Получение информации о загрузке системы""" + logger.info("Getting system load information") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for system load request") + return {} + + try: + # Запрашиваем информацию о загрузке системы + result = self._make_api_request("SYNO.Core.System.Utilization", "get", version=1) + + if not result: + # Пробуем альтернативный API + logger.info("Trying alternative API for system load") + alt_result = self._make_api_request("SYNO.Core.System", "info", version=3) + + if not alt_result: + return {} + + # Формируем из частичных данных + return { + "cpu_load": alt_result.get("cpu_usage", 0), + "memory": { + "total": alt_result.get("memory_size", 0), + "used": alt_result.get("memory_usage", 0), + "usage_percent": alt_result.get("memory_usage_percent", 0) + } + } + + # Формируем структурированный результат + return { + "cpu_load": result.get("cpu", {}).get("user", 0) + result.get("cpu", {}).get("system", 0), + "memory": result.get("memory", {}), + "network": result.get("network", {}) + } + + except Exception as e: + logger.error(f"Error getting system load: {str(e)}") + return {} + + def get_storage_status(self) -> Dict[str, Any]: + """Получение подробной информации о хранилище""" + logger.info("Getting storage status information") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for storage status request") + return {"error": "authentication_failed"} + + try: + # Пробуем получить информацию о хранилище через SYNO.Storage.CGI.Storage API + result = self._make_api_request("SYNO.Storage.CGI.Storage", "load_info", version=1) + + if not result: + # Пробуем альтернативный API + logger.info("Trying alternative API for storage info") + alt_result = self._make_api_request("SYNO.Core.Storage", "info", version=1) + + if not alt_result: + # Пробуем еще один альтернативный API + logger.info("Trying SYNO.Core.System API for storage info") + sys_result = self._make_api_request("SYNO.Core.System", "info", version=3) + + if not sys_result: + return { + "volumes": [], + "disks": [], + "total_size": 0, + "total_used": 0, + "error": "no_data" + } + + # Извлекаем базовую информацию о хранилище из системной информации + return { + "volumes": [], + "disks": [], + "total_size": sys_result.get("hdd_total_size", 0) * 1024**2, # МБ в байты + "total_used": sys_result.get("hdd_total_size", 0) * sys_result.get("hdd_usage", 0) / 100 * 1024**2, + } + + # Обрабатываем данные из альтернативного API + volumes = alt_result.get("volumes", []) + disks = alt_result.get("disks", []) + + else: + # Обрабатываем данные из основного API + volumes = result.get("volumes", []) + disks = result.get("disks", []) + + # Рассчитываем общие размеры + total_size = 0 + total_used = 0 + + for volume in volumes: + volume_size = volume.get("size", {}).get("total", 0) + volume_used = volume.get("size", {}).get("used", 0) + + total_size += volume_size + total_used += volume_used + + return { + "volumes": volumes, + "disks": disks, + "total_size": total_size, + "total_used": total_used + } + + except Exception as e: + logger.error(f"Error in get_storage_status: {str(e)}") + return {"error": str(e)} + + def get_security_status(self) -> Dict[str, Any]: + """Получение информации о состоянии безопасности""" + logger.info("Getting security status information") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for security status request") + return {"success": False, "error": "authentication_failed"} + + try: + # Пробуем получить информацию о безопасности через API Security Scan + result = self._make_api_request("SYNO.Core.SecurityScan.Status", "get", version=1) + + if not result: + # Пробуем альтернативный API + logger.info("Trying alternative API for security status") + # Проверяем статус брандмауэра + firewall_result = self._make_api_request("SYNO.Core.Security.Firewall.Status", "get", version=1) + + # Проверяем статус автоматических обновлений + update_result = self._make_api_request("SYNO.Core.Upgrade.Server.Setting", "get", version=1) + + # Если ни один из API не отвечает + if not firewall_result and not update_result: + # Получаем общую информацию о системе для базовой проверки безопасности + sys_result = self._make_api_request("SYNO.DSM.Info", "getinfo", version=2) + + if not sys_result: + return { + "success": False, + "status": "unknown", + "last_check": None, + "is_secure": False, + "error": "no_security_api" + } + + # Собираем базовые сведения из системной информации + return { + "success": True, + "status": "basic", + "last_check": None, + "is_secure": True, # Предполагаем, что система в целом безопасна + "firewall_enabled": None, + "auto_update": None, + "version_latest": sys_result.get("version_string", "") + } + + # Собираем информацию из доступных результатов + firewall_enabled = firewall_result.get("enabled", False) if firewall_result else None + auto_update = update_result.get("auto_update", False) if update_result else None + + # Определяем, насколько система безопасна + is_secure = True # По умолчанию предполагаем, что система безопасна + if firewall_enabled is not None and not firewall_enabled: + is_secure = False + + return { + "success": True, + "status": "partial", + "last_check": None, + "is_secure": is_secure, + "firewall_enabled": firewall_enabled, + "auto_update": auto_update + } + + # Если основное API отвечает, возвращаем его данные + return { + "success": True, + "status": result.get("status", "unknown"), + "last_check": result.get("last_check", None), + "is_secure": result.get("is_secure", False), + "details": result.get("details", {}) + } + + except Exception as e: + logger.error(f"Error in get_security_status: {str(e)}") + return {"success": False, "error": str(e)} + + def get_processes(self, limit: int = 20) -> List[Dict[str, Any]]: + """Получение списка активных процессов""" + logger.info(f"Getting list of active processes (limit={limit})") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for processes request") + return [] + + try: + # Получаем список процессов через API + result = self._make_api_request("SYNO.Core.System.Process", "list", version=1, + params={"sort_by": "cpu", "order": "DESC", "limit": limit}) + + if not result: + logger.warning("Failed to get process list") + return [] + + return result.get("processes", []) + + except Exception as e: + logger.error(f"Error getting process list: {str(e)}") + return [] + + def get_network_status(self) -> Dict[str, Any]: + """Получение информации о сетевых подключениях""" + logger.info("Getting network status information") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for network status request") + return {} + + try: + # Получаем информацию о сетевых интерфейсах + interface_result = self._make_api_request("SYNO.Core.Network.Interface", "list", version=1) + + # Получаем статистику использования сети + utilization_result = self._make_api_request("SYNO.Core.System.Utilization", "get", version=1) + + interfaces = [] + if interface_result: + interfaces = interface_result.get("interfaces", []) + + network_stats = {} + if utilization_result and "network" in utilization_result: + network_stats = utilization_result.get("network", {}) + + # Объединяем данные + for interface in interfaces: + iface_id = interface.get("id", "") + if iface_id in network_stats: + interface["rx"] = network_stats[iface_id].get("rx", 0) + interface["tx"] = network_stats[iface_id].get("tx", 0) + + return { + "interfaces": interfaces, + "statistics": network_stats + } + + except Exception as e: + logger.error(f"Error getting network status: {str(e)}") + return {} + + def get_system_logs(self, limit: int = 20) -> List[Dict[str, Any]]: + """Получение журналов системы""" + logger.info(f"Getting system logs (limit={limit})") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for system logs request") + return [] + + try: + # Получаем журналы через API + result = self._make_api_request("SYNO.Core.SyslogClient.Log", "list", version=1, + params={"start": 0, "limit": limit, "sort": "time", "dir": "DESC"}) + + if not result: + # Пробуем альтернативный API + logger.info("Trying alternative API for system logs") + alt_result = self._make_api_request("SYNO.Core.Log", "list", version=1, + params={"start": 0, "limit": limit, "sort": "time", "dir": "DESC"}) + + if alt_result: + return alt_result.get("logs", []) + return [] + + return result.get("logs", []) + + except Exception as e: + logger.error(f"Error getting system logs: {str(e)}") + return [] + + def get_power_schedule(self) -> Dict[str, Any]: + """Получение расписания включения/выключения""" + logger.info("Getting power schedule") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for power schedule request") + return {} + + try: + # Получаем расписание через API + result = self._make_api_request("SYNO.Core.Hardware.PowerSchedule", "get", version=1) + + if not result: + return {} + + return result + + except Exception as e: + logger.error(f"Error getting power schedule: {str(e)}") + return {} + + def set_power_schedule(self, schedule_type: str, days: List[int], time: str, enabled: bool = True) -> bool: + """Настройка расписания включения/выключения + + Args: + schedule_type: Тип расписания ('boot' или 'shutdown') + days: Список дней недели (0-6, где 0 - понедельник) + time: Время в формате 'HH:MM' + enabled: Включить или выключить расписание + + Returns: + True если успешно, False в противном случае + """ + logger.info(f"Setting power schedule for {schedule_type} at {time} on days {days}") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for setting power schedule") + return False + + try: + # Получаем текущее расписание + current_schedule = self._make_api_request("SYNO.Core.Hardware.PowerSchedule", "get", version=1) + + if not current_schedule: + logger.error("Failed to get current power schedule") + return False + + # Подготавливаем новое расписание + params = { + "enabled": enabled, + "type": schedule_type, + "day": days, + "time": time + } + + # Устанавливаем новое расписание + result = self._make_api_request("SYNO.Core.Hardware.PowerSchedule", "set", version=1, params=params) + + if not result: + logger.error("Failed to set power schedule") + return False + + logger.info(f"Power schedule for {schedule_type} set successfully") + return True + + except Exception as e: + logger.error(f"Error setting power schedule: {str(e)}") + return False + + def get_temperature_status(self) -> Dict[str, Any]: + """Получение информации о температуре системы и дисков""" + logger.info("Getting temperature status") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for temperature status request") + return {} + + try: + # Получаем информацию о системе для общей температуры + system_info = self._make_api_request("SYNO.DSM.Info", "getinfo", version=2) + + # Получаем информацию о дисках для их температуры + storage_info = self._make_api_request("SYNO.Storage.CGI.Storage", "load_info", version=1) + + system_temp = None + disk_temps = [] + + if system_info: + system_temp = system_info.get("temperature") + + if storage_info: + disks = storage_info.get("disks", []) + for disk in disks: + name = disk.get("name", "unknown") + model = disk.get("model", "unknown") + temp = disk.get("temp", None) + if temp is not None: + disk_temps.append({ + "name": name, + "model": model, + "temperature": temp + }) + + return { + "system_temperature": system_temp, + "disk_temperatures": disk_temps, + "warning": system_info.get("temperature_warn", False) if system_info else False + } + + except Exception as e: + logger.error(f"Error getting temperature status: {str(e)}") + return {} + + def browse_files(self, folder_path: str = "", limit: int = 100) -> Dict[str, Any]: + """Просмотр файлов в указанной директории + + Args: + folder_path: Путь к папке (пустая строка для корневых общих папок) + limit: Максимальное количество элементов для возврата + + Returns: + Словарь с информацией о файлах и папках + """ + logger.info(f"Browsing files in {folder_path or 'root'}") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for file browsing") + return {"success": False, "error": "authentication_failed"} + + try: + if not folder_path: + # Если путь не указан, получаем список общих папок + result = self._make_api_request("SYNO.FileStation.List", "list_share", version=2) + + if not result: + logger.error("Failed to list shared folders") + return {"success": False, "error": "api_error"} + + return { + "success": True, + "items": result.get("shares", []), + "path": "", + "is_root": True + } + else: + # Получаем список файлов в указанной директории + params = { + "folder_path": folder_path, + "limit": limit, + "offset": 0, + "sort_by": "name", + "sort_direction": "ASC" + } + + result = self._make_api_request("SYNO.FileStation.List", "list", version=2, params=params) + + if not result: + logger.error(f"Failed to list files in {folder_path}") + return {"success": False, "error": "api_error"} + + return { + "success": True, + "items": result.get("files", []), + "path": folder_path, + "is_root": False, + "total": result.get("total", 0) + } + + except Exception as e: + logger.error(f"Error browsing files: {str(e)}") + return {"success": False, "error": str(e)} + + def manage_service(self, service_name: str, action: str = "status") -> Dict[str, Any]: + """Управление системным сервисом + + Args: + service_name: Имя сервиса + action: Действие (status/start/stop/restart) + + Returns: + Словарь с результатом операции + """ + logger.info(f"Managing service {service_name}, action: {action}") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for service management") + return {"success": False, "error": "authentication_failed"} + + try: + # Проверяем доступное API для управления сервисами + if action == "status": + result = self._make_api_request("SYNO.Core.Service", "get", version=1, + params={"service": service_name}) + else: + result = self._make_api_request("SYNO.Core.Service", action, version=1, + params={"service": service_name}) + + if not result: + logger.error(f"Failed to {action} service {service_name}") + return {"success": False, "error": "api_error"} + + return { + "success": True, + "service": service_name, + "action": action, + "result": result, + "status": result.get("status") if action == "status" else "completed" + } + + except Exception as e: + logger.error(f"Error managing service {service_name}: {str(e)}") + return {"success": False, "error": str(e)} + + def search_files(self, pattern: str, folder_path: str = "", limit: int = 100) -> Dict[str, Any]: + """Поиск файлов по шаблону + + Args: + pattern: Шаблон для поиска + folder_path: Путь к папке для поиска (пустая строка для всех общих папок) + limit: Максимальное количество результатов + + Returns: + Словарь с найденными файлами + """ + logger.info(f"Searching for {pattern} in {folder_path or 'all shared folders'}") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for file search") + return {"success": False, "error": "authentication_failed"} + + try: + if not folder_path: + # Получаем список всех общих папок для поиска + shares_result = self._make_api_request("SYNO.FileStation.List", "list_share", version=2) + + if not shares_result: + logger.error("Failed to list shared folders for search") + return {"success": False, "error": "api_error"} + + # Формируем список путей для поиска + folder_paths = [share.get("path") for share in shares_result.get("shares", [])] + else: + folder_paths = [folder_path] + + # Запускаем поиск + params = { + "folder_path": folder_paths, + "pattern": pattern, + "limit": limit, + "offset": 0 + } + + result = self._make_api_request("SYNO.FileStation.Search", "start", version=2, params=params) + + if not result: + logger.error(f"Failed to start search for {pattern}") + return {"success": False, "error": "api_error"} + + # Получаем taskid для проверки результатов + taskid = result.get("taskid") + if not taskid: + logger.error("No taskid received for search") + return {"success": False, "error": "no_task_id"} + + # Ожидаем завершения поиска + search_result = {"finished": False, "progress": 0} + for _ in range(10): # Максимум 10 попыток + search_status = self._make_api_request("SYNO.FileStation.Search", "status", version=2, + params={"taskid": taskid}) + + if not search_status: + break + + search_result["progress"] = search_status.get("progress", 0) + + if search_status.get("finished", False): + search_result["finished"] = True + break + + time.sleep(0.5) # Пауза между запросами + + # Получаем результаты поиска + if search_result["finished"]: + list_result = self._make_api_request("SYNO.FileStation.Search", "list", version=2, + params={"taskid": taskid, "limit": limit}) + + if list_result: + files = list_result.get("files", []) + return { + "success": True, + "pattern": pattern, + "results": files, + "total": list_result.get("total", len(files)) + } + + # Если не удалось получить результаты, останавливаем поиск + self._make_api_request("SYNO.FileStation.Search", "stop", version=2, + params={"taskid": taskid}) + + return { + "success": False, + "error": "search_timeout", + "progress": search_result["progress"] + } + + except Exception as e: + logger.error(f"Error searching files: {str(e)}") + return {"success": False, "error": str(e)} + + def get_backup_status(self) -> Dict[str, Any]: + """Получение информации о резервном копировании""" + logger.info("Getting backup status information") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for backup status request") + return {"success": False, "error": "authentication_failed"} + + try: + # Пробуем получить информацию о Hyper Backup + hyper_result = self._make_api_request("SYNO.Backup.Repository", "list", version=1) + + # Пробуем получить информацию о задачах Time Backup + time_result = self._make_api_request("SYNO.BackupService.Task", "list", version=1) + + # Проверяем статус резервного копирования USB + usb_result = self._make_api_request("SYNO.USBCopy", "status", version=1) + + backups = { + "hyper_backup": hyper_result.get("backups", []) if hyper_result else [], + "time_backup": time_result.get("tasks", []) if time_result else [], + "usb_copy": {"enabled": usb_result.get("enable", False) if usb_result else False} + } + + return { + "success": True, + "backups": backups, + "available_apis": { + "hyper_backup": hyper_result is not None, + "time_backup": time_result is not None, + "usb_copy": usb_result is not None + } + } + + except Exception as e: + logger.error(f"Error getting backup status: {str(e)}") + return {"success": False, "error": str(e)} + + def check_for_updates(self) -> Dict[str, Any]: + """Проверка наличия обновлений системы""" + logger.info("Checking for system updates") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for update check") + return {"success": False, "error": "authentication_failed"} + + try: + # Получаем текущую информацию о системе + system_info = self._make_api_request("SYNO.DSM.Info", "getinfo", version=2) + + if not system_info: + logger.error("Failed to get system info for update check") + return {"success": False, "error": "api_error"} + + # Проверяем наличие обновлений + update_result = self._make_api_request("SYNO.Core.Upgrade.Server", "check", version=1) + + # Получаем настройки автоматического обновления + settings_result = self._make_api_request("SYNO.Core.Upgrade.Server.Setting", "get", version=1) + + # Получаем информацию о доступных обновлениях + update_info = self._make_api_request("SYNO.Core.Upgrade.Server.Download", "list", version=1) + + current_version = system_info.get("version_string", "unknown") + auto_update_enabled = settings_result.get("auto_update", False) if settings_result else False + + updates = [] + if update_info and "updates" in update_info: + updates = update_info.get("updates", []) + + update_available = len(updates) > 0 + + return { + "success": True, + "current_version": current_version, + "update_available": update_available, + "auto_update_enabled": auto_update_enabled, + "updates": updates + } + + except Exception as e: + logger.error(f"Error checking for updates: {str(e)}") + return {"success": False, "error": str(e)} + + def get_quota_info(self) -> Dict[str, Any]: + """Получение информации о квотах пользователей""" + logger.info("Getting user quota information") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for quota info request") + return {"success": False, "error": "authentication_failed"} + + try: + # Получаем список пользователей + users_result = self._make_api_request("SYNO.Core.User", "list", version=1) + + if not users_result: + logger.error("Failed to get user list for quota info") + return {"success": False, "error": "api_error"} + + users = users_result.get("users", []) + user_quotas = [] + + # Получаем квоты для каждого пользователя + for user in users: + user_name = user.get("name") + if not user_name: + continue + + quota_result = self._make_api_request("SYNO.Core.Quota", "get", version=1, + params={"user_name": user_name}) + + if quota_result and "quotas" in quota_result: + user_quotas.append({ + "user": user_name, + "quotas": quota_result.get("quotas", []) + }) + + return { + "success": True, + "user_quotas": user_quotas + } + + except Exception as e: + logger.error(f"Error getting quota info: {str(e)}") + return {"success": False, "error": str(e)} diff --git a/.history/src/api/synology_20250830095113.py b/.history/src/api/synology_20250830095113.py new file mode 100644 index 0000000..018a5d3 --- /dev/null +++ b/.history/src/api/synology_20250830095113.py @@ -0,0 +1,1907 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Модуль для взаимодействия с API Synology NAS +""" + +import requests +from requests.adapters import HTTPAdapter +import json +import logging +import time +import urllib3 +from urllib3.util import Retry +from typing import Dict, Any, Optional, List +import socket +import struct +from time import sleep + +from src.config.config import ( + SYNOLOGY_HOST, + SYNOLOGY_PORT, + SYNOLOGY_USERNAME, + SYNOLOGY_PASSWORD, + SYNOLOGY_SECURE, + SYNOLOGY_TIMEOUT, + SYNOLOGY_MAC, + WOL_PORT, + SYNOLOGY_API_VERSION, + SYNOLOGY_POWER_API, + SYNOLOGY_INFO_API +) +from src.api.api_discovery import discover_available_apis, find_compatible_api + +# Отключение предупреждений о небезопасных SSL-соединениях +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + +logger = logging.getLogger(__name__) + +class SynologyAPI: + """Класс для взаимодействия с API Synology NAS""" + + def __init__(self): + """Инициализация класса SynologyAPI""" + logger.info("Creating API with auto-retry and connection pool") + logger.debug(f"Connection details: Host={SYNOLOGY_HOST}, Port={SYNOLOGY_PORT}, Secure={SYNOLOGY_SECURE}") + + self.protocol = "https" if SYNOLOGY_SECURE else "http" + self.base_url = f"{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi" + self.sid = None + self.session = requests.Session() + + # Настройка SSL + if self.protocol == "https": + logger.debug("SSL enabled, disabling certificate verification for internal network") + self.session.verify = False # Отключаем проверку SSL для внутренней сети + + # Добавляем пользовательские заголовки для улучшения совместимости с API + custom_headers = { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36', + 'Accept': 'application/json, text/javascript, */*; q=0.01', + 'Accept-Language': 'ru-RU,ru;q=0.9,en-US;q=0.8,en;q=0.7', + 'X-Requested-With': 'XMLHttpRequest', + 'Connection': 'keep-alive', + 'Referer': f'{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/' + } + self.session.headers.update(custom_headers) + logger.debug("Added browser-like headers for API compatibility") + + # Добавляем повторные попытки для HTTP-запросов + retry_strategy = Retry( + total=5, # Увеличиваем количество попыток + status_forcelist=[429, 500, 502, 503, 504, 404], + allowed_methods=["GET", "POST"], + backoff_factor=1.5, # Увеличиваем задержку между попытками + respect_retry_after_header=True + ) + adapter = HTTPAdapter( + max_retries=retry_strategy, + pool_connections=3, + pool_maxsize=10 + ) + self.session.mount("http://", adapter) + self.session.mount("https://", adapter) + + # Таймауты будут указаны в запросах + self.default_timeout = (SYNOLOGY_TIMEOUT, SYNOLOGY_TIMEOUT*2) + logger.debug(f"Setting default request timeout: {self.default_timeout}") + + # Кэш для хранения результатов запросов + self._cache = {} + self._cache_ttl = {} + self._last_online_check = 0 + self._last_online_status = False + self._online_check_interval = 30 # Интервал для кэширования проверок онлайн-статуса + + # Время последней успешной аутентификации и срок действия сессии + self._last_auth_time = 0 + self._auth_expiry = 3600 # По умолчанию 1 час + + # Информация о доступных API + self._available_apis = {} + self._api_info_ttl = 0 + + # Инициализируем API version resolver для автоматического определения совместимых API + self.api_resolver = None # Будет создан при необходимости + + def login(self) -> bool: + """Авторизация в API Synology NAS""" + # Сбрасываем SID для новой сессии + self.sid = None + + logger.info("Attempting to authenticate with Synology NAS...") + logger.debug(f"Base URL: {self.base_url}") + + # Проверяем доступность NAS перед запросом с помощью прямой TCP-проверки + # Избегаем вызова is_online(), чтобы не создавать рекурсию + online_status = self._check_tcp_connection() + if not online_status: + logger.error("Cannot login: Synology NAS is not reachable") + return False + + # Пробуем различные версии API для аутентификации + # Начинаем с версии 3, которая показала лучшую совместимость в тестах + auth_versions_to_try = [3, 2, 1, 6] # Пробуем сначала более стабильные версии + + for auth_version in auth_versions_to_try: + try: + # Определяем путь к API аутентификации + auth_path = "entry.cgi" # Используем entry.cgi вместо auth.cgi по умолчанию + + # Проверка информации API для определения доступных версий API + api_info_url = f"{self.base_url}/entry.cgi" + api_info_params = { + "api": "SYNO.API.Info", + "version": "1", + "method": "query", + "query": "SYNO.API.Auth" + } + + logger.debug(f"Querying API info for auth version {auth_version}") + try: + api_info_response = self.session.get( + api_info_url, + params=api_info_params, + timeout=self.default_timeout, + verify=False + ) + + if api_info_response.status_code == 200: + api_info_data = api_info_response.json() + if api_info_data.get("success"): + auth_info = api_info_data.get("data", {}).get("SYNO.API.Auth", {}) + max_version = auth_info.get("maxVersion", 6) + min_version = auth_info.get("minVersion", 1) + auth_path = auth_info.get("path", "entry.cgi") + logger.debug(f"Auth API versions: min={min_version}, max={max_version}, path={auth_path}") + + # Проверяем поддержку текущей версии + if auth_version < min_version or auth_version > max_version: + logger.warning(f"Auth API version {auth_version} not supported (min={min_version}, max={max_version}), trying next version") + continue + else: + logger.warning("Failed to query API info, using default auth path") + else: + logger.warning(f"API info request failed with status {api_info_response.status_code}, using default auth path") + except Exception as e: + logger.warning(f"Error querying API info: {str(e)}, using default auth path") + + # Основной запрос авторизации + url = f"{self.base_url}/{auth_path}" + params = { + "api": "SYNO.API.Auth", + "version": str(auth_version), + "method": "login", + "account": SYNOLOGY_USERNAME, + "passwd": SYNOLOGY_PASSWORD, + "session": "SynologyPowerControlBot", + "format": "cookie" + } + + # Для версии 6+ используем немного другой формат + if auth_version >= 6: + params["enable_syno_token"] = "yes" + + logger.debug(f"Sending auth request to {url} with API version {auth_version}") + start_time = time.time() + response = self.session.get( + url, + params=params, + timeout=self.default_timeout, + verify=False + ) + elapsed_time = time.time() - start_time + logger.debug(f"Auth request completed in {elapsed_time:.2f}s with status code {response.status_code}") + + # Проверка статуса HTTP + if response.status_code != 200: + logger.error(f"HTTP error: {response.status_code}") + logger.debug(f"Response content: {response.text[:200]}") + continue # Пробуем следующую версию + + try: + data = response.json() + except json.JSONDecodeError: + logger.error("Failed to decode JSON response") + logger.debug(f"Response content: {response.text[:200]}") + continue # Пробуем следующую версию + + if data.get("success"): + self.sid = data.get("data", {}).get("sid") + self._last_auth_time = time.time() + logger.info(f"Successfully logged in to Synology NAS using API version {auth_version}") + logger.debug(f"Session ID obtained: {self.sid[:5] if self.sid else 'None'}...") + + # Получаем и сохраняем токен SYNO, если он есть + syno_token = data.get("data", {}).get("synotoken") + if syno_token: + self.session.headers.update({'X-SYNO-TOKEN': syno_token}) + logger.debug("Added X-SYNO-TOKEN header for improved API compatibility") + + # Также добавляем SID в cookies для улучшения совместимости + self.session.cookies.update({ + 'id': self.sid, + 'sid': self.sid + }) + logger.debug("Added SID to session cookies for improved compatibility") + + # Проверка валидности полученной сессии с помощью простого запроса + # Будем использовать SYNO.API.Info без проверки сложных методов + + # Даем системе немного времени для инициализации сессии + time.sleep(0.5) + + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to log in with version {auth_version}: Error code {error_code}") + + # Если ошибка связана с версией API, пробуем следующую версию + if error_code in [104, 105]: + logger.warning(f"Auth version {auth_version} not supported, trying next version") + continue + + # Дополнительная диагностика + if error_code == 400: + logger.error("Authentication error: Invalid credentials") + elif error_code == 401: + logger.error("Authentication error: Account disabled") + elif error_code == 402: + logger.error("Authentication error: Permission denied") + elif error_code == 403: + logger.error("Authentication error: 2-factor authentication required") + elif error_code == 404: + logger.error("Authentication error: Failed to authenticate with 2-factor authentication") + + # Если ошибка связана с неверными учетными данными, нет смысла пробовать другие версии API + if error_code in [400, 401, 402, 403, 404]: + return False + + except requests.exceptions.Timeout: + logger.error(f"Connection timeout during auth with version {auth_version}") + continue # Пробуем следующую версию + except requests.exceptions.ConnectionError as e: + logger.error(f"Connection error during auth with version {auth_version}: {str(e)}") + continue # Пробуем следующую версию + except requests.RequestException as e: + logger.error(f"Request error during auth with version {auth_version}: {str(e)}") + continue # Пробуем следующую версию + except Exception as e: + logger.error(f"Unexpected error during login with version {auth_version}: {str(e)}", exc_info=True) + continue # Пробуем следующую версию + + # Если все версии не сработали + logger.error("Failed to authenticate with any API version") + return False + + def _validate_session(self) -> bool: + """Проверяет валидность сессии после авторизации""" + if not self.sid: + return False + + # Попробуем сделать простой запрос для проверки сессии + test_apis = [ + {"api": "SYNO.Core.System", "method": "info", "version": 1}, + {"api": "SYNO.DSM.Info", "method": "getinfo", "version": 1} + ] + + for test_api in test_apis: + try: + url = f"{self.base_url}/entry.cgi" + params = { + "api": test_api["api"], + "version": str(test_api["version"]), + "method": test_api["method"], + "_sid": self.sid + } + + response = self.session.get(url, params=params, timeout=self.default_timeout, verify=False) + + if response.status_code == 200: + data = response.json() + if data.get("success"): + logger.debug(f"Session validation successful using {test_api['api']}") + return True + else: + error_code = data.get("error", {}).get("code", -1) + if error_code != 119: # Не сессия истекла + logger.debug(f"Session validation with {test_api['api']} returned error code {error_code}") + return True # Считаем сессию валидной, если ошибка не связана с истечением сессии + except Exception as e: + logger.warning(f"Error during session validation with {test_api['api']}: {str(e)}") + + logger.warning("Session validation failed with all test APIs") + return False + + def logout(self) -> bool: + """Выход из API Synology NAS""" + if not self.sid: + return True + + try: + url = f"{self.base_url}/auth.cgi" + params = { + "api": "SYNO.API.Auth", + "version": "1", + "method": "logout", + "session": "SynologyPowerControlBot", + "_sid": self.sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + self.sid = None + logger.info("Successfully logged out from Synology NAS") + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to log out from Synology NAS: Error code {error_code}") + return False + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + return False + + def _make_api_request(self, api_name: str, method: str, version: int = 1, + params: Optional[Dict[str, Any]] = None, retry_auth: bool = True, retry_count: int = 0) -> Optional[Dict[str, Any]]: + """Обобщенный метод для выполнения API запросов с обработкой ошибок""" + # Ограничение на количество повторных попыток + if retry_count >= 3: + logger.error(f"Too many retries for {api_name}.{method}, giving up") + return None + + # Проверка наличия авторизации + if not self.sid and not self.login(): + logger.error(f"Not authenticated for API request: {api_name}.{method}") + return None + + # Проверка информации API для определения пути и поддерживаемой версии + api_info_url = f"{self.base_url}/entry.cgi" + api_info_params = { + "api": "SYNO.API.Info", + "version": "1", + "method": "query", + "query": api_name + } + + api_path = "entry.cgi" + try: + logger.debug(f"Querying API info for {api_name}") + api_info_response = self.session.get( + api_info_url, + params=api_info_params, + timeout=self.default_timeout, + verify=False + ) + + if api_info_response.status_code == 200: + api_info_data = api_info_response.json() + if api_info_data.get("success"): + api_info = api_info_data.get("data", {}).get(api_name, {}) + if api_info: + max_version = api_info.get("maxVersion", version) + min_version = api_info.get("minVersion", version) + api_path = api_info.get("path", "entry.cgi") + + # Проверка, поддерживается ли запрошенная версия + if version < min_version: + logger.warning(f"API version {version} for {api_name} is below minimum {min_version}, using {min_version}") + version = min_version + elif version > max_version: + logger.warning(f"API version {version} for {api_name} exceeds maximum {max_version}, using {max_version}") + version = max_version + + logger.debug(f"Using API path: {api_path}, version: {version}") + else: + logger.warning(f"API {api_name} not found in API info, using defaults") + except Exception as e: + logger.warning(f"Error querying API info for {api_name}: {str(e)}, using defaults") + + # Подготовка базовых параметров запроса + base_params = { + "api": api_name, + "version": str(version), + "method": method, + "_sid": self.sid # Используем _sid вместо sid для лучшей совместимости + } + + # Добавление дополнительных параметров, если они заданы + if params: + base_params.update(params) + + url = f"{self.base_url}/{api_path}" + logger.debug(f"API request: {api_name}.{method} v{version} to {url}") + logger.debug(f"Full request params: {base_params}") + + try: + start_time = time.time() + response = self.session.get( + url, + params=base_params, + timeout=self.default_timeout, + verify=False + ) + elapsed_time = time.time() - start_time + logger.debug(f"API request completed in {elapsed_time:.2f}s with status {response.status_code}") + + # Проверка статуса HTTP + if response.status_code != 200: + logger.error(f"HTTP error: {response.status_code} for {api_name}.{method}") + + # Повторная попытка при ошибках соединения + if response.status_code in [500, 502, 503, 504]: + logger.info(f"Server error, retrying request for {api_name}.{method}") + sleep(2) + return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1) + + return None + + try: + data = response.json() + except json.JSONDecodeError: + logger.error(f"Failed to decode JSON response for {api_name}.{method}") + logger.debug(f"Response content: {response.text[:200]}") + + # Повторная попытка при ошибках декодирования + logger.info(f"JSON decode error, retrying request for {api_name}.{method}") + sleep(2) + return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1) + + if data.get("success"): + logger.info(f"API request successful for {api_name}.{method}") + return data.get("data", {}) + else: + error_code = data.get("error", {}).get("code", -1) + error_desc = self._get_error_description(error_code) + logger.error(f"API error for {api_name}.{method}: {error_code} - {error_desc}") + + # Ошибки доступа или прав часто встречаются, но они не критичные + # Например, ошибка 102 означает, что нет прав, но NAS доступен + if error_code in [102, 103, 104, 105]: + logger.warning(f"Permission-related error for {api_name}.{method}: {error_code}") + # Возвращаем пустой словарь вместо None, + # чтобы вызывающий код мог понять, что запрос выполнен + return {} + + # Если ошибка связана с авторизацией и нам разрешено повторить попытку + if error_code in [106, 107, 119] and retry_auth: + logger.info(f"Session error (code {error_code}), creating fresh session...") + self.sid = None # Сбрасываем SID + + # Для ошибки 119 (Session timeout) дадим системе немного времени + if error_code == 119: + logger.info("Session timeout detected, waiting before retry...") + sleep(3) + + if self.login(): + logger.info("Re-authenticated with fresh session, retrying API request...") + # Рекурсивный вызов, но со счетчиком повторов + return self._make_api_request(api_name, method, version, params, False, retry_count + 1) + + # Для некоторых ошибок можно автоматически повторить запрос + if error_code in [408, 429, 500, 502, 503, 504]: + logger.info(f"Temporary error {error_code}, retrying request for {api_name}.{method}") + sleep(2) + return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1) + + return None + + except requests.exceptions.Timeout: + logger.error(f"Request timeout for {api_name}.{method}") + + # Повторная попытка при таймауте + if retry_count < 2: + logger.info(f"Timeout, retrying request for {api_name}.{method}") + sleep(2) + return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1) + + return None + except requests.exceptions.ConnectionError as e: + logger.error(f"Connection error for {api_name}.{method}: {str(e)}") + + # Повторная попытка при ошибке соединения + if retry_count < 2: + logger.info(f"Connection error, retrying request for {api_name}.{method}") + sleep(2) + return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1) + + return None + except Exception as e: + logger.error(f"Unexpected error during {api_name}.{method}: {str(e)}") + return None + + def get_system_status(self) -> Dict[str, Any]: + """Получение статуса системы""" + # Проверяем доступность системы + if not self.is_online(): + logger.info("Device is offline, skipping API request") + return {"status": "offline"} + + # Проверяем, есть ли кэшированный результат + cache_key = "system_status" + current_time = time.time() + if cache_key in self._cache and current_time - self._cache_ttl.get(cache_key, 0) < 60: + logger.debug("Using cached system status") + return self._cache[cache_key] + + # Используем рекомендованный API для получения информации о системе + logger.info(f"Getting system info using {SYNOLOGY_INFO_API} v{SYNOLOGY_API_VERSION}") + + # Для SYNO.DSM.Info метод getinfo, для других обычно используется get или info + if SYNOLOGY_INFO_API == "SYNO.DSM.Info": + method = "getinfo" + else: + method = "get" + + result = self._make_api_request(SYNOLOGY_INFO_API, method, version=SYNOLOGY_API_VERSION) + + if result: + logger.info(f"Successfully retrieved system info using {SYNOLOGY_INFO_API}") + + # Формируем расширенный ответ с дополнительной информацией + system_info = { + "status": "online", + "hostname": result.get("hostname", "unknown"), + "model": result.get("model", "unknown"), + "version": result.get("version", "unknown"), + "uptime": result.get("uptime", 0), + "time": current_time, + "is_online": True, + "api_used": SYNOLOGY_INFO_API + } + + # Сохраняем в кэше + self._cache[cache_key] = system_info + self._cache_ttl[cache_key] = current_time + + return system_info + + # Если основной API не сработал, пробуем резервные варианты + logger.warning(f"Failed to retrieve system info with {SYNOLOGY_INFO_API}, trying fallback APIs") + + # Пробуем резервные API + apis_to_try = [ + {"name": "SYNO.DSM.Info", "method": "getinfo", "version": 2}, + {"name": "SYNO.Core.System", "method": "info", "version": 1}, + {"name": "SYNO.Core.System.Status", "method": "get", "version": 1}, + {"name": "SYNO.Core.System.Info", "method": "get", "version": 1}, + ] + + for api in apis_to_try: + if api["name"] == SYNOLOGY_INFO_API: + continue # Пропускаем уже проверенный API + + logger.info(f"Trying to get system info using {api['name']}.{api['method']} v{api['version']}") + result = self._make_api_request(api["name"], api["method"], version=api["version"]) + + if result: + logger.info(f"Successfully retrieved system info using {api['name']}") + + # Формируем расширенный ответ с дополнительной информацией + system_info = { + "status": "online", + "hostname": result.get("hostname", "unknown"), + "model": result.get("model", "unknown"), + "version": result.get("version", "unknown"), + "uptime": result.get("uptime", 0), + "time": current_time, + "is_online": True, + "api_used": api["name"] + } + + # Сохраняем в кэше + self._cache[cache_key] = system_info + self._cache_ttl[cache_key] = current_time + + return system_info + + # Если все запросы не удались, но система онлайн, возвращаем базовую информацию + logger.warning("Failed to retrieve system info with all API methods") + return { + "status": "error", + "error": "Failed to fetch system information", + "is_online": True, + "time": current_time + } + + def shutdown_system(self) -> bool: + """Выключение системы""" + # Проверяем, включено ли устройство перед попыткой его выключить + if not self.is_online(force_check=True): + logger.info("Synology NAS is already offline, no need to shut down") + return True + + logger.info("Attempting to shutdown Synology NAS...") + + # Попробуем сначала использовать предпочтительный API для управления питанием + logger.info(f"Trying shutdown with {SYNOLOGY_POWER_API} v{SYNOLOGY_API_VERSION}") + + # Для SYNO.Core.Hardware.PowerRecovery используется метод setPowerOnState + # Для других API обычно используется метод shutdown или reboot + if SYNOLOGY_POWER_API == "SYNO.Core.Hardware.PowerRecovery": + # Для этого API нужны специальные параметры + params = {"state": "powerbtn"} # powerbtn имитирует нажатие кнопки питания + result = self._make_api_request(SYNOLOGY_POWER_API, "setPowerOnState", version=SYNOLOGY_API_VERSION, params=params) + else: + # Пробуем стандартный метод + result = self._make_api_request(SYNOLOGY_POWER_API, "shutdown", version=SYNOLOGY_API_VERSION) + + if result is not None: + logger.info(f"Successfully initiated system shutdown using {SYNOLOGY_POWER_API}") + + # Даем системе время начать процесс выключения + logger.info("Waiting for shutdown to initialize...") + sleep(5) + + # Проверяем статус + if not self.is_online(force_check=True): + logger.info("System is now offline. Shutdown confirmed successful.") + return True + else: + logger.info("System still appears to be online, but shutdown may be in progress.") + return True + else: + logger.warning(f"Failed to shutdown using {SYNOLOGY_POWER_API}, trying alternative methods") + + # Если не сработал основной метод, пробуем резервные варианты + # Проверка всех доступных методов API для выключения + apis_to_try = [ + {"name": "SYNO.Core.System", "method": "shutdown", "version": 3}, + {"name": "SYNO.Core.System.Power", "method": "shutdown", "version": 1}, + {"name": "SYNO.DSM.Power", "method": "shutdown", "version": 1}, + {"name": "SYNO.System.Power", "method": "shutdown", "version": 1}, + {"name": "SYNO.DSM.System", "method": "shutdown", "version": 1} + ] + + # Проверяем доступные API + try: + api_info_url = f"{self.base_url}/entry.cgi" + api_info_params = { + "api": "SYNO.API.Info", + "version": "1", + "method": "query", + "query": "SYNO.Core.System.Power,SYNO.DSM.Power,SYNO.Core.System,SYNO.System.Power,SYNO.DSM.System" + } + + logger.debug("Checking available shutdown APIs") + api_info_response = self.session.get( + api_info_url, + params=api_info_params, + timeout=self.default_timeout, + verify=False + ) + + if api_info_response.status_code == 200: + api_info_data = api_info_response.json() + if api_info_data.get("success"): + available_apis = api_info_data.get("data", {}) + logger.debug(f"Available APIs: {list(available_apis.keys())}") + + # Фильтруем только доступные API + filtered_apis = [] + for api in apis_to_try: + if api["name"] in available_apis: + api_info = available_apis[api["name"]] + # Проверка версии API + if api_info.get("minVersion", 1) <= api["version"] <= api_info.get("maxVersion", 1): + filtered_apis.append(api) + logger.debug(f"Adding {api['name']} to available shutdown APIs") + else: + logger.debug(f"API {api['name']} version {api['version']} not supported. Supported: {api_info.get('minVersion')}-{api_info.get('maxVersion')}") + + if filtered_apis: + apis_to_try = filtered_apis + else: + logger.warning("No compatible APIs found, trying all methods as fallback") + else: + logger.warning("Failed to query API info, using default methods") + else: + logger.warning(f"API info request failed with status {api_info_response.status_code}, using default methods") + except Exception as e: + logger.warning(f"Error querying API info: {str(e)}, using default methods") + + # Пробуем все доступные методы по порядку + for api in apis_to_try: + logger.info(f"Trying shutdown with {api['name']}.{api['method']} v{api['version']}") + result = self._make_api_request(api["name"], api["method"], version=api["version"]) + + if result is not None: + logger.info(f"Successfully initiated system shutdown using {api['name']}") + + # Даем системе время начать процесс выключения + logger.info("Waiting for shutdown to initialize...") + sleep(5) + + # Проверяем статус + if not self.is_online(force_check=True): + logger.info("System is now offline. Shutdown confirmed successful.") + return True + else: + logger.info("System still appears to be online, but shutdown may be in progress.") + return True + else: + logger.warning(f"Failed to shutdown using {api['name']}.{api['method']} v{api['version']}") + + # Если ни один метод не сработал, но система стала недоступна + if not self.is_online(force_check=True): + logger.info("System appears to be shutting down despite API errors") + return True + + logger.error("Failed to shutdown system after trying multiple APIs") + return False + + def reboot_system(self) -> bool: + """Перезагрузка системы""" + # Проверяем, включена ли система + if not self.is_online(force_check=True): + logger.error("Cannot reboot: System is offline") + return False + + logger.info("Attempting to reboot Synology NAS...") + + # Попробуем сначала использовать предпочтительный API для управления питанием + logger.info(f"Trying reboot with {SYNOLOGY_POWER_API} v{SYNOLOGY_API_VERSION}") + + # Пробуем использовать наиболее совместимые API для перезагрузки + # SYNO.Core.System обычно доступен на большинстве систем + logger.info("Trying reboot with SYNO.Core.System API") + result = self._make_api_request("SYNO.Core.System", "reboot", version=1) + + if result is None: + # Если не сработало, пробуем альтернативные методы + logger.info("Trying alternative reboot method with SYNO.DSM.System API") + result = self._make_api_request("SYNO.DSM.System", "reboot", version=1) + + if result is None and SYNOLOGY_POWER_API != "SYNO.Core.System": + # Пробуем настроенный в конфигурации API, если он отличается + logger.info(f"Trying reboot with configured API: {SYNOLOGY_POWER_API}") + methods_to_try = ["restart", "reboot"] + for method in methods_to_try: + result = self._make_api_request(SYNOLOGY_POWER_API, method, version=SYNOLOGY_API_VERSION) + if result is not None: + logger.info(f"Successfully used method {method} for {SYNOLOGY_POWER_API}") + break + + if result is not None: + logger.info(f"Successfully initiated system reboot using {SYNOLOGY_POWER_API}") + + # Даем системе время начать процесс перезагрузки + logger.info("Waiting for reboot to initialize...") + sleep(5) + + # Ждем, пока система станет недоступна (признак перезагрузки) + reboot_started = False + for i in range(12): # Проверяем в течение 60 секунд (12 * 5сек) + if not self.is_online(force_check=True): + logger.info(f"System went offline after {i*5} seconds, reboot in progress") + reboot_started = True + break + logger.debug(f"System still online, waiting... ({i+1}/12)") + sleep(5) + + if reboot_started: + # Ждем, пока система снова станет доступна + logger.info("Waiting for system to come back online...") + return self.wait_for_boot(max_attempts=30, delay=10) + else: + logger.warning("System did not go offline after reboot command, but command was accepted") + # Даже если система не ушла оффлайн, команда могла быть принята + return True + else: + logger.warning(f"Failed to reboot using {SYNOLOGY_POWER_API}, trying alternative methods") + + # Если не сработал основной метод, пробуем резервные варианты + # Проверка всех доступных методов API для перезагрузки + apis_to_try = [ + {"name": "SYNO.Core.System.Power", "method": "restart", "version": 1}, + {"name": "SYNO.DSM.Power", "method": "reboot", "version": 1}, + {"name": "SYNO.Core.System", "method": "reboot", "version": 3}, + {"name": "SYNO.System.Power", "method": "reboot", "version": 1}, + {"name": "SYNO.DSM.System", "method": "reboot", "version": 1}, + {"name": "SYNO.Core.Hardware.NeedReboot", "method": "reboot", "version": 1} + ] + + # Получаем список доступных API + try: + api_info_url = f"{self.base_url}/entry.cgi" + api_info_params = { + "api": "SYNO.API.Info", + "version": "1", + "method": "query", + "query": "SYNO.Core.System.Power,SYNO.DSM.Power,SYNO.Core.System,SYNO.System.Power,SYNO.DSM.System,SYNO.Core.Hardware.NeedReboot" + } + + logger.debug("Checking available reboot APIs") + api_info_response = self.session.get( + api_info_url, + params=api_info_params, + timeout=self.default_timeout, + verify=False + ) + + if api_info_response.status_code == 200: + api_info_data = api_info_response.json() + if api_info_data.get("success"): + available_apis = api_info_data.get("data", {}) + logger.debug(f"Available APIs: {list(available_apis.keys())}") + + # Фильтруем только доступные API + filtered_apis = [] + for api in apis_to_try: + if api["name"] in available_apis: + api_info = available_apis[api["name"]] + # Проверка версии API + if api_info.get("minVersion", 1) <= api["version"] <= api_info.get("maxVersion", 1): + filtered_apis.append(api) + logger.debug(f"Adding {api['name']} to available reboot APIs") + else: + logger.debug(f"API {api['name']} version {api['version']} not supported. Supported: {api_info.get('minVersion')}-{api_info.get('maxVersion')}") + + if filtered_apis: + apis_to_try = filtered_apis + else: + logger.warning("No compatible APIs found, trying all methods as fallback") + else: + logger.warning("Failed to query API info, using default methods") + else: + logger.warning(f"API info request failed with status {api_info_response.status_code}, using default methods") + except Exception as e: + logger.warning(f"Error querying API info: {str(e)}, using default methods") + + # Пробуем все доступные методы по порядку + for api in apis_to_try: + logger.info(f"Trying reboot with {api['name']}.{api['method']} v{api['version']}") + result = self._make_api_request(api["name"], api["method"], version=api["version"]) + + if result is not None: + logger.info(f"Successfully initiated system reboot using {api['name']}") + + # Даем системе время начать процесс перезагрузки + logger.info("Waiting for reboot to initialize...") + sleep(5) + + # Ждем, пока система станет недоступна (признак перезагрузки) + reboot_started = False + for i in range(12): # Проверяем в течение 60 секунд (12 * 5сек) + if not self.is_online(force_check=True): + logger.info(f"System went offline after {i*5} seconds, reboot in progress") + reboot_started = True + break + logger.debug(f"System still online, waiting... ({i+1}/12)") + sleep(5) + + if reboot_started: + # Ждем, пока система снова станет доступна + logger.info("Waiting for system to come back online...") + return self.wait_for_boot(max_attempts=30, delay=10) + else: + logger.warning("System did not go offline after reboot command, but command was accepted") + # Даже если система не ушла оффлайн, команда могла быть принята + return True + else: + logger.warning(f"Failed to reboot using {api['name']}.{api['method']} v{api['version']}") + + logger.error("Failed to reboot system after trying multiple APIs") + return False + + def _get_error_description(self, error_code: int) -> str: + """Получение описания ошибки по коду""" + error_descriptions = { + 100: "Unknown error", + 101: "Invalid parameter", + 102: "API does not exist", + 103: "Method does not exist", + 104: "Version does not support", + 105: "Permission denied", + 106: "Session timeout", + 107: "Session interrupted by duplicate login", + 400: "Invalid credentials", + 401: "Account disabled", + 402: "Permission denied", + 403: "2FA required", + 404: "Failed to authenticate with 2FA" + } + return error_descriptions.get(error_code, "Unknown error code") + + def _check_tcp_connection(self) -> bool: + """Проверка базового TCP-соединения с Synology NAS""" + try: + socket_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + socket_obj.settimeout(SYNOLOGY_TIMEOUT) + result = socket_obj.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT)) + socket_obj.close() + + return result == 0 + except socket.error as e: + logger.error(f"Socket error during connection check: {str(e)}") + return False + except Exception as e: + logger.error(f"Unexpected error during connection check: {str(e)}") + return False + + def is_online(self, force_check=False) -> bool: + """Проверка онлайн-статуса Synology NAS""" + # Используем кэшированное значение, если доступно и не устарело + current_time = time.time() + if not force_check and (current_time - self._last_online_check) < self._online_check_interval: + logger.debug(f"Using cached online status: {self._last_online_status}") + return self._last_online_status + + logger.info("Checking if NAS is online...") + + # Проверяем TCP-соединение + online_status = self._check_tcp_connection() + logger.info(f"Detected Synology NAS online status: {online_status}") + + # Если TCP-соединение успешно и у нас есть действующий SID, + # попробуем более детальную проверку через API + if online_status and self.sid: + logger.info("Trying to fetch more detailed online status through API...") + + # Пробуем разные API для проверки онлайн-статуса + api_checks = [ + {"api": "SYNO.DSM.Info", "version": "2", "method": "getinfo"}, + {"api": "SYNO.Core.System", "version": "1", "method": "info"}, + {"api": "SYNO.Core.System.Status", "version": "1", "method": "get"} + ] + + api_success = False + for api_check in api_checks: + try: + url = f"{self.base_url}/entry.cgi" + params = { + "api": api_check["api"], + "version": api_check["version"], + "method": api_check["method"], + "sid": self.sid + } + + logger.debug(f"Trying online status check with {api_check['api']}") + response = self.session.get( + url, + params=params, + timeout=self.default_timeout, + verify=False + ) + + if response.status_code == 200: + data = response.json() + if data.get("success"): + logger.info(f"API request successful for {api_check['api']}") + logger.info("Synology NAS is online with API access") + api_success = True + break + else: + error_code = data.get("error", {}).get("code", -1) + logger.warning(f"API response indicates an error: {error_code}, but NAS is reachable") + else: + logger.warning(f"API returned status code {response.status_code}") + except Exception as e: + logger.warning(f"API check failed for {api_check['api']}, but TCP connection succeeded: {str(e)}") + + if not api_success: + logger.warning("All API checks failed, but TCP connection is successful") + + # Обновляем кэшированное значение + self._last_online_check = current_time + self._last_online_status = online_status + + return online_status + + def wake_on_lan(self) -> bool: + """Отправка Wake-on-LAN пакета для включения Synology NAS""" + if not SYNOLOGY_MAC: + logger.error("MAC address not configured") + return False + + try: + # Преобразование MAC-адреса в байты + mac_address = SYNOLOGY_MAC.replace(':', '').replace('-', '') + if len(mac_address) != 12: + logger.error(f"Invalid MAC address format: {SYNOLOGY_MAC}") + return False + + try: + mac_bytes = bytes.fromhex(mac_address) + except ValueError as e: + logger.error(f"Failed to parse MAC address: {str(e)}") + logger.error(f"MAC address must contain only hex characters: {SYNOLOGY_MAC}") + return False + + # Создание Magic Packet + magic_packet = b'\xff' * 6 + mac_bytes * 16 + + # Отправка пакета на конкретный адрес + try: + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) + sock.sendto(magic_packet, (SYNOLOGY_HOST, WOL_PORT)) + sock.close() + logger.info(f"Wake-on-LAN packet sent to {SYNOLOGY_MAC} at {SYNOLOGY_HOST}:{WOL_PORT}") + except Exception as e: + logger.error(f"Error sending directed WoL packet: {str(e)}") + return False + + # Для надежности отправляем также широковещательный пакет + try: + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) + + # Используем стандартный широковещательный адрес + broadcast_addr = "255.255.255.255" + sock.sendto(magic_packet, (broadcast_addr, WOL_PORT)) + sock.close() + logger.info(f"Wake-on-LAN broadcast packet sent to {broadcast_addr}:{WOL_PORT}") + except Exception as e: + logger.warning(f"Error sending broadcast WoL packet (non-critical): {str(e)}") + # Не считаем ошибкой, т.к. основной пакет уже отправлен + + return True + + except Exception as e: + logger.error(f"Unexpected error in wake_on_lan: {str(e)}") + return False + + def wait_for_boot(self, max_attempts: int = 30, delay: int = 10) -> bool: + """Ожидание загрузки Synology NAS""" + logger.info(f"Waiting for Synology NAS to boot (timeout: {max_attempts * delay}s)...") + + for attempt in range(max_attempts): + # Принудительно проверяем статус без использования кэша + if self.is_online(force_check=True): + logger.info(f"Synology NAS is online after {attempt + 1} attempts ({(attempt + 1) * delay}s)") + + # Проверяем, что не только сеть доступна, но и API загрузился + api_ready = False + logger.info("Waiting for API services to initialize...") + + for api_check in range(5): # Даем еще до 50 секунд для загрузки API + if self.sid or self.login(): + api_ready = True + logger.info(f"API services are ready after {api_check + 1} attempts") + break + logger.debug(f"API not ready yet, waiting... ({api_check + 1}/5)") + sleep(10) + + if not api_ready: + logger.warning("System is online but API services may not be fully initialized") + + # Дадим дополнительное время для полной загрузки всех сервисов + sleep(delay) + return True + + sleep(delay) + logger.info(f"Waiting for boot... Attempt {attempt + 1}/{max_attempts}") + + logger.error(f"Synology NAS didn't come online after {max_attempts * delay} seconds") + return False + + def power_on(self) -> bool: + """Включение Synology NAS""" + # Принудительная проверка статуса + if self.is_online(force_check=True): + logger.info("Synology NAS is already online") + return True + + logger.info("Powering on Synology NAS via Wake-on-LAN...") + + # Проверяем, настроен ли MAC-адрес + if not SYNOLOGY_MAC: + logger.error("Cannot power on: MAC address not configured in settings") + return False + + # Пробуем отправить несколько WoL пакетов для надежности + success = False + for attempt in range(3): + logger.debug(f"Sending WoL packet, attempt {attempt + 1}/3") + if self.wake_on_lan(): + success = True + break + sleep(1) + + if not success: + logger.error("Failed to send Wake-on-LAN packets") + return False + + # Ожидание загрузки + logger.info("WoL packets sent successfully, waiting for system to boot...") + boot_result = self.wait_for_boot(max_attempts=30, delay=10) + + if boot_result: + # Проверяем доступность API после загрузки + system_status = self.get_system_status() + if system_status.get("status") == "online": + logger.info("System booted successfully with API access") + return True + else: + logger.warning("System appears to be online but API may not be fully ready") + return True + else: + logger.error("System did not come online after WoL") + return False + + def power_off(self) -> bool: + """Выключение Synology NAS""" + if not self.is_online(force_check=True): + logger.info("Synology NAS is already offline") + return True + + logger.info("Powering off Synology NAS...") + result = self.shutdown_system() + + if result: + # Дополнительная проверка, что система действительно выключилась + logger.info("Verifying system is offline...") + for attempt in range(12): # Проверяем в течение 2 минут (12 * 10 сек) + if not self.is_online(force_check=True): + logger.info(f"System confirmed offline after {attempt * 10} seconds") + return True + logger.debug(f"System still shutting down, checking again... ({attempt + 1}/12)") + sleep(10) + + logger.warning("System still appears to be online after shutdown command") + return False + else: + logger.error("Failed to initiate shutdown") + return False + + # Заглушки для расширенных методов + def get_shared_folders(self) -> List[Dict[str, Any]]: + """Получение списка общих папок""" + logger.info("Getting list of shared folders") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for shared folders request") + return [] + + try: + # Запрашиваем список общих папок через FileStation API + result = self._make_api_request("SYNO.FileStation.List", "list_share", version=2) + + if not result: + # Пробуем альтернативный API + logger.info("Trying alternative API for shared folders") + alt_result = self._make_api_request("SYNO.FileStation.List", "list_share", version=1) + if alt_result: + return alt_result.get("shares", []) + return [] + + return result.get("shares", []) + + except Exception as e: + logger.error(f"Error getting shared folders: {str(e)}") + return [] + + def get_system_load(self) -> Dict[str, Any]: + """Получение информации о загрузке системы""" + logger.info("Getting system load information") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for system load request") + return {} + + try: + # Запрашиваем информацию о загрузке системы + result = self._make_api_request("SYNO.Core.System.Utilization", "get", version=1) + + if not result: + # Пробуем альтернативный API + logger.info("Trying alternative API for system load") + alt_result = self._make_api_request("SYNO.Core.System", "info", version=3) + + if not alt_result: + return {} + + # Формируем из частичных данных + return { + "cpu_load": alt_result.get("cpu_usage", 0), + "memory": { + "total": alt_result.get("memory_size", 0), + "used": alt_result.get("memory_usage", 0), + "usage_percent": alt_result.get("memory_usage_percent", 0) + } + } + + # Формируем структурированный результат + return { + "cpu_load": result.get("cpu", {}).get("user", 0) + result.get("cpu", {}).get("system", 0), + "memory": result.get("memory", {}), + "network": result.get("network", {}) + } + + except Exception as e: + logger.error(f"Error getting system load: {str(e)}") + return {} + + def get_storage_status(self) -> Dict[str, Any]: + """Получение подробной информации о хранилище""" + logger.info("Getting storage status information") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for storage status request") + return {"error": "authentication_failed"} + + try: + # Пробуем получить информацию о хранилище через SYNO.Storage.CGI.Storage API + result = self._make_api_request("SYNO.Storage.CGI.Storage", "load_info", version=1) + + if not result: + # Пробуем альтернативный API + logger.info("Trying alternative API for storage info") + alt_result = self._make_api_request("SYNO.Core.Storage", "info", version=1) + + if not alt_result: + # Пробуем еще один альтернативный API + logger.info("Trying SYNO.Core.System API for storage info") + sys_result = self._make_api_request("SYNO.Core.System", "info", version=3) + + if not sys_result: + return { + "volumes": [], + "disks": [], + "total_size": 0, + "total_used": 0, + "error": "no_data" + } + + # Извлекаем базовую информацию о хранилище из системной информации + return { + "volumes": [], + "disks": [], + "total_size": sys_result.get("hdd_total_size", 0) * 1024**2, # МБ в байты + "total_used": sys_result.get("hdd_total_size", 0) * sys_result.get("hdd_usage", 0) / 100 * 1024**2, + } + + # Обрабатываем данные из альтернативного API + volumes = alt_result.get("volumes", []) + disks = alt_result.get("disks", []) + + else: + # Обрабатываем данные из основного API + volumes = result.get("volumes", []) + disks = result.get("disks", []) + + # Рассчитываем общие размеры + total_size = 0 + total_used = 0 + + for volume in volumes: + volume_size = volume.get("size", {}).get("total", 0) + volume_used = volume.get("size", {}).get("used", 0) + + total_size += volume_size + total_used += volume_used + + return { + "volumes": volumes, + "disks": disks, + "total_size": total_size, + "total_used": total_used + } + + except Exception as e: + logger.error(f"Error in get_storage_status: {str(e)}") + return {"error": str(e)} + + def get_security_status(self) -> Dict[str, Any]: + """Получение информации о состоянии безопасности""" + logger.info("Getting security status information") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for security status request") + return {"success": False, "error": "authentication_failed"} + + try: + # Пробуем получить информацию о безопасности через API Security Scan + result = self._make_api_request("SYNO.Core.SecurityScan.Status", "get", version=1) + + if not result: + # Пробуем альтернативный API + logger.info("Trying alternative API for security status") + # Проверяем статус брандмауэра + firewall_result = self._make_api_request("SYNO.Core.Security.Firewall.Status", "get", version=1) + + # Проверяем статус автоматических обновлений + update_result = self._make_api_request("SYNO.Core.Upgrade.Server.Setting", "get", version=1) + + # Если ни один из API не отвечает + if not firewall_result and not update_result: + # Получаем общую информацию о системе для базовой проверки безопасности + sys_result = self._make_api_request("SYNO.DSM.Info", "getinfo", version=2) + + if not sys_result: + return { + "success": False, + "status": "unknown", + "last_check": None, + "is_secure": False, + "error": "no_security_api" + } + + # Собираем базовые сведения из системной информации + return { + "success": True, + "status": "basic", + "last_check": None, + "is_secure": True, # Предполагаем, что система в целом безопасна + "firewall_enabled": None, + "auto_update": None, + "version_latest": sys_result.get("version_string", "") + } + + # Собираем информацию из доступных результатов + firewall_enabled = firewall_result.get("enabled", False) if firewall_result else None + auto_update = update_result.get("auto_update", False) if update_result else None + + # Определяем, насколько система безопасна + is_secure = True # По умолчанию предполагаем, что система безопасна + if firewall_enabled is not None and not firewall_enabled: + is_secure = False + + return { + "success": True, + "status": "partial", + "last_check": None, + "is_secure": is_secure, + "firewall_enabled": firewall_enabled, + "auto_update": auto_update + } + + # Если основное API отвечает, возвращаем его данные + return { + "success": True, + "status": result.get("status", "unknown"), + "last_check": result.get("last_check", None), + "is_secure": result.get("is_secure", False), + "details": result.get("details", {}) + } + + except Exception as e: + logger.error(f"Error in get_security_status: {str(e)}") + return {"success": False, "error": str(e)} + + def get_processes(self, limit: int = 20) -> List[Dict[str, Any]]: + """Получение списка активных процессов""" + logger.info(f"Getting list of active processes (limit={limit})") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for processes request") + return [] + + try: + # Получаем список процессов через API + result = self._make_api_request("SYNO.Core.System.Process", "list", version=1, + params={"sort_by": "cpu", "order": "DESC", "limit": limit}) + + if not result: + logger.warning("Failed to get process list") + return [] + + return result.get("processes", []) + + except Exception as e: + logger.error(f"Error getting process list: {str(e)}") + return [] + + def get_network_status(self) -> Dict[str, Any]: + """Получение информации о сетевых подключениях""" + logger.info("Getting network status information") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for network status request") + return {} + + try: + # Получаем информацию о сетевых интерфейсах + interface_result = self._make_api_request("SYNO.Core.Network.Interface", "list", version=1) + + # Получаем статистику использования сети + utilization_result = self._make_api_request("SYNO.Core.System.Utilization", "get", version=1) + + interfaces = [] + if interface_result: + interfaces = interface_result.get("interfaces", []) + + network_stats = {} + if utilization_result and "network" in utilization_result: + network_stats = utilization_result.get("network", {}) + + # Объединяем данные + for interface in interfaces: + iface_id = interface.get("id", "") + if iface_id in network_stats: + interface["rx"] = network_stats[iface_id].get("rx", 0) + interface["tx"] = network_stats[iface_id].get("tx", 0) + + return { + "interfaces": interfaces, + "statistics": network_stats + } + + except Exception as e: + logger.error(f"Error getting network status: {str(e)}") + return {} + + def get_system_logs(self, limit: int = 20) -> List[Dict[str, Any]]: + """Получение журналов системы""" + logger.info(f"Getting system logs (limit={limit})") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for system logs request") + return [] + + try: + # Получаем журналы через API + result = self._make_api_request("SYNO.Core.SyslogClient.Log", "list", version=1, + params={"start": 0, "limit": limit, "sort": "time", "dir": "DESC"}) + + if not result: + # Пробуем альтернативный API + logger.info("Trying alternative API for system logs") + alt_result = self._make_api_request("SYNO.Core.Log", "list", version=1, + params={"start": 0, "limit": limit, "sort": "time", "dir": "DESC"}) + + if alt_result: + return alt_result.get("logs", []) + return [] + + return result.get("logs", []) + + except Exception as e: + logger.error(f"Error getting system logs: {str(e)}") + return [] + + def get_power_schedule(self) -> Dict[str, Any]: + """Получение расписания включения/выключения""" + logger.info("Getting power schedule") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for power schedule request") + return {} + + try: + # Получаем расписание через API + result = self._make_api_request("SYNO.Core.Hardware.PowerSchedule", "get", version=1) + + if not result: + return {} + + return result + + except Exception as e: + logger.error(f"Error getting power schedule: {str(e)}") + return {} + + def set_power_schedule(self, schedule_type: str, days: List[int], time: str, enabled: bool = True) -> bool: + """Настройка расписания включения/выключения + + Args: + schedule_type: Тип расписания ('boot' или 'shutdown') + days: Список дней недели (0-6, где 0 - понедельник) + time: Время в формате 'HH:MM' + enabled: Включить или выключить расписание + + Returns: + True если успешно, False в противном случае + """ + logger.info(f"Setting power schedule for {schedule_type} at {time} on days {days}") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for setting power schedule") + return False + + try: + # Получаем текущее расписание + current_schedule = self._make_api_request("SYNO.Core.Hardware.PowerSchedule", "get", version=1) + + if not current_schedule: + logger.error("Failed to get current power schedule") + return False + + # Подготавливаем новое расписание + params = { + "enabled": enabled, + "type": schedule_type, + "day": days, + "time": time + } + + # Устанавливаем новое расписание + result = self._make_api_request("SYNO.Core.Hardware.PowerSchedule", "set", version=1, params=params) + + if not result: + logger.error("Failed to set power schedule") + return False + + logger.info(f"Power schedule for {schedule_type} set successfully") + return True + + except Exception as e: + logger.error(f"Error setting power schedule: {str(e)}") + return False + + def get_temperature_status(self) -> Dict[str, Any]: + """Получение информации о температуре системы и дисков""" + logger.info("Getting temperature status") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for temperature status request") + return {} + + try: + # Получаем информацию о системе для общей температуры + system_info = self._make_api_request("SYNO.DSM.Info", "getinfo", version=2) + + # Получаем информацию о дисках для их температуры + storage_info = self._make_api_request("SYNO.Storage.CGI.Storage", "load_info", version=1) + + system_temp = None + disk_temps = [] + + if system_info: + system_temp = system_info.get("temperature") + + if storage_info: + disks = storage_info.get("disks", []) + for disk in disks: + name = disk.get("name", "unknown") + model = disk.get("model", "unknown") + temp = disk.get("temp", None) + if temp is not None: + disk_temps.append({ + "name": name, + "model": model, + "temperature": temp + }) + + return { + "system_temperature": system_temp, + "disk_temperatures": disk_temps, + "warning": system_info.get("temperature_warn", False) if system_info else False + } + + except Exception as e: + logger.error(f"Error getting temperature status: {str(e)}") + return {} + + def browse_files(self, folder_path: str = "", limit: int = 100) -> Dict[str, Any]: + """Просмотр файлов в указанной директории + + Args: + folder_path: Путь к папке (пустая строка для корневых общих папок) + limit: Максимальное количество элементов для возврата + + Returns: + Словарь с информацией о файлах и папках + """ + logger.info(f"Browsing files in {folder_path or 'root'}") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for file browsing") + return {"success": False, "error": "authentication_failed"} + + try: + if not folder_path: + # Если путь не указан, получаем список общих папок + result = self._make_api_request("SYNO.FileStation.List", "list_share", version=2) + + if not result: + logger.error("Failed to list shared folders") + return {"success": False, "error": "api_error"} + + return { + "success": True, + "items": result.get("shares", []), + "path": "", + "is_root": True + } + else: + # Получаем список файлов в указанной директории + params = { + "folder_path": folder_path, + "limit": limit, + "offset": 0, + "sort_by": "name", + "sort_direction": "ASC" + } + + result = self._make_api_request("SYNO.FileStation.List", "list", version=2, params=params) + + if not result: + logger.error(f"Failed to list files in {folder_path}") + return {"success": False, "error": "api_error"} + + return { + "success": True, + "items": result.get("files", []), + "path": folder_path, + "is_root": False, + "total": result.get("total", 0) + } + + except Exception as e: + logger.error(f"Error browsing files: {str(e)}") + return {"success": False, "error": str(e)} + + def manage_service(self, service_name: str, action: str = "status") -> Dict[str, Any]: + """Управление системным сервисом + + Args: + service_name: Имя сервиса + action: Действие (status/start/stop/restart) + + Returns: + Словарь с результатом операции + """ + logger.info(f"Managing service {service_name}, action: {action}") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for service management") + return {"success": False, "error": "authentication_failed"} + + try: + # Проверяем доступное API для управления сервисами + if action == "status": + result = self._make_api_request("SYNO.Core.Service", "get", version=1, + params={"service": service_name}) + else: + result = self._make_api_request("SYNO.Core.Service", action, version=1, + params={"service": service_name}) + + if not result: + logger.error(f"Failed to {action} service {service_name}") + return {"success": False, "error": "api_error"} + + return { + "success": True, + "service": service_name, + "action": action, + "result": result, + "status": result.get("status") if action == "status" else "completed" + } + + except Exception as e: + logger.error(f"Error managing service {service_name}: {str(e)}") + return {"success": False, "error": str(e)} + + def search_files(self, pattern: str, folder_path: str = "", limit: int = 100) -> Dict[str, Any]: + """Поиск файлов по шаблону + + Args: + pattern: Шаблон для поиска + folder_path: Путь к папке для поиска (пустая строка для всех общих папок) + limit: Максимальное количество результатов + + Returns: + Словарь с найденными файлами + """ + logger.info(f"Searching for {pattern} in {folder_path or 'all shared folders'}") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for file search") + return {"success": False, "error": "authentication_failed"} + + try: + if not folder_path: + # Получаем список всех общих папок для поиска + shares_result = self._make_api_request("SYNO.FileStation.List", "list_share", version=2) + + if not shares_result: + logger.error("Failed to list shared folders for search") + return {"success": False, "error": "api_error"} + + # Формируем список путей для поиска + folder_paths = [share.get("path") for share in shares_result.get("shares", [])] + else: + folder_paths = [folder_path] + + # Запускаем поиск + params = { + "folder_path": folder_paths, + "pattern": pattern, + "limit": limit, + "offset": 0 + } + + result = self._make_api_request("SYNO.FileStation.Search", "start", version=2, params=params) + + if not result: + logger.error(f"Failed to start search for {pattern}") + return {"success": False, "error": "api_error"} + + # Получаем taskid для проверки результатов + taskid = result.get("taskid") + if not taskid: + logger.error("No taskid received for search") + return {"success": False, "error": "no_task_id"} + + # Ожидаем завершения поиска + search_result = {"finished": False, "progress": 0} + for _ in range(10): # Максимум 10 попыток + search_status = self._make_api_request("SYNO.FileStation.Search", "status", version=2, + params={"taskid": taskid}) + + if not search_status: + break + + search_result["progress"] = search_status.get("progress", 0) + + if search_status.get("finished", False): + search_result["finished"] = True + break + + time.sleep(0.5) # Пауза между запросами + + # Получаем результаты поиска + if search_result["finished"]: + list_result = self._make_api_request("SYNO.FileStation.Search", "list", version=2, + params={"taskid": taskid, "limit": limit}) + + if list_result: + files = list_result.get("files", []) + return { + "success": True, + "pattern": pattern, + "results": files, + "total": list_result.get("total", len(files)) + } + + # Если не удалось получить результаты, останавливаем поиск + self._make_api_request("SYNO.FileStation.Search", "stop", version=2, + params={"taskid": taskid}) + + return { + "success": False, + "error": "search_timeout", + "progress": search_result["progress"] + } + + except Exception as e: + logger.error(f"Error searching files: {str(e)}") + return {"success": False, "error": str(e)} + + def get_backup_status(self) -> Dict[str, Any]: + """Получение информации о резервном копировании""" + logger.info("Getting backup status information") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for backup status request") + return {"success": False, "error": "authentication_failed"} + + try: + # Пробуем получить информацию о Hyper Backup + hyper_result = self._make_api_request("SYNO.Backup.Repository", "list", version=1) + + # Пробуем получить информацию о задачах Time Backup + time_result = self._make_api_request("SYNO.BackupService.Task", "list", version=1) + + # Проверяем статус резервного копирования USB + usb_result = self._make_api_request("SYNO.USBCopy", "status", version=1) + + backups = { + "hyper_backup": hyper_result.get("backups", []) if hyper_result else [], + "time_backup": time_result.get("tasks", []) if time_result else [], + "usb_copy": {"enabled": usb_result.get("enable", False) if usb_result else False} + } + + return { + "success": True, + "backups": backups, + "available_apis": { + "hyper_backup": hyper_result is not None, + "time_backup": time_result is not None, + "usb_copy": usb_result is not None + } + } + + except Exception as e: + logger.error(f"Error getting backup status: {str(e)}") + return {"success": False, "error": str(e)} + + def check_for_updates(self) -> Dict[str, Any]: + """Проверка наличия обновлений системы""" + logger.info("Checking for system updates") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for update check") + return {"success": False, "error": "authentication_failed"} + + try: + # Получаем текущую информацию о системе + system_info = self._make_api_request("SYNO.DSM.Info", "getinfo", version=2) + + if not system_info: + logger.error("Failed to get system info for update check") + return {"success": False, "error": "api_error"} + + # Проверяем наличие обновлений + update_result = self._make_api_request("SYNO.Core.Upgrade.Server", "check", version=1) + + # Получаем настройки автоматического обновления + settings_result = self._make_api_request("SYNO.Core.Upgrade.Server.Setting", "get", version=1) + + # Получаем информацию о доступных обновлениях + update_info = self._make_api_request("SYNO.Core.Upgrade.Server.Download", "list", version=1) + + current_version = system_info.get("version_string", "unknown") + auto_update_enabled = settings_result.get("auto_update", False) if settings_result else False + + updates = [] + if update_info and "updates" in update_info: + updates = update_info.get("updates", []) + + update_available = len(updates) > 0 + + return { + "success": True, + "current_version": current_version, + "update_available": update_available, + "auto_update_enabled": auto_update_enabled, + "updates": updates + } + + except Exception as e: + logger.error(f"Error checking for updates: {str(e)}") + return {"success": False, "error": str(e)} + + def get_quota_info(self) -> Dict[str, Any]: + """Получение информации о квотах пользователей""" + logger.info("Getting user quota information") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for quota info request") + return {"success": False, "error": "authentication_failed"} + + try: + # Получаем список пользователей + users_result = self._make_api_request("SYNO.Core.User", "list", version=1) + + if not users_result: + logger.error("Failed to get user list for quota info") + return {"success": False, "error": "api_error"} + + users = users_result.get("users", []) + user_quotas = [] + + # Получаем квоты для каждого пользователя + for user in users: + user_name = user.get("name") + if not user_name: + continue + + quota_result = self._make_api_request("SYNO.Core.Quota", "get", version=1, + params={"user_name": user_name}) + + if quota_result and "quotas" in quota_result: + user_quotas.append({ + "user": user_name, + "quotas": quota_result.get("quotas", []) + }) + + return { + "success": True, + "user_quotas": user_quotas + } + + except Exception as e: + logger.error(f"Error getting quota info: {str(e)}") + return {"success": False, "error": str(e)} diff --git a/.history/src/api/synology_20250830095635.py b/.history/src/api/synology_20250830095635.py new file mode 100644 index 0000000..672c37a --- /dev/null +++ b/.history/src/api/synology_20250830095635.py @@ -0,0 +1,1918 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Модуль для взаимодействия с API Synology NAS +""" + +import requests +from requests.adapters import HTTPAdapter +import json +import logging +import time +import urllib3 +from urllib3.util import Retry +from typing import Dict, Any, Optional, List +import socket +import struct +from time import sleep + +from src.config.config import ( + SYNOLOGY_HOST, + SYNOLOGY_PORT, + SYNOLOGY_USERNAME, + SYNOLOGY_PASSWORD, + SYNOLOGY_SECURE, + SYNOLOGY_TIMEOUT, + SYNOLOGY_MAC, + WOL_PORT, + SYNOLOGY_API_VERSION, + SYNOLOGY_POWER_API, + SYNOLOGY_INFO_API +) +from src.api.api_discovery import discover_available_apis, find_compatible_api + +# Отключение предупреждений о небезопасных SSL-соединениях +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + +logger = logging.getLogger(__name__) + +class SynologyAPI: + """Класс для взаимодействия с API Synology NAS""" + + def __init__(self): + """Инициализация класса SynologyAPI""" + logger.info("Creating API with auto-retry and connection pool") + logger.debug(f"Connection details: Host={SYNOLOGY_HOST}, Port={SYNOLOGY_PORT}, Secure={SYNOLOGY_SECURE}") + + self.protocol = "https" if SYNOLOGY_SECURE else "http" + self.base_url = f"{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi" + self.sid = None + self.session = requests.Session() + + # Настройка SSL + if self.protocol == "https": + logger.debug("SSL enabled, disabling certificate verification for internal network") + self.session.verify = False # Отключаем проверку SSL для внутренней сети + + # Добавляем пользовательские заголовки для улучшения совместимости с API + custom_headers = { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36', + 'Accept': 'application/json, text/javascript, */*; q=0.01', + 'Accept-Language': 'ru-RU,ru;q=0.9,en-US;q=0.8,en;q=0.7', + 'X-Requested-With': 'XMLHttpRequest', + 'Connection': 'keep-alive', + 'Referer': f'{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/' + } + self.session.headers.update(custom_headers) + logger.debug("Added browser-like headers for API compatibility") + + # Добавляем повторные попытки для HTTP-запросов + retry_strategy = Retry( + total=5, # Увеличиваем количество попыток + status_forcelist=[429, 500, 502, 503, 504, 404], + allowed_methods=["GET", "POST"], + backoff_factor=1.5, # Увеличиваем задержку между попытками + respect_retry_after_header=True + ) + adapter = HTTPAdapter( + max_retries=retry_strategy, + pool_connections=3, + pool_maxsize=10 + ) + self.session.mount("http://", adapter) + self.session.mount("https://", adapter) + + # Таймауты будут указаны в запросах + self.default_timeout = (SYNOLOGY_TIMEOUT, SYNOLOGY_TIMEOUT*2) + logger.debug(f"Setting default request timeout: {self.default_timeout}") + + # Кэш для хранения результатов запросов + self._cache = {} + self._cache_ttl = {} + self._last_online_check = 0 + self._last_online_status = False + self._online_check_interval = 30 # Интервал для кэширования проверок онлайн-статуса + + # Время последней успешной аутентификации и срок действия сессии + self._last_auth_time = 0 + self._auth_expiry = 3600 # По умолчанию 1 час + + # Информация о доступных API + self._available_apis = {} + self._api_info_ttl = 0 + + # Инициализируем API version resolver для автоматического определения совместимых API + self.api_resolver = None # Будет создан при необходимости + + def login(self) -> bool: + """Авторизация в API Synology NAS""" + # Сбрасываем SID для новой сессии + self.sid = None + + logger.info("Attempting to authenticate with Synology NAS...") + logger.debug(f"Base URL: {self.base_url}") + + # Проверяем доступность NAS перед запросом с помощью прямой TCP-проверки + # Избегаем вызова is_online(), чтобы не создавать рекурсию + online_status = self._check_tcp_connection() + if not online_status: + logger.error("Cannot login: Synology NAS is not reachable") + return False + + # Пробуем различные версии API для аутентификации + # Начинаем с версии 3, которая показала лучшую совместимость в тестах + auth_versions_to_try = [3, 2, 1, 6] # Пробуем сначала более стабильные версии + + for auth_version in auth_versions_to_try: + try: + # Определяем путь к API аутентификации + auth_path = "entry.cgi" # Используем entry.cgi вместо auth.cgi по умолчанию + + # Проверка информации API для определения доступных версий API + api_info_url = f"{self.base_url}/entry.cgi" + api_info_params = { + "api": "SYNO.API.Info", + "version": "1", + "method": "query", + "query": "SYNO.API.Auth" + } + + logger.debug(f"Querying API info for auth version {auth_version}") + try: + api_info_response = self.session.get( + api_info_url, + params=api_info_params, + timeout=self.default_timeout, + verify=False + ) + + if api_info_response.status_code == 200: + api_info_data = api_info_response.json() + if api_info_data.get("success"): + auth_info = api_info_data.get("data", {}).get("SYNO.API.Auth", {}) + max_version = auth_info.get("maxVersion", 6) + min_version = auth_info.get("minVersion", 1) + auth_path = auth_info.get("path", "entry.cgi") + logger.debug(f"Auth API versions: min={min_version}, max={max_version}, path={auth_path}") + + # Проверяем поддержку текущей версии + if auth_version < min_version or auth_version > max_version: + logger.warning(f"Auth API version {auth_version} not supported (min={min_version}, max={max_version}), trying next version") + continue + else: + logger.warning("Failed to query API info, using default auth path") + else: + logger.warning(f"API info request failed with status {api_info_response.status_code}, using default auth path") + except Exception as e: + logger.warning(f"Error querying API info: {str(e)}, using default auth path") + + # Основной запрос авторизации + url = f"{self.base_url}/{auth_path}" + params = { + "api": "SYNO.API.Auth", + "version": str(auth_version), + "method": "login", + "account": SYNOLOGY_USERNAME, + "passwd": SYNOLOGY_PASSWORD, + "session": "SynologyPowerControlBot", + "format": "cookie" + } + + # Для версии 6+ используем немного другой формат + if auth_version >= 6: + params["enable_syno_token"] = "yes" + + logger.debug(f"Sending auth request to {url} with API version {auth_version}") + start_time = time.time() + response = self.session.get( + url, + params=params, + timeout=self.default_timeout, + verify=False + ) + elapsed_time = time.time() - start_time + logger.debug(f"Auth request completed in {elapsed_time:.2f}s with status code {response.status_code}") + + # Проверка статуса HTTP + if response.status_code != 200: + logger.error(f"HTTP error: {response.status_code}") + logger.debug(f"Response content: {response.text[:200]}") + continue # Пробуем следующую версию + + try: + data = response.json() + except json.JSONDecodeError: + logger.error("Failed to decode JSON response") + logger.debug(f"Response content: {response.text[:200]}") + continue # Пробуем следующую версию + + if data.get("success"): + self.sid = data.get("data", {}).get("sid") + self._last_auth_time = time.time() + logger.info(f"Successfully logged in to Synology NAS using API version {auth_version}") + logger.debug(f"Session ID obtained: {self.sid[:5] if self.sid else 'None'}...") + + # Получаем и сохраняем токен SYNO, если он есть + syno_token = data.get("data", {}).get("synotoken") + if syno_token: + self.session.headers.update({'X-SYNO-TOKEN': syno_token}) + logger.debug("Added X-SYNO-TOKEN header for improved API compatibility") + + # Также добавляем SID в cookies для улучшения совместимости + self.session.cookies.update({ + 'id': self.sid, + 'sid': self.sid + }) + logger.debug("Added SID to session cookies for improved compatibility") + + # Проверка валидности полученной сессии с помощью простого запроса + # Будем использовать SYNO.API.Info без проверки сложных методов + + # Даем системе немного времени для инициализации сессии + time.sleep(0.5) + + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to log in with version {auth_version}: Error code {error_code}") + + # Если ошибка связана с версией API, пробуем следующую версию + if error_code in [104, 105]: + logger.warning(f"Auth version {auth_version} not supported, trying next version") + continue + + # Дополнительная диагностика + if error_code == 400: + logger.error("Authentication error: Invalid credentials") + elif error_code == 401: + logger.error("Authentication error: Account disabled") + elif error_code == 402: + logger.error("Authentication error: Permission denied") + elif error_code == 403: + logger.error("Authentication error: 2-factor authentication required") + elif error_code == 404: + logger.error("Authentication error: Failed to authenticate with 2-factor authentication") + + # Если ошибка связана с неверными учетными данными, нет смысла пробовать другие версии API + if error_code in [400, 401, 402, 403, 404]: + return False + + except requests.exceptions.Timeout: + logger.error(f"Connection timeout during auth with version {auth_version}") + continue # Пробуем следующую версию + except requests.exceptions.ConnectionError as e: + logger.error(f"Connection error during auth with version {auth_version}: {str(e)}") + continue # Пробуем следующую версию + except requests.RequestException as e: + logger.error(f"Request error during auth with version {auth_version}: {str(e)}") + continue # Пробуем следующую версию + except Exception as e: + logger.error(f"Unexpected error during login with version {auth_version}: {str(e)}", exc_info=True) + continue # Пробуем следующую версию + + # Если все версии не сработали + logger.error("Failed to authenticate with any API version") + return False + + def _validate_session(self) -> bool: + """Проверяет валидность сессии после авторизации""" + if not self.sid: + return False + + # Попробуем сделать простой запрос для проверки сессии + test_apis = [ + {"api": "SYNO.Core.System", "method": "info", "version": 1}, + {"api": "SYNO.DSM.Info", "method": "getinfo", "version": 1} + ] + + for test_api in test_apis: + try: + url = f"{self.base_url}/entry.cgi" + params = { + "api": test_api["api"], + "version": str(test_api["version"]), + "method": test_api["method"], + "_sid": self.sid + } + + response = self.session.get(url, params=params, timeout=self.default_timeout, verify=False) + + if response.status_code == 200: + data = response.json() + if data.get("success"): + logger.debug(f"Session validation successful using {test_api['api']}") + return True + else: + error_code = data.get("error", {}).get("code", -1) + if error_code != 119: # Не сессия истекла + logger.debug(f"Session validation with {test_api['api']} returned error code {error_code}") + return True # Считаем сессию валидной, если ошибка не связана с истечением сессии + except Exception as e: + logger.warning(f"Error during session validation with {test_api['api']}: {str(e)}") + + logger.warning("Session validation failed with all test APIs") + return False + + def logout(self) -> bool: + """Выход из API Synology NAS""" + if not self.sid: + return True + + try: + url = f"{self.base_url}/auth.cgi" + params = { + "api": "SYNO.API.Auth", + "version": "1", + "method": "logout", + "session": "SynologyPowerControlBot", + "_sid": self.sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + self.sid = None + logger.info("Successfully logged out from Synology NAS") + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to log out from Synology NAS: Error code {error_code}") + return False + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + return False + + def _make_api_request(self, api_name: str, method: str, version: int = 1, + params: Optional[Dict[str, Any]] = None, retry_auth: bool = True, retry_count: int = 0) -> Optional[Dict[str, Any]]: + """Обобщенный метод для выполнения API запросов с обработкой ошибок""" + # Ограничение на количество повторных попыток + if retry_count >= 3: + logger.error(f"Too many retries for {api_name}.{method}, giving up") + return None + + # Проверка наличия авторизации + if not self.sid and not self.login(): + logger.error(f"Not authenticated for API request: {api_name}.{method}") + return None + + # Проверка информации API для определения пути и поддерживаемой версии + api_info_url = f"{self.base_url}/entry.cgi" + api_info_params = { + "api": "SYNO.API.Info", + "version": "1", + "method": "query", + "query": api_name + } + + api_path = "entry.cgi" + try: + logger.debug(f"Querying API info for {api_name}") + api_info_response = self.session.get( + api_info_url, + params=api_info_params, + timeout=self.default_timeout, + verify=False + ) + + if api_info_response.status_code == 200: + api_info_data = api_info_response.json() + if api_info_data.get("success"): + api_info = api_info_data.get("data", {}).get(api_name, {}) + if api_info: + max_version = api_info.get("maxVersion", version) + min_version = api_info.get("minVersion", version) + api_path = api_info.get("path", "entry.cgi") + + # Проверка, поддерживается ли запрошенная версия + if version < min_version: + logger.warning(f"API version {version} for {api_name} is below minimum {min_version}, using {min_version}") + version = min_version + elif version > max_version: + logger.warning(f"API version {version} for {api_name} exceeds maximum {max_version}, using {max_version}") + version = max_version + + logger.debug(f"Using API path: {api_path}, version: {version}") + else: + logger.warning(f"API {api_name} not found in API info, using defaults") + except Exception as e: + logger.warning(f"Error querying API info for {api_name}: {str(e)}, using defaults") + + # Подготовка базовых параметров запроса + base_params = { + "api": api_name, + "version": str(version), + "method": method, + "_sid": self.sid # Используем _sid вместо sid для лучшей совместимости + } + + # Добавление дополнительных параметров, если они заданы + if params: + base_params.update(params) + + url = f"{self.base_url}/{api_path}" + logger.debug(f"API request: {api_name}.{method} v{version} to {url}") + logger.debug(f"Full request params: {base_params}") + + try: + start_time = time.time() + response = self.session.get( + url, + params=base_params, + timeout=self.default_timeout, + verify=False + ) + elapsed_time = time.time() - start_time + logger.debug(f"API request completed in {elapsed_time:.2f}s with status {response.status_code}") + + # Проверка статуса HTTP + if response.status_code != 200: + logger.error(f"HTTP error: {response.status_code} for {api_name}.{method}") + + # Повторная попытка при ошибках соединения + if response.status_code in [500, 502, 503, 504]: + logger.info(f"Server error, retrying request for {api_name}.{method}") + sleep(2) + return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1) + + return None + + try: + data = response.json() + except json.JSONDecodeError: + logger.error(f"Failed to decode JSON response for {api_name}.{method}") + logger.debug(f"Response content: {response.text[:200]}") + + # Повторная попытка при ошибках декодирования + logger.info(f"JSON decode error, retrying request for {api_name}.{method}") + sleep(2) + return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1) + + if data.get("success"): + logger.info(f"API request successful for {api_name}.{method}") + return data.get("data", {}) + else: + error_code = data.get("error", {}).get("code", -1) + error_desc = self._get_error_description(error_code) + logger.error(f"API error for {api_name}.{method}: {error_code} - {error_desc}") + + # Ошибки доступа или прав часто встречаются, но они не критичные + # Например, ошибка 102 означает, что нет прав, но NAS доступен + if error_code in [102, 103, 104, 105]: + logger.warning(f"Permission-related error for {api_name}.{method}: {error_code}") + # Возвращаем пустой словарь вместо None, + # чтобы вызывающий код мог понять, что запрос выполнен + return {} + + # Если ошибка связана с авторизацией и нам разрешено повторить попытку + if error_code in [106, 107, 119] and retry_auth: + logger.info(f"Session error (code {error_code}), creating fresh session...") + self.sid = None # Сбрасываем SID + + # Для ошибки 119 (Session timeout) дадим системе немного времени + if error_code == 119: + logger.info("Session timeout detected, waiting before retry...") + sleep(3) + + if self.login(): + logger.info("Re-authenticated with fresh session, retrying API request...") + # Рекурсивный вызов, но со счетчиком повторов + return self._make_api_request(api_name, method, version, params, False, retry_count + 1) + + # Для некоторых ошибок можно автоматически повторить запрос + if error_code in [408, 429, 500, 502, 503, 504]: + logger.info(f"Temporary error {error_code}, retrying request for {api_name}.{method}") + sleep(2) + return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1) + + return None + + except requests.exceptions.Timeout: + logger.error(f"Request timeout for {api_name}.{method}") + + # Повторная попытка при таймауте + if retry_count < 2: + logger.info(f"Timeout, retrying request for {api_name}.{method}") + sleep(2) + return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1) + + return None + except requests.exceptions.ConnectionError as e: + logger.error(f"Connection error for {api_name}.{method}: {str(e)}") + + # Повторная попытка при ошибке соединения + if retry_count < 2: + logger.info(f"Connection error, retrying request for {api_name}.{method}") + sleep(2) + return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1) + + return None + except Exception as e: + logger.error(f"Unexpected error during {api_name}.{method}: {str(e)}") + return None + + def get_system_status(self) -> Dict[str, Any]: + """Получение статуса системы""" + # Проверяем доступность системы + if not self.is_online(): + logger.info("Device is offline, skipping API request") + return {"status": "offline"} + + # Проверяем, есть ли кэшированный результат + cache_key = "system_status" + current_time = time.time() + if cache_key in self._cache and current_time - self._cache_ttl.get(cache_key, 0) < 60: + logger.debug("Using cached system status") + return self._cache[cache_key] + + # Используем рекомендованный API для получения информации о системе + logger.info(f"Getting system info using {SYNOLOGY_INFO_API} v{SYNOLOGY_API_VERSION}") + + # Для SYNO.DSM.Info метод getinfo, для других обычно используется get или info + if SYNOLOGY_INFO_API == "SYNO.DSM.Info": + method = "getinfo" + else: + method = "get" + + result = self._make_api_request(SYNOLOGY_INFO_API, method, version=SYNOLOGY_API_VERSION) + + if result: + logger.info(f"Successfully retrieved system info using {SYNOLOGY_INFO_API}") + + # Формируем расширенный ответ с дополнительной информацией + system_info = { + "status": "online", + "hostname": result.get("hostname", "unknown"), + "model": result.get("model", "unknown"), + "version": result.get("version", "unknown"), + "uptime": result.get("uptime", 0), + "time": current_time, + "is_online": True, + "api_used": SYNOLOGY_INFO_API + } + + # Сохраняем в кэше + self._cache[cache_key] = system_info + self._cache_ttl[cache_key] = current_time + + return system_info + + # Если основной API не сработал, пробуем резервные варианты + logger.warning(f"Failed to retrieve system info with {SYNOLOGY_INFO_API}, trying fallback APIs") + + # Пробуем резервные API + apis_to_try = [ + {"name": "SYNO.DSM.Info", "method": "getinfo", "version": 2}, + {"name": "SYNO.Core.System", "method": "info", "version": 1}, + {"name": "SYNO.Core.System.Status", "method": "get", "version": 1}, + {"name": "SYNO.Core.System.Info", "method": "get", "version": 1}, + ] + + for api in apis_to_try: + if api["name"] == SYNOLOGY_INFO_API: + continue # Пропускаем уже проверенный API + + logger.info(f"Trying to get system info using {api['name']}.{api['method']} v{api['version']}") + result = self._make_api_request(api["name"], api["method"], version=api["version"]) + + if result: + logger.info(f"Successfully retrieved system info using {api['name']}") + + # Формируем расширенный ответ с дополнительной информацией + system_info = { + "status": "online", + "hostname": result.get("hostname", "unknown"), + "model": result.get("model", "unknown"), + "version": result.get("version", "unknown"), + "uptime": result.get("uptime", 0), + "time": current_time, + "is_online": True, + "api_used": api["name"] + } + + # Сохраняем в кэше + self._cache[cache_key] = system_info + self._cache_ttl[cache_key] = current_time + + return system_info + + # Если все запросы не удались, но система онлайн, возвращаем базовую информацию + logger.warning("Failed to retrieve system info with all API methods") + return { + "status": "error", + "error": "Failed to fetch system information", + "is_online": True, + "time": current_time + } + + def shutdown_system(self) -> bool: + """Выключение системы""" + # Проверяем, включено ли устройство перед попыткой его выключить + if not self.is_online(force_check=True): + logger.info("Synology NAS is already offline, no need to shut down") + return True + + logger.info("Attempting to shutdown Synology NAS...") + + # Попробуем сначала использовать предпочтительный API для управления питанием + logger.info(f"Trying shutdown with {SYNOLOGY_POWER_API} v{SYNOLOGY_API_VERSION}") + + # Для SYNO.Core.Hardware.PowerRecovery используется метод setPowerOnState + # Для других API обычно используется метод shutdown или reboot + if SYNOLOGY_POWER_API == "SYNO.Core.Hardware.PowerRecovery": + # Для этого API нужны специальные параметры + params = {"state": "powerbtn"} # powerbtn имитирует нажатие кнопки питания + result = self._make_api_request(SYNOLOGY_POWER_API, "setPowerOnState", version=SYNOLOGY_API_VERSION, params=params) + else: + # Пробуем стандартный метод + result = self._make_api_request(SYNOLOGY_POWER_API, "shutdown", version=SYNOLOGY_API_VERSION) + + if result is not None: + logger.info(f"Successfully initiated system shutdown using {SYNOLOGY_POWER_API}") + + # Даем системе время начать процесс выключения + logger.info("Waiting for shutdown to initialize...") + sleep(5) + + # Проверяем статус + if not self.is_online(force_check=True): + logger.info("System is now offline. Shutdown confirmed successful.") + return True + else: + logger.info("System still appears to be online, but shutdown may be in progress.") + return True + else: + logger.warning(f"Failed to shutdown using {SYNOLOGY_POWER_API}, trying alternative methods") + + # Если не сработал основной метод, пробуем резервные варианты + # Проверка всех доступных методов API для выключения + apis_to_try = [ + {"name": "SYNO.Core.System", "method": "shutdown", "version": 3}, + {"name": "SYNO.Core.System.Power", "method": "shutdown", "version": 1}, + {"name": "SYNO.DSM.Power", "method": "shutdown", "version": 1}, + {"name": "SYNO.System.Power", "method": "shutdown", "version": 1}, + {"name": "SYNO.DSM.System", "method": "shutdown", "version": 1} + ] + + # Проверяем доступные API + try: + api_info_url = f"{self.base_url}/entry.cgi" + api_info_params = { + "api": "SYNO.API.Info", + "version": "1", + "method": "query", + "query": "SYNO.Core.System.Power,SYNO.DSM.Power,SYNO.Core.System,SYNO.System.Power,SYNO.DSM.System" + } + + logger.debug("Checking available shutdown APIs") + api_info_response = self.session.get( + api_info_url, + params=api_info_params, + timeout=self.default_timeout, + verify=False + ) + + if api_info_response.status_code == 200: + api_info_data = api_info_response.json() + if api_info_data.get("success"): + available_apis = api_info_data.get("data", {}) + logger.debug(f"Available APIs: {list(available_apis.keys())}") + + # Фильтруем только доступные API + filtered_apis = [] + for api in apis_to_try: + if api["name"] in available_apis: + api_info = available_apis[api["name"]] + # Проверка версии API + if api_info.get("minVersion", 1) <= api["version"] <= api_info.get("maxVersion", 1): + filtered_apis.append(api) + logger.debug(f"Adding {api['name']} to available shutdown APIs") + else: + logger.debug(f"API {api['name']} version {api['version']} not supported. Supported: {api_info.get('minVersion')}-{api_info.get('maxVersion')}") + + if filtered_apis: + apis_to_try = filtered_apis + else: + logger.warning("No compatible APIs found, trying all methods as fallback") + else: + logger.warning("Failed to query API info, using default methods") + else: + logger.warning(f"API info request failed with status {api_info_response.status_code}, using default methods") + except Exception as e: + logger.warning(f"Error querying API info: {str(e)}, using default methods") + + # Пробуем все доступные методы по порядку + for api in apis_to_try: + logger.info(f"Trying shutdown with {api['name']}.{api['method']} v{api['version']}") + result = self._make_api_request(api["name"], api["method"], version=api["version"]) + + if result is not None: + logger.info(f"Successfully initiated system shutdown using {api['name']}") + + # Даем системе время начать процесс выключения + logger.info("Waiting for shutdown to initialize...") + sleep(5) + + # Проверяем статус + if not self.is_online(force_check=True): + logger.info("System is now offline. Shutdown confirmed successful.") + return True + else: + logger.info("System still appears to be online, but shutdown may be in progress.") + return True + else: + logger.warning(f"Failed to shutdown using {api['name']}.{api['method']} v{api['version']}") + + # Если ни один метод не сработал, но система стала недоступна + if not self.is_online(force_check=True): + logger.info("System appears to be shutting down despite API errors") + return True + + logger.error("Failed to shutdown system after trying multiple APIs") + return False + + def reboot_system(self) -> bool: + """Перезагрузка системы""" + # Проверяем, включена ли система + if not self.is_online(force_check=True): + logger.error("Cannot reboot: System is offline") + return False + + logger.info("Attempting to reboot Synology NAS...") + + # Попробуем сначала использовать предпочтительный API для управления питанием + logger.info(f"Trying reboot with {SYNOLOGY_POWER_API} v{SYNOLOGY_API_VERSION}") + + # Пробуем использовать наиболее совместимые API для перезагрузки + # SYNO.Core.System обычно доступен на большинстве систем + logger.info("Trying reboot with SYNO.Core.System API") + result = self._make_api_request("SYNO.Core.System", "reboot", version=1) + + if result is None: + # Если не сработало, пробуем альтернативные методы + logger.info("Trying alternative reboot method with SYNO.DSM.System API") + result = self._make_api_request("SYNO.DSM.System", "reboot", version=1) + + if result is None and SYNOLOGY_POWER_API != "SYNO.Core.System": + # Пробуем настроенный в конфигурации API, если он отличается + logger.info(f"Trying reboot with configured API: {SYNOLOGY_POWER_API}") + methods_to_try = ["restart", "reboot"] + for method in methods_to_try: + result = self._make_api_request(SYNOLOGY_POWER_API, method, version=SYNOLOGY_API_VERSION) + if result is not None: + logger.info(f"Successfully used method {method} for {SYNOLOGY_POWER_API}") + break + + if result is not None: + logger.info(f"Successfully initiated system reboot using {SYNOLOGY_POWER_API}") + + # Даем системе время начать процесс перезагрузки + logger.info("Waiting for reboot to initialize...") + sleep(5) + + # Ждем, пока система станет недоступна (признак перезагрузки) + reboot_started = False + for i in range(12): # Проверяем в течение 60 секунд (12 * 5сек) + if not self.is_online(force_check=True): + logger.info(f"System went offline after {i*5} seconds, reboot in progress") + reboot_started = True + break + logger.debug(f"System still online, waiting... ({i+1}/12)") + sleep(5) + + if reboot_started: + # Ждем, пока система снова станет доступна + logger.info("Waiting for system to come back online...") + return self.wait_for_boot(max_attempts=30, delay=10) + else: + logger.warning("System did not go offline after reboot command, but command was accepted") + # Даже если система не ушла оффлайн, команда могла быть принята + return True + else: + logger.warning(f"Failed to reboot using {SYNOLOGY_POWER_API}, trying alternative methods") + + # Если не сработал основной метод, пробуем резервные варианты + # Проверка всех доступных методов API для перезагрузки + apis_to_try = [ + {"name": "SYNO.Core.System.Power", "method": "restart", "version": 1}, + {"name": "SYNO.DSM.Power", "method": "reboot", "version": 1}, + {"name": "SYNO.Core.System", "method": "reboot", "version": 3}, + {"name": "SYNO.System.Power", "method": "reboot", "version": 1}, + {"name": "SYNO.DSM.System", "method": "reboot", "version": 1}, + {"name": "SYNO.Core.Hardware.NeedReboot", "method": "reboot", "version": 1} + ] + + # Получаем список доступных API + try: + api_info_url = f"{self.base_url}/entry.cgi" + api_info_params = { + "api": "SYNO.API.Info", + "version": "1", + "method": "query", + "query": "SYNO.Core.System.Power,SYNO.DSM.Power,SYNO.Core.System,SYNO.System.Power,SYNO.DSM.System,SYNO.Core.Hardware.NeedReboot" + } + + logger.debug("Checking available reboot APIs") + api_info_response = self.session.get( + api_info_url, + params=api_info_params, + timeout=self.default_timeout, + verify=False + ) + + if api_info_response.status_code == 200: + api_info_data = api_info_response.json() + if api_info_data.get("success"): + available_apis = api_info_data.get("data", {}) + logger.debug(f"Available APIs: {list(available_apis.keys())}") + + # Фильтруем только доступные API + filtered_apis = [] + for api in apis_to_try: + if api["name"] in available_apis: + api_info = available_apis[api["name"]] + # Проверка версии API + if api_info.get("minVersion", 1) <= api["version"] <= api_info.get("maxVersion", 1): + filtered_apis.append(api) + logger.debug(f"Adding {api['name']} to available reboot APIs") + else: + logger.debug(f"API {api['name']} version {api['version']} not supported. Supported: {api_info.get('minVersion')}-{api_info.get('maxVersion')}") + + if filtered_apis: + apis_to_try = filtered_apis + else: + logger.warning("No compatible APIs found, trying all methods as fallback") + else: + logger.warning("Failed to query API info, using default methods") + else: + logger.warning(f"API info request failed with status {api_info_response.status_code}, using default methods") + except Exception as e: + logger.warning(f"Error querying API info: {str(e)}, using default methods") + + # Пробуем все доступные методы по порядку + for api in apis_to_try: + logger.info(f"Trying reboot with {api['name']}.{api['method']} v{api['version']}") + result = self._make_api_request(api["name"], api["method"], version=api["version"]) + + if result is not None: + logger.info(f"Successfully initiated system reboot using {api['name']}") + + # Даем системе время начать процесс перезагрузки + logger.info("Waiting for reboot to initialize...") + sleep(5) + + # Ждем, пока система станет недоступна (признак перезагрузки) + reboot_started = False + for i in range(12): # Проверяем в течение 60 секунд (12 * 5сек) + if not self.is_online(force_check=True): + logger.info(f"System went offline after {i*5} seconds, reboot in progress") + reboot_started = True + break + logger.debug(f"System still online, waiting... ({i+1}/12)") + sleep(5) + + if reboot_started: + # Ждем, пока система снова станет доступна + logger.info("Waiting for system to come back online...") + return self.wait_for_boot(max_attempts=30, delay=10) + else: + logger.warning("System did not go offline after reboot command, but command was accepted") + # Даже если система не ушла оффлайн, команда могла быть принята + return True + else: + logger.warning(f"Failed to reboot using {api['name']}.{api['method']} v{api['version']}") + + logger.error("Failed to reboot system after trying multiple APIs") + return False + + def _get_error_description(self, error_code: int) -> str: + """Получение описания ошибки по коду""" + error_descriptions = { + 100: "Unknown error", + 101: "Invalid parameter", + 102: "API does not exist", + 103: "Method does not exist", + 104: "Version does not support", + 105: "Permission denied", + 106: "Session timeout", + 107: "Session interrupted by duplicate login", + 400: "Invalid credentials", + 401: "Account disabled", + 402: "Permission denied", + 403: "2FA required", + 404: "Failed to authenticate with 2FA" + } + return error_descriptions.get(error_code, "Unknown error code") + + def _check_tcp_connection(self) -> bool: + """Проверка базового TCP-соединения с Synology NAS""" + try: + socket_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + socket_obj.settimeout(SYNOLOGY_TIMEOUT) + result = socket_obj.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT)) + socket_obj.close() + + return result == 0 + except socket.error as e: + logger.error(f"Socket error during connection check: {str(e)}") + return False + except Exception as e: + logger.error(f"Unexpected error during connection check: {str(e)}") + return False + + def is_online(self, force_check=False) -> bool: + """Проверка онлайн-статуса Synology NAS""" + # Используем кэшированное значение, если доступно и не устарело + current_time = time.time() + if not force_check and (current_time - self._last_online_check) < self._online_check_interval: + logger.debug(f"Using cached online status: {self._last_online_status}") + return self._last_online_status + + logger.info("Checking if NAS is online...") + + # Проверяем TCP-соединение + online_status = self._check_tcp_connection() + logger.info(f"Detected Synology NAS online status: {online_status}") + + # Если TCP-соединение успешно и у нас есть действующий SID, + # попробуем более детальную проверку через API + if online_status and self.sid: + logger.info("Trying to fetch more detailed online status through API...") + + # Пробуем разные API для проверки онлайн-статуса + api_checks = [ + {"api": "SYNO.DSM.Info", "version": "2", "method": "getinfo"}, + {"api": "SYNO.Core.System", "version": "1", "method": "info"}, + {"api": "SYNO.Core.System.Status", "version": "1", "method": "get"} + ] + + api_success = False + for api_check in api_checks: + try: + url = f"{self.base_url}/entry.cgi" + params = { + "api": api_check["api"], + "version": api_check["version"], + "method": api_check["method"], + "sid": self.sid + } + + logger.debug(f"Trying online status check with {api_check['api']}") + response = self.session.get( + url, + params=params, + timeout=self.default_timeout, + verify=False + ) + + if response.status_code == 200: + data = response.json() + if data.get("success"): + logger.info(f"API request successful for {api_check['api']}") + logger.info("Synology NAS is online with API access") + api_success = True + break + else: + error_code = data.get("error", {}).get("code", -1) + logger.warning(f"API response indicates an error: {error_code}, but NAS is reachable") + else: + logger.warning(f"API returned status code {response.status_code}") + except Exception as e: + logger.warning(f"API check failed for {api_check['api']}, but TCP connection succeeded: {str(e)}") + + if not api_success: + logger.warning("All API checks failed, but TCP connection is successful") + + # Обновляем кэшированное значение + self._last_online_check = current_time + self._last_online_status = online_status + + return online_status + + def wake_on_lan(self) -> bool: + """Отправка Wake-on-LAN пакета для включения Synology NAS""" + if not SYNOLOGY_MAC: + logger.error("MAC address not configured") + return False + + try: + # Преобразование MAC-адреса в байты + mac_address = SYNOLOGY_MAC.replace(':', '').replace('-', '') + if len(mac_address) != 12: + logger.error(f"Invalid MAC address format: {SYNOLOGY_MAC}") + return False + + try: + mac_bytes = bytes.fromhex(mac_address) + except ValueError as e: + logger.error(f"Failed to parse MAC address: {str(e)}") + logger.error(f"MAC address must contain only hex characters: {SYNOLOGY_MAC}") + return False + + # Создание Magic Packet + magic_packet = b'\xff' * 6 + mac_bytes * 16 + + # Отправка пакета на конкретный адрес + try: + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) + sock.sendto(magic_packet, (SYNOLOGY_HOST, WOL_PORT)) + sock.close() + logger.info(f"Wake-on-LAN packet sent to {SYNOLOGY_MAC} at {SYNOLOGY_HOST}:{WOL_PORT}") + except Exception as e: + logger.error(f"Error sending directed WoL packet: {str(e)}") + return False + + # Для надежности отправляем также широковещательный пакет + try: + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) + + # Используем стандартный широковещательный адрес + broadcast_addr = "255.255.255.255" + sock.sendto(magic_packet, (broadcast_addr, WOL_PORT)) + sock.close() + logger.info(f"Wake-on-LAN broadcast packet sent to {broadcast_addr}:{WOL_PORT}") + except Exception as e: + logger.warning(f"Error sending broadcast WoL packet (non-critical): {str(e)}") + # Не считаем ошибкой, т.к. основной пакет уже отправлен + + return True + + except Exception as e: + logger.error(f"Unexpected error in wake_on_lan: {str(e)}") + return False + + def wait_for_boot(self, max_attempts: int = 30, delay: int = 10) -> bool: + """Ожидание загрузки Synology NAS""" + logger.info(f"Waiting for Synology NAS to boot (timeout: {max_attempts * delay}s)...") + + for attempt in range(max_attempts): + # Принудительно проверяем статус без использования кэша + if self.is_online(force_check=True): + logger.info(f"Synology NAS is online after {attempt + 1} attempts ({(attempt + 1) * delay}s)") + + # Проверяем, что не только сеть доступна, но и API загрузился + api_ready = False + logger.info("Waiting for API services to initialize...") + + for api_check in range(5): # Даем еще до 50 секунд для загрузки API + if self.sid or self.login(): + api_ready = True + logger.info(f"API services are ready after {api_check + 1} attempts") + break + logger.debug(f"API not ready yet, waiting... ({api_check + 1}/5)") + sleep(10) + + if not api_ready: + logger.warning("System is online but API services may not be fully initialized") + + # Дадим дополнительное время для полной загрузки всех сервисов + sleep(delay) + return True + + sleep(delay) + logger.info(f"Waiting for boot... Attempt {attempt + 1}/{max_attempts}") + + logger.error(f"Synology NAS didn't come online after {max_attempts * delay} seconds") + return False + + def power_on(self) -> bool: + """Включение Synology NAS""" + # Принудительная проверка статуса + if self.is_online(force_check=True): + logger.info("Synology NAS is already online") + return True + + logger.info("Powering on Synology NAS via Wake-on-LAN...") + + # Проверяем, настроен ли MAC-адрес + if not SYNOLOGY_MAC: + logger.error("Cannot power on: MAC address not configured in settings") + return False + + # Пробуем отправить несколько WoL пакетов для надежности + success = False + for attempt in range(3): + logger.debug(f"Sending WoL packet, attempt {attempt + 1}/3") + if self.wake_on_lan(): + success = True + break + sleep(1) + + if not success: + logger.error("Failed to send Wake-on-LAN packets") + return False + + # Ожидание загрузки + logger.info("WoL packets sent successfully, waiting for system to boot...") + boot_result = self.wait_for_boot(max_attempts=30, delay=10) + + if boot_result: + # Проверяем доступность API после загрузки + system_status = self.get_system_status() + if system_status.get("status") == "online": + logger.info("System booted successfully with API access") + return True + else: + logger.warning("System appears to be online but API may not be fully ready") + return True + else: + logger.error("System did not come online after WoL") + return False + + def power_off(self) -> bool: + """Выключение Synology NAS""" + if not self.is_online(force_check=True): + logger.info("Synology NAS is already offline") + return True + + logger.info("Powering off Synology NAS...") + result = self.shutdown_system() + + if result: + # Дополнительная проверка, что система действительно выключилась + logger.info("Verifying system is offline...") + for attempt in range(12): # Проверяем в течение 2 минут (12 * 10 сек) + if not self.is_online(force_check=True): + logger.info(f"System confirmed offline after {attempt * 10} seconds") + return True + logger.debug(f"System still shutting down, checking again... ({attempt + 1}/12)") + sleep(10) + + logger.warning("System still appears to be online after shutdown command") + return False + else: + logger.error("Failed to initiate shutdown") + return False + + # Заглушки для расширенных методов + def get_shared_folders(self) -> List[Dict[str, Any]]: + """Получение списка общих папок""" + logger.info("Getting list of shared folders") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for shared folders request") + return [] + + try: + # Запрашиваем список общих папок через FileStation API + result = self._make_api_request("SYNO.FileStation.List", "list_share", version=2) + + if not result: + # Пробуем альтернативный API + logger.info("Trying alternative API for shared folders") + alt_result = self._make_api_request("SYNO.FileStation.List", "list_share", version=1) + if alt_result: + return alt_result.get("shares", []) + return [] + + return result.get("shares", []) + + except Exception as e: + logger.error(f"Error getting shared folders: {str(e)}") + return [] + + def get_system_load(self) -> Dict[str, Any]: + """Получение информации о загрузке системы""" + logger.info("Getting system load information") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for system load request") + return {} + + try: + # Запрашиваем информацию о загрузке системы + result = self._make_api_request("SYNO.Core.System.Utilization", "get", version=1) + + if not result: + # Пробуем альтернативный API + logger.info("Trying alternative API for system load") + alt_result = self._make_api_request("SYNO.Core.System", "info", version=3) + + if not alt_result: + return {} + + # Формируем из частичных данных + return { + "cpu_load": alt_result.get("cpu_usage", 0), + "memory": { + "total": alt_result.get("memory_size", 0), + "used": alt_result.get("memory_usage", 0), + "usage_percent": alt_result.get("memory_usage_percent", 0) + } + } + + # Формируем структурированный результат + return { + "cpu_load": result.get("cpu", {}).get("user", 0) + result.get("cpu", {}).get("system", 0), + "memory": result.get("memory", {}), + "network": result.get("network", {}) + } + + except Exception as e: + logger.error(f"Error getting system load: {str(e)}") + return {} + + def is_online_api(self) -> bool: + """Проверка онлайн-статуса Synology NAS с использованием API""" + if not self.is_online(): + return False + + # Проверяем доступность API через авторизацию + if not self.sid and not self.login(): + return False + + return True + + def get_storage_status(self) -> Dict[str, Any]: + """Получение подробной информации о хранилище""" + logger.info("Getting storage status information") + + # Проверяем доступность NAS и API + if not self.is_online_api(): + logger.error("Cannot get storage status: NAS is not online or API is not accessible") + return {"error": "authentication_failed"} + + try: + # Пробуем получить информацию о хранилище через SYNO.Storage.CGI.Storage API + result = self._make_api_request("SYNO.Storage.CGI.Storage", "load_info", version=1) + + if not result: + # Пробуем альтернативный API + logger.info("Trying alternative API for storage info") + alt_result = self._make_api_request("SYNO.Core.Storage", "info", version=1) + + if not alt_result: + # Пробуем еще один альтернативный API + logger.info("Trying SYNO.Core.System API for storage info") + sys_result = self._make_api_request("SYNO.Core.System", "info", version=3) + + if not sys_result: + return { + "volumes": [], + "disks": [], + "total_size": 0, + "total_used": 0, + "error": "no_data" + } + + # Извлекаем базовую информацию о хранилище из системной информации + return { + "volumes": [], + "disks": [], + "total_size": sys_result.get("hdd_total_size", 0) * 1024**2, # МБ в байты + "total_used": sys_result.get("hdd_total_size", 0) * sys_result.get("hdd_usage", 0) / 100 * 1024**2, + } + + # Обрабатываем данные из альтернативного API + volumes = alt_result.get("volumes", []) + disks = alt_result.get("disks", []) + + else: + # Обрабатываем данные из основного API + volumes = result.get("volumes", []) + disks = result.get("disks", []) + + # Рассчитываем общие размеры + total_size = 0 + total_used = 0 + + for volume in volumes: + volume_size = volume.get("size", {}).get("total", 0) + volume_used = volume.get("size", {}).get("used", 0) + + total_size += volume_size + total_used += volume_used + + return { + "volumes": volumes, + "disks": disks, + "total_size": total_size, + "total_used": total_used + } + + except Exception as e: + logger.error(f"Error in get_storage_status: {str(e)}") + return {"error": str(e)} + + def get_security_status(self) -> Dict[str, Any]: + """Получение информации о состоянии безопасности""" + logger.info("Getting security status information") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for security status request") + return {"success": False, "error": "authentication_failed"} + + try: + # Пробуем получить информацию о безопасности через API Security Scan + result = self._make_api_request("SYNO.Core.SecurityScan.Status", "get", version=1) + + if not result: + # Пробуем альтернативный API + logger.info("Trying alternative API for security status") + # Проверяем статус брандмауэра + firewall_result = self._make_api_request("SYNO.Core.Security.Firewall.Status", "get", version=1) + + # Проверяем статус автоматических обновлений + update_result = self._make_api_request("SYNO.Core.Upgrade.Server.Setting", "get", version=1) + + # Если ни один из API не отвечает + if not firewall_result and not update_result: + # Получаем общую информацию о системе для базовой проверки безопасности + sys_result = self._make_api_request("SYNO.DSM.Info", "getinfo", version=2) + + if not sys_result: + return { + "success": False, + "status": "unknown", + "last_check": None, + "is_secure": False, + "error": "no_security_api" + } + + # Собираем базовые сведения из системной информации + return { + "success": True, + "status": "basic", + "last_check": None, + "is_secure": True, # Предполагаем, что система в целом безопасна + "firewall_enabled": None, + "auto_update": None, + "version_latest": sys_result.get("version_string", "") + } + + # Собираем информацию из доступных результатов + firewall_enabled = firewall_result.get("enabled", False) if firewall_result else None + auto_update = update_result.get("auto_update", False) if update_result else None + + # Определяем, насколько система безопасна + is_secure = True # По умолчанию предполагаем, что система безопасна + if firewall_enabled is not None and not firewall_enabled: + is_secure = False + + return { + "success": True, + "status": "partial", + "last_check": None, + "is_secure": is_secure, + "firewall_enabled": firewall_enabled, + "auto_update": auto_update + } + + # Если основное API отвечает, возвращаем его данные + return { + "success": True, + "status": result.get("status", "unknown"), + "last_check": result.get("last_check", None), + "is_secure": result.get("is_secure", False), + "details": result.get("details", {}) + } + + except Exception as e: + logger.error(f"Error in get_security_status: {str(e)}") + return {"success": False, "error": str(e)} + + def get_processes(self, limit: int = 20) -> List[Dict[str, Any]]: + """Получение списка активных процессов""" + logger.info(f"Getting list of active processes (limit={limit})") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for processes request") + return [] + + try: + # Получаем список процессов через API + result = self._make_api_request("SYNO.Core.System.Process", "list", version=1, + params={"sort_by": "cpu", "order": "DESC", "limit": limit}) + + if not result: + logger.warning("Failed to get process list") + return [] + + return result.get("processes", []) + + except Exception as e: + logger.error(f"Error getting process list: {str(e)}") + return [] + + def get_network_status(self) -> Dict[str, Any]: + """Получение информации о сетевых подключениях""" + logger.info("Getting network status information") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for network status request") + return {} + + try: + # Получаем информацию о сетевых интерфейсах + interface_result = self._make_api_request("SYNO.Core.Network.Interface", "list", version=1) + + # Получаем статистику использования сети + utilization_result = self._make_api_request("SYNO.Core.System.Utilization", "get", version=1) + + interfaces = [] + if interface_result: + interfaces = interface_result.get("interfaces", []) + + network_stats = {} + if utilization_result and "network" in utilization_result: + network_stats = utilization_result.get("network", {}) + + # Объединяем данные + for interface in interfaces: + iface_id = interface.get("id", "") + if iface_id in network_stats: + interface["rx"] = network_stats[iface_id].get("rx", 0) + interface["tx"] = network_stats[iface_id].get("tx", 0) + + return { + "interfaces": interfaces, + "statistics": network_stats + } + + except Exception as e: + logger.error(f"Error getting network status: {str(e)}") + return {} + + def get_system_logs(self, limit: int = 20) -> List[Dict[str, Any]]: + """Получение журналов системы""" + logger.info(f"Getting system logs (limit={limit})") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for system logs request") + return [] + + try: + # Получаем журналы через API + result = self._make_api_request("SYNO.Core.SyslogClient.Log", "list", version=1, + params={"start": 0, "limit": limit, "sort": "time", "dir": "DESC"}) + + if not result: + # Пробуем альтернативный API + logger.info("Trying alternative API for system logs") + alt_result = self._make_api_request("SYNO.Core.Log", "list", version=1, + params={"start": 0, "limit": limit, "sort": "time", "dir": "DESC"}) + + if alt_result: + return alt_result.get("logs", []) + return [] + + return result.get("logs", []) + + except Exception as e: + logger.error(f"Error getting system logs: {str(e)}") + return [] + + def get_power_schedule(self) -> Dict[str, Any]: + """Получение расписания включения/выключения""" + logger.info("Getting power schedule") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for power schedule request") + return {} + + try: + # Получаем расписание через API + result = self._make_api_request("SYNO.Core.Hardware.PowerSchedule", "get", version=1) + + if not result: + return {} + + return result + + except Exception as e: + logger.error(f"Error getting power schedule: {str(e)}") + return {} + + def set_power_schedule(self, schedule_type: str, days: List[int], time: str, enabled: bool = True) -> bool: + """Настройка расписания включения/выключения + + Args: + schedule_type: Тип расписания ('boot' или 'shutdown') + days: Список дней недели (0-6, где 0 - понедельник) + time: Время в формате 'HH:MM' + enabled: Включить или выключить расписание + + Returns: + True если успешно, False в противном случае + """ + logger.info(f"Setting power schedule for {schedule_type} at {time} on days {days}") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for setting power schedule") + return False + + try: + # Получаем текущее расписание + current_schedule = self._make_api_request("SYNO.Core.Hardware.PowerSchedule", "get", version=1) + + if not current_schedule: + logger.error("Failed to get current power schedule") + return False + + # Подготавливаем новое расписание + params = { + "enabled": enabled, + "type": schedule_type, + "day": days, + "time": time + } + + # Устанавливаем новое расписание + result = self._make_api_request("SYNO.Core.Hardware.PowerSchedule", "set", version=1, params=params) + + if not result: + logger.error("Failed to set power schedule") + return False + + logger.info(f"Power schedule for {schedule_type} set successfully") + return True + + except Exception as e: + logger.error(f"Error setting power schedule: {str(e)}") + return False + + def get_temperature_status(self) -> Dict[str, Any]: + """Получение информации о температуре системы и дисков""" + logger.info("Getting temperature status") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for temperature status request") + return {} + + try: + # Получаем информацию о системе для общей температуры + system_info = self._make_api_request("SYNO.DSM.Info", "getinfo", version=2) + + # Получаем информацию о дисках для их температуры + storage_info = self._make_api_request("SYNO.Storage.CGI.Storage", "load_info", version=1) + + system_temp = None + disk_temps = [] + + if system_info: + system_temp = system_info.get("temperature") + + if storage_info: + disks = storage_info.get("disks", []) + for disk in disks: + name = disk.get("name", "unknown") + model = disk.get("model", "unknown") + temp = disk.get("temp", None) + if temp is not None: + disk_temps.append({ + "name": name, + "model": model, + "temperature": temp + }) + + return { + "system_temperature": system_temp, + "disk_temperatures": disk_temps, + "warning": system_info.get("temperature_warn", False) if system_info else False + } + + except Exception as e: + logger.error(f"Error getting temperature status: {str(e)}") + return {} + + def browse_files(self, folder_path: str = "", limit: int = 100) -> Dict[str, Any]: + """Просмотр файлов в указанной директории + + Args: + folder_path: Путь к папке (пустая строка для корневых общих папок) + limit: Максимальное количество элементов для возврата + + Returns: + Словарь с информацией о файлах и папках + """ + logger.info(f"Browsing files in {folder_path or 'root'}") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for file browsing") + return {"success": False, "error": "authentication_failed"} + + try: + if not folder_path: + # Если путь не указан, получаем список общих папок + result = self._make_api_request("SYNO.FileStation.List", "list_share", version=2) + + if not result: + logger.error("Failed to list shared folders") + return {"success": False, "error": "api_error"} + + return { + "success": True, + "items": result.get("shares", []), + "path": "", + "is_root": True + } + else: + # Получаем список файлов в указанной директории + params = { + "folder_path": folder_path, + "limit": limit, + "offset": 0, + "sort_by": "name", + "sort_direction": "ASC" + } + + result = self._make_api_request("SYNO.FileStation.List", "list", version=2, params=params) + + if not result: + logger.error(f"Failed to list files in {folder_path}") + return {"success": False, "error": "api_error"} + + return { + "success": True, + "items": result.get("files", []), + "path": folder_path, + "is_root": False, + "total": result.get("total", 0) + } + + except Exception as e: + logger.error(f"Error browsing files: {str(e)}") + return {"success": False, "error": str(e)} + + def manage_service(self, service_name: str, action: str = "status") -> Dict[str, Any]: + """Управление системным сервисом + + Args: + service_name: Имя сервиса + action: Действие (status/start/stop/restart) + + Returns: + Словарь с результатом операции + """ + logger.info(f"Managing service {service_name}, action: {action}") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for service management") + return {"success": False, "error": "authentication_failed"} + + try: + # Проверяем доступное API для управления сервисами + if action == "status": + result = self._make_api_request("SYNO.Core.Service", "get", version=1, + params={"service": service_name}) + else: + result = self._make_api_request("SYNO.Core.Service", action, version=1, + params={"service": service_name}) + + if not result: + logger.error(f"Failed to {action} service {service_name}") + return {"success": False, "error": "api_error"} + + return { + "success": True, + "service": service_name, + "action": action, + "result": result, + "status": result.get("status") if action == "status" else "completed" + } + + except Exception as e: + logger.error(f"Error managing service {service_name}: {str(e)}") + return {"success": False, "error": str(e)} + + def search_files(self, pattern: str, folder_path: str = "", limit: int = 100) -> Dict[str, Any]: + """Поиск файлов по шаблону + + Args: + pattern: Шаблон для поиска + folder_path: Путь к папке для поиска (пустая строка для всех общих папок) + limit: Максимальное количество результатов + + Returns: + Словарь с найденными файлами + """ + logger.info(f"Searching for {pattern} in {folder_path or 'all shared folders'}") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for file search") + return {"success": False, "error": "authentication_failed"} + + try: + if not folder_path: + # Получаем список всех общих папок для поиска + shares_result = self._make_api_request("SYNO.FileStation.List", "list_share", version=2) + + if not shares_result: + logger.error("Failed to list shared folders for search") + return {"success": False, "error": "api_error"} + + # Формируем список путей для поиска + folder_paths = [share.get("path") for share in shares_result.get("shares", [])] + else: + folder_paths = [folder_path] + + # Запускаем поиск + params = { + "folder_path": folder_paths, + "pattern": pattern, + "limit": limit, + "offset": 0 + } + + result = self._make_api_request("SYNO.FileStation.Search", "start", version=2, params=params) + + if not result: + logger.error(f"Failed to start search for {pattern}") + return {"success": False, "error": "api_error"} + + # Получаем taskid для проверки результатов + taskid = result.get("taskid") + if not taskid: + logger.error("No taskid received for search") + return {"success": False, "error": "no_task_id"} + + # Ожидаем завершения поиска + search_result = {"finished": False, "progress": 0} + for _ in range(10): # Максимум 10 попыток + search_status = self._make_api_request("SYNO.FileStation.Search", "status", version=2, + params={"taskid": taskid}) + + if not search_status: + break + + search_result["progress"] = search_status.get("progress", 0) + + if search_status.get("finished", False): + search_result["finished"] = True + break + + time.sleep(0.5) # Пауза между запросами + + # Получаем результаты поиска + if search_result["finished"]: + list_result = self._make_api_request("SYNO.FileStation.Search", "list", version=2, + params={"taskid": taskid, "limit": limit}) + + if list_result: + files = list_result.get("files", []) + return { + "success": True, + "pattern": pattern, + "results": files, + "total": list_result.get("total", len(files)) + } + + # Если не удалось получить результаты, останавливаем поиск + self._make_api_request("SYNO.FileStation.Search", "stop", version=2, + params={"taskid": taskid}) + + return { + "success": False, + "error": "search_timeout", + "progress": search_result["progress"] + } + + except Exception as e: + logger.error(f"Error searching files: {str(e)}") + return {"success": False, "error": str(e)} + + def get_backup_status(self) -> Dict[str, Any]: + """Получение информации о резервном копировании""" + logger.info("Getting backup status information") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for backup status request") + return {"success": False, "error": "authentication_failed"} + + try: + # Пробуем получить информацию о Hyper Backup + hyper_result = self._make_api_request("SYNO.Backup.Repository", "list", version=1) + + # Пробуем получить информацию о задачах Time Backup + time_result = self._make_api_request("SYNO.BackupService.Task", "list", version=1) + + # Проверяем статус резервного копирования USB + usb_result = self._make_api_request("SYNO.USBCopy", "status", version=1) + + backups = { + "hyper_backup": hyper_result.get("backups", []) if hyper_result else [], + "time_backup": time_result.get("tasks", []) if time_result else [], + "usb_copy": {"enabled": usb_result.get("enable", False) if usb_result else False} + } + + return { + "success": True, + "backups": backups, + "available_apis": { + "hyper_backup": hyper_result is not None, + "time_backup": time_result is not None, + "usb_copy": usb_result is not None + } + } + + except Exception as e: + logger.error(f"Error getting backup status: {str(e)}") + return {"success": False, "error": str(e)} + + def check_for_updates(self) -> Dict[str, Any]: + """Проверка наличия обновлений системы""" + logger.info("Checking for system updates") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for update check") + return {"success": False, "error": "authentication_failed"} + + try: + # Получаем текущую информацию о системе + system_info = self._make_api_request("SYNO.DSM.Info", "getinfo", version=2) + + if not system_info: + logger.error("Failed to get system info for update check") + return {"success": False, "error": "api_error"} + + # Проверяем наличие обновлений + update_result = self._make_api_request("SYNO.Core.Upgrade.Server", "check", version=1) + + # Получаем настройки автоматического обновления + settings_result = self._make_api_request("SYNO.Core.Upgrade.Server.Setting", "get", version=1) + + # Получаем информацию о доступных обновлениях + update_info = self._make_api_request("SYNO.Core.Upgrade.Server.Download", "list", version=1) + + current_version = system_info.get("version_string", "unknown") + auto_update_enabled = settings_result.get("auto_update", False) if settings_result else False + + updates = [] + if update_info and "updates" in update_info: + updates = update_info.get("updates", []) + + update_available = len(updates) > 0 + + return { + "success": True, + "current_version": current_version, + "update_available": update_available, + "auto_update_enabled": auto_update_enabled, + "updates": updates + } + + except Exception as e: + logger.error(f"Error checking for updates: {str(e)}") + return {"success": False, "error": str(e)} + + def get_quota_info(self) -> Dict[str, Any]: + """Получение информации о квотах пользователей""" + logger.info("Getting user quota information") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for quota info request") + return {"success": False, "error": "authentication_failed"} + + try: + # Получаем список пользователей + users_result = self._make_api_request("SYNO.Core.User", "list", version=1) + + if not users_result: + logger.error("Failed to get user list for quota info") + return {"success": False, "error": "api_error"} + + users = users_result.get("users", []) + user_quotas = [] + + # Получаем квоты для каждого пользователя + for user in users: + user_name = user.get("name") + if not user_name: + continue + + quota_result = self._make_api_request("SYNO.Core.Quota", "get", version=1, + params={"user_name": user_name}) + + if quota_result and "quotas" in quota_result: + user_quotas.append({ + "user": user_name, + "quotas": quota_result.get("quotas", []) + }) + + return { + "success": True, + "user_quotas": user_quotas + } + + except Exception as e: + logger.error(f"Error getting quota info: {str(e)}") + return {"success": False, "error": str(e)} diff --git a/.history/src/api/synology_20250830095651.py b/.history/src/api/synology_20250830095651.py new file mode 100644 index 0000000..672c37a --- /dev/null +++ b/.history/src/api/synology_20250830095651.py @@ -0,0 +1,1918 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Модуль для взаимодействия с API Synology NAS +""" + +import requests +from requests.adapters import HTTPAdapter +import json +import logging +import time +import urllib3 +from urllib3.util import Retry +from typing import Dict, Any, Optional, List +import socket +import struct +from time import sleep + +from src.config.config import ( + SYNOLOGY_HOST, + SYNOLOGY_PORT, + SYNOLOGY_USERNAME, + SYNOLOGY_PASSWORD, + SYNOLOGY_SECURE, + SYNOLOGY_TIMEOUT, + SYNOLOGY_MAC, + WOL_PORT, + SYNOLOGY_API_VERSION, + SYNOLOGY_POWER_API, + SYNOLOGY_INFO_API +) +from src.api.api_discovery import discover_available_apis, find_compatible_api + +# Отключение предупреждений о небезопасных SSL-соединениях +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + +logger = logging.getLogger(__name__) + +class SynologyAPI: + """Класс для взаимодействия с API Synology NAS""" + + def __init__(self): + """Инициализация класса SynologyAPI""" + logger.info("Creating API with auto-retry and connection pool") + logger.debug(f"Connection details: Host={SYNOLOGY_HOST}, Port={SYNOLOGY_PORT}, Secure={SYNOLOGY_SECURE}") + + self.protocol = "https" if SYNOLOGY_SECURE else "http" + self.base_url = f"{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi" + self.sid = None + self.session = requests.Session() + + # Настройка SSL + if self.protocol == "https": + logger.debug("SSL enabled, disabling certificate verification for internal network") + self.session.verify = False # Отключаем проверку SSL для внутренней сети + + # Добавляем пользовательские заголовки для улучшения совместимости с API + custom_headers = { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36', + 'Accept': 'application/json, text/javascript, */*; q=0.01', + 'Accept-Language': 'ru-RU,ru;q=0.9,en-US;q=0.8,en;q=0.7', + 'X-Requested-With': 'XMLHttpRequest', + 'Connection': 'keep-alive', + 'Referer': f'{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/' + } + self.session.headers.update(custom_headers) + logger.debug("Added browser-like headers for API compatibility") + + # Добавляем повторные попытки для HTTP-запросов + retry_strategy = Retry( + total=5, # Увеличиваем количество попыток + status_forcelist=[429, 500, 502, 503, 504, 404], + allowed_methods=["GET", "POST"], + backoff_factor=1.5, # Увеличиваем задержку между попытками + respect_retry_after_header=True + ) + adapter = HTTPAdapter( + max_retries=retry_strategy, + pool_connections=3, + pool_maxsize=10 + ) + self.session.mount("http://", adapter) + self.session.mount("https://", adapter) + + # Таймауты будут указаны в запросах + self.default_timeout = (SYNOLOGY_TIMEOUT, SYNOLOGY_TIMEOUT*2) + logger.debug(f"Setting default request timeout: {self.default_timeout}") + + # Кэш для хранения результатов запросов + self._cache = {} + self._cache_ttl = {} + self._last_online_check = 0 + self._last_online_status = False + self._online_check_interval = 30 # Интервал для кэширования проверок онлайн-статуса + + # Время последней успешной аутентификации и срок действия сессии + self._last_auth_time = 0 + self._auth_expiry = 3600 # По умолчанию 1 час + + # Информация о доступных API + self._available_apis = {} + self._api_info_ttl = 0 + + # Инициализируем API version resolver для автоматического определения совместимых API + self.api_resolver = None # Будет создан при необходимости + + def login(self) -> bool: + """Авторизация в API Synology NAS""" + # Сбрасываем SID для новой сессии + self.sid = None + + logger.info("Attempting to authenticate with Synology NAS...") + logger.debug(f"Base URL: {self.base_url}") + + # Проверяем доступность NAS перед запросом с помощью прямой TCP-проверки + # Избегаем вызова is_online(), чтобы не создавать рекурсию + online_status = self._check_tcp_connection() + if not online_status: + logger.error("Cannot login: Synology NAS is not reachable") + return False + + # Пробуем различные версии API для аутентификации + # Начинаем с версии 3, которая показала лучшую совместимость в тестах + auth_versions_to_try = [3, 2, 1, 6] # Пробуем сначала более стабильные версии + + for auth_version in auth_versions_to_try: + try: + # Определяем путь к API аутентификации + auth_path = "entry.cgi" # Используем entry.cgi вместо auth.cgi по умолчанию + + # Проверка информации API для определения доступных версий API + api_info_url = f"{self.base_url}/entry.cgi" + api_info_params = { + "api": "SYNO.API.Info", + "version": "1", + "method": "query", + "query": "SYNO.API.Auth" + } + + logger.debug(f"Querying API info for auth version {auth_version}") + try: + api_info_response = self.session.get( + api_info_url, + params=api_info_params, + timeout=self.default_timeout, + verify=False + ) + + if api_info_response.status_code == 200: + api_info_data = api_info_response.json() + if api_info_data.get("success"): + auth_info = api_info_data.get("data", {}).get("SYNO.API.Auth", {}) + max_version = auth_info.get("maxVersion", 6) + min_version = auth_info.get("minVersion", 1) + auth_path = auth_info.get("path", "entry.cgi") + logger.debug(f"Auth API versions: min={min_version}, max={max_version}, path={auth_path}") + + # Проверяем поддержку текущей версии + if auth_version < min_version or auth_version > max_version: + logger.warning(f"Auth API version {auth_version} not supported (min={min_version}, max={max_version}), trying next version") + continue + else: + logger.warning("Failed to query API info, using default auth path") + else: + logger.warning(f"API info request failed with status {api_info_response.status_code}, using default auth path") + except Exception as e: + logger.warning(f"Error querying API info: {str(e)}, using default auth path") + + # Основной запрос авторизации + url = f"{self.base_url}/{auth_path}" + params = { + "api": "SYNO.API.Auth", + "version": str(auth_version), + "method": "login", + "account": SYNOLOGY_USERNAME, + "passwd": SYNOLOGY_PASSWORD, + "session": "SynologyPowerControlBot", + "format": "cookie" + } + + # Для версии 6+ используем немного другой формат + if auth_version >= 6: + params["enable_syno_token"] = "yes" + + logger.debug(f"Sending auth request to {url} with API version {auth_version}") + start_time = time.time() + response = self.session.get( + url, + params=params, + timeout=self.default_timeout, + verify=False + ) + elapsed_time = time.time() - start_time + logger.debug(f"Auth request completed in {elapsed_time:.2f}s with status code {response.status_code}") + + # Проверка статуса HTTP + if response.status_code != 200: + logger.error(f"HTTP error: {response.status_code}") + logger.debug(f"Response content: {response.text[:200]}") + continue # Пробуем следующую версию + + try: + data = response.json() + except json.JSONDecodeError: + logger.error("Failed to decode JSON response") + logger.debug(f"Response content: {response.text[:200]}") + continue # Пробуем следующую версию + + if data.get("success"): + self.sid = data.get("data", {}).get("sid") + self._last_auth_time = time.time() + logger.info(f"Successfully logged in to Synology NAS using API version {auth_version}") + logger.debug(f"Session ID obtained: {self.sid[:5] if self.sid else 'None'}...") + + # Получаем и сохраняем токен SYNO, если он есть + syno_token = data.get("data", {}).get("synotoken") + if syno_token: + self.session.headers.update({'X-SYNO-TOKEN': syno_token}) + logger.debug("Added X-SYNO-TOKEN header for improved API compatibility") + + # Также добавляем SID в cookies для улучшения совместимости + self.session.cookies.update({ + 'id': self.sid, + 'sid': self.sid + }) + logger.debug("Added SID to session cookies for improved compatibility") + + # Проверка валидности полученной сессии с помощью простого запроса + # Будем использовать SYNO.API.Info без проверки сложных методов + + # Даем системе немного времени для инициализации сессии + time.sleep(0.5) + + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to log in with version {auth_version}: Error code {error_code}") + + # Если ошибка связана с версией API, пробуем следующую версию + if error_code in [104, 105]: + logger.warning(f"Auth version {auth_version} not supported, trying next version") + continue + + # Дополнительная диагностика + if error_code == 400: + logger.error("Authentication error: Invalid credentials") + elif error_code == 401: + logger.error("Authentication error: Account disabled") + elif error_code == 402: + logger.error("Authentication error: Permission denied") + elif error_code == 403: + logger.error("Authentication error: 2-factor authentication required") + elif error_code == 404: + logger.error("Authentication error: Failed to authenticate with 2-factor authentication") + + # Если ошибка связана с неверными учетными данными, нет смысла пробовать другие версии API + if error_code in [400, 401, 402, 403, 404]: + return False + + except requests.exceptions.Timeout: + logger.error(f"Connection timeout during auth with version {auth_version}") + continue # Пробуем следующую версию + except requests.exceptions.ConnectionError as e: + logger.error(f"Connection error during auth with version {auth_version}: {str(e)}") + continue # Пробуем следующую версию + except requests.RequestException as e: + logger.error(f"Request error during auth with version {auth_version}: {str(e)}") + continue # Пробуем следующую версию + except Exception as e: + logger.error(f"Unexpected error during login with version {auth_version}: {str(e)}", exc_info=True) + continue # Пробуем следующую версию + + # Если все версии не сработали + logger.error("Failed to authenticate with any API version") + return False + + def _validate_session(self) -> bool: + """Проверяет валидность сессии после авторизации""" + if not self.sid: + return False + + # Попробуем сделать простой запрос для проверки сессии + test_apis = [ + {"api": "SYNO.Core.System", "method": "info", "version": 1}, + {"api": "SYNO.DSM.Info", "method": "getinfo", "version": 1} + ] + + for test_api in test_apis: + try: + url = f"{self.base_url}/entry.cgi" + params = { + "api": test_api["api"], + "version": str(test_api["version"]), + "method": test_api["method"], + "_sid": self.sid + } + + response = self.session.get(url, params=params, timeout=self.default_timeout, verify=False) + + if response.status_code == 200: + data = response.json() + if data.get("success"): + logger.debug(f"Session validation successful using {test_api['api']}") + return True + else: + error_code = data.get("error", {}).get("code", -1) + if error_code != 119: # Не сессия истекла + logger.debug(f"Session validation with {test_api['api']} returned error code {error_code}") + return True # Считаем сессию валидной, если ошибка не связана с истечением сессии + except Exception as e: + logger.warning(f"Error during session validation with {test_api['api']}: {str(e)}") + + logger.warning("Session validation failed with all test APIs") + return False + + def logout(self) -> bool: + """Выход из API Synology NAS""" + if not self.sid: + return True + + try: + url = f"{self.base_url}/auth.cgi" + params = { + "api": "SYNO.API.Auth", + "version": "1", + "method": "logout", + "session": "SynologyPowerControlBot", + "_sid": self.sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + self.sid = None + logger.info("Successfully logged out from Synology NAS") + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to log out from Synology NAS: Error code {error_code}") + return False + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + return False + + def _make_api_request(self, api_name: str, method: str, version: int = 1, + params: Optional[Dict[str, Any]] = None, retry_auth: bool = True, retry_count: int = 0) -> Optional[Dict[str, Any]]: + """Обобщенный метод для выполнения API запросов с обработкой ошибок""" + # Ограничение на количество повторных попыток + if retry_count >= 3: + logger.error(f"Too many retries for {api_name}.{method}, giving up") + return None + + # Проверка наличия авторизации + if not self.sid and not self.login(): + logger.error(f"Not authenticated for API request: {api_name}.{method}") + return None + + # Проверка информации API для определения пути и поддерживаемой версии + api_info_url = f"{self.base_url}/entry.cgi" + api_info_params = { + "api": "SYNO.API.Info", + "version": "1", + "method": "query", + "query": api_name + } + + api_path = "entry.cgi" + try: + logger.debug(f"Querying API info for {api_name}") + api_info_response = self.session.get( + api_info_url, + params=api_info_params, + timeout=self.default_timeout, + verify=False + ) + + if api_info_response.status_code == 200: + api_info_data = api_info_response.json() + if api_info_data.get("success"): + api_info = api_info_data.get("data", {}).get(api_name, {}) + if api_info: + max_version = api_info.get("maxVersion", version) + min_version = api_info.get("minVersion", version) + api_path = api_info.get("path", "entry.cgi") + + # Проверка, поддерживается ли запрошенная версия + if version < min_version: + logger.warning(f"API version {version} for {api_name} is below minimum {min_version}, using {min_version}") + version = min_version + elif version > max_version: + logger.warning(f"API version {version} for {api_name} exceeds maximum {max_version}, using {max_version}") + version = max_version + + logger.debug(f"Using API path: {api_path}, version: {version}") + else: + logger.warning(f"API {api_name} not found in API info, using defaults") + except Exception as e: + logger.warning(f"Error querying API info for {api_name}: {str(e)}, using defaults") + + # Подготовка базовых параметров запроса + base_params = { + "api": api_name, + "version": str(version), + "method": method, + "_sid": self.sid # Используем _sid вместо sid для лучшей совместимости + } + + # Добавление дополнительных параметров, если они заданы + if params: + base_params.update(params) + + url = f"{self.base_url}/{api_path}" + logger.debug(f"API request: {api_name}.{method} v{version} to {url}") + logger.debug(f"Full request params: {base_params}") + + try: + start_time = time.time() + response = self.session.get( + url, + params=base_params, + timeout=self.default_timeout, + verify=False + ) + elapsed_time = time.time() - start_time + logger.debug(f"API request completed in {elapsed_time:.2f}s with status {response.status_code}") + + # Проверка статуса HTTP + if response.status_code != 200: + logger.error(f"HTTP error: {response.status_code} for {api_name}.{method}") + + # Повторная попытка при ошибках соединения + if response.status_code in [500, 502, 503, 504]: + logger.info(f"Server error, retrying request for {api_name}.{method}") + sleep(2) + return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1) + + return None + + try: + data = response.json() + except json.JSONDecodeError: + logger.error(f"Failed to decode JSON response for {api_name}.{method}") + logger.debug(f"Response content: {response.text[:200]}") + + # Повторная попытка при ошибках декодирования + logger.info(f"JSON decode error, retrying request for {api_name}.{method}") + sleep(2) + return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1) + + if data.get("success"): + logger.info(f"API request successful for {api_name}.{method}") + return data.get("data", {}) + else: + error_code = data.get("error", {}).get("code", -1) + error_desc = self._get_error_description(error_code) + logger.error(f"API error for {api_name}.{method}: {error_code} - {error_desc}") + + # Ошибки доступа или прав часто встречаются, но они не критичные + # Например, ошибка 102 означает, что нет прав, но NAS доступен + if error_code in [102, 103, 104, 105]: + logger.warning(f"Permission-related error for {api_name}.{method}: {error_code}") + # Возвращаем пустой словарь вместо None, + # чтобы вызывающий код мог понять, что запрос выполнен + return {} + + # Если ошибка связана с авторизацией и нам разрешено повторить попытку + if error_code in [106, 107, 119] and retry_auth: + logger.info(f"Session error (code {error_code}), creating fresh session...") + self.sid = None # Сбрасываем SID + + # Для ошибки 119 (Session timeout) дадим системе немного времени + if error_code == 119: + logger.info("Session timeout detected, waiting before retry...") + sleep(3) + + if self.login(): + logger.info("Re-authenticated with fresh session, retrying API request...") + # Рекурсивный вызов, но со счетчиком повторов + return self._make_api_request(api_name, method, version, params, False, retry_count + 1) + + # Для некоторых ошибок можно автоматически повторить запрос + if error_code in [408, 429, 500, 502, 503, 504]: + logger.info(f"Temporary error {error_code}, retrying request for {api_name}.{method}") + sleep(2) + return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1) + + return None + + except requests.exceptions.Timeout: + logger.error(f"Request timeout for {api_name}.{method}") + + # Повторная попытка при таймауте + if retry_count < 2: + logger.info(f"Timeout, retrying request for {api_name}.{method}") + sleep(2) + return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1) + + return None + except requests.exceptions.ConnectionError as e: + logger.error(f"Connection error for {api_name}.{method}: {str(e)}") + + # Повторная попытка при ошибке соединения + if retry_count < 2: + logger.info(f"Connection error, retrying request for {api_name}.{method}") + sleep(2) + return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1) + + return None + except Exception as e: + logger.error(f"Unexpected error during {api_name}.{method}: {str(e)}") + return None + + def get_system_status(self) -> Dict[str, Any]: + """Получение статуса системы""" + # Проверяем доступность системы + if not self.is_online(): + logger.info("Device is offline, skipping API request") + return {"status": "offline"} + + # Проверяем, есть ли кэшированный результат + cache_key = "system_status" + current_time = time.time() + if cache_key in self._cache and current_time - self._cache_ttl.get(cache_key, 0) < 60: + logger.debug("Using cached system status") + return self._cache[cache_key] + + # Используем рекомендованный API для получения информации о системе + logger.info(f"Getting system info using {SYNOLOGY_INFO_API} v{SYNOLOGY_API_VERSION}") + + # Для SYNO.DSM.Info метод getinfo, для других обычно используется get или info + if SYNOLOGY_INFO_API == "SYNO.DSM.Info": + method = "getinfo" + else: + method = "get" + + result = self._make_api_request(SYNOLOGY_INFO_API, method, version=SYNOLOGY_API_VERSION) + + if result: + logger.info(f"Successfully retrieved system info using {SYNOLOGY_INFO_API}") + + # Формируем расширенный ответ с дополнительной информацией + system_info = { + "status": "online", + "hostname": result.get("hostname", "unknown"), + "model": result.get("model", "unknown"), + "version": result.get("version", "unknown"), + "uptime": result.get("uptime", 0), + "time": current_time, + "is_online": True, + "api_used": SYNOLOGY_INFO_API + } + + # Сохраняем в кэше + self._cache[cache_key] = system_info + self._cache_ttl[cache_key] = current_time + + return system_info + + # Если основной API не сработал, пробуем резервные варианты + logger.warning(f"Failed to retrieve system info with {SYNOLOGY_INFO_API}, trying fallback APIs") + + # Пробуем резервные API + apis_to_try = [ + {"name": "SYNO.DSM.Info", "method": "getinfo", "version": 2}, + {"name": "SYNO.Core.System", "method": "info", "version": 1}, + {"name": "SYNO.Core.System.Status", "method": "get", "version": 1}, + {"name": "SYNO.Core.System.Info", "method": "get", "version": 1}, + ] + + for api in apis_to_try: + if api["name"] == SYNOLOGY_INFO_API: + continue # Пропускаем уже проверенный API + + logger.info(f"Trying to get system info using {api['name']}.{api['method']} v{api['version']}") + result = self._make_api_request(api["name"], api["method"], version=api["version"]) + + if result: + logger.info(f"Successfully retrieved system info using {api['name']}") + + # Формируем расширенный ответ с дополнительной информацией + system_info = { + "status": "online", + "hostname": result.get("hostname", "unknown"), + "model": result.get("model", "unknown"), + "version": result.get("version", "unknown"), + "uptime": result.get("uptime", 0), + "time": current_time, + "is_online": True, + "api_used": api["name"] + } + + # Сохраняем в кэше + self._cache[cache_key] = system_info + self._cache_ttl[cache_key] = current_time + + return system_info + + # Если все запросы не удались, но система онлайн, возвращаем базовую информацию + logger.warning("Failed to retrieve system info with all API methods") + return { + "status": "error", + "error": "Failed to fetch system information", + "is_online": True, + "time": current_time + } + + def shutdown_system(self) -> bool: + """Выключение системы""" + # Проверяем, включено ли устройство перед попыткой его выключить + if not self.is_online(force_check=True): + logger.info("Synology NAS is already offline, no need to shut down") + return True + + logger.info("Attempting to shutdown Synology NAS...") + + # Попробуем сначала использовать предпочтительный API для управления питанием + logger.info(f"Trying shutdown with {SYNOLOGY_POWER_API} v{SYNOLOGY_API_VERSION}") + + # Для SYNO.Core.Hardware.PowerRecovery используется метод setPowerOnState + # Для других API обычно используется метод shutdown или reboot + if SYNOLOGY_POWER_API == "SYNO.Core.Hardware.PowerRecovery": + # Для этого API нужны специальные параметры + params = {"state": "powerbtn"} # powerbtn имитирует нажатие кнопки питания + result = self._make_api_request(SYNOLOGY_POWER_API, "setPowerOnState", version=SYNOLOGY_API_VERSION, params=params) + else: + # Пробуем стандартный метод + result = self._make_api_request(SYNOLOGY_POWER_API, "shutdown", version=SYNOLOGY_API_VERSION) + + if result is not None: + logger.info(f"Successfully initiated system shutdown using {SYNOLOGY_POWER_API}") + + # Даем системе время начать процесс выключения + logger.info("Waiting for shutdown to initialize...") + sleep(5) + + # Проверяем статус + if not self.is_online(force_check=True): + logger.info("System is now offline. Shutdown confirmed successful.") + return True + else: + logger.info("System still appears to be online, but shutdown may be in progress.") + return True + else: + logger.warning(f"Failed to shutdown using {SYNOLOGY_POWER_API}, trying alternative methods") + + # Если не сработал основной метод, пробуем резервные варианты + # Проверка всех доступных методов API для выключения + apis_to_try = [ + {"name": "SYNO.Core.System", "method": "shutdown", "version": 3}, + {"name": "SYNO.Core.System.Power", "method": "shutdown", "version": 1}, + {"name": "SYNO.DSM.Power", "method": "shutdown", "version": 1}, + {"name": "SYNO.System.Power", "method": "shutdown", "version": 1}, + {"name": "SYNO.DSM.System", "method": "shutdown", "version": 1} + ] + + # Проверяем доступные API + try: + api_info_url = f"{self.base_url}/entry.cgi" + api_info_params = { + "api": "SYNO.API.Info", + "version": "1", + "method": "query", + "query": "SYNO.Core.System.Power,SYNO.DSM.Power,SYNO.Core.System,SYNO.System.Power,SYNO.DSM.System" + } + + logger.debug("Checking available shutdown APIs") + api_info_response = self.session.get( + api_info_url, + params=api_info_params, + timeout=self.default_timeout, + verify=False + ) + + if api_info_response.status_code == 200: + api_info_data = api_info_response.json() + if api_info_data.get("success"): + available_apis = api_info_data.get("data", {}) + logger.debug(f"Available APIs: {list(available_apis.keys())}") + + # Фильтруем только доступные API + filtered_apis = [] + for api in apis_to_try: + if api["name"] in available_apis: + api_info = available_apis[api["name"]] + # Проверка версии API + if api_info.get("minVersion", 1) <= api["version"] <= api_info.get("maxVersion", 1): + filtered_apis.append(api) + logger.debug(f"Adding {api['name']} to available shutdown APIs") + else: + logger.debug(f"API {api['name']} version {api['version']} not supported. Supported: {api_info.get('minVersion')}-{api_info.get('maxVersion')}") + + if filtered_apis: + apis_to_try = filtered_apis + else: + logger.warning("No compatible APIs found, trying all methods as fallback") + else: + logger.warning("Failed to query API info, using default methods") + else: + logger.warning(f"API info request failed with status {api_info_response.status_code}, using default methods") + except Exception as e: + logger.warning(f"Error querying API info: {str(e)}, using default methods") + + # Пробуем все доступные методы по порядку + for api in apis_to_try: + logger.info(f"Trying shutdown with {api['name']}.{api['method']} v{api['version']}") + result = self._make_api_request(api["name"], api["method"], version=api["version"]) + + if result is not None: + logger.info(f"Successfully initiated system shutdown using {api['name']}") + + # Даем системе время начать процесс выключения + logger.info("Waiting for shutdown to initialize...") + sleep(5) + + # Проверяем статус + if not self.is_online(force_check=True): + logger.info("System is now offline. Shutdown confirmed successful.") + return True + else: + logger.info("System still appears to be online, but shutdown may be in progress.") + return True + else: + logger.warning(f"Failed to shutdown using {api['name']}.{api['method']} v{api['version']}") + + # Если ни один метод не сработал, но система стала недоступна + if not self.is_online(force_check=True): + logger.info("System appears to be shutting down despite API errors") + return True + + logger.error("Failed to shutdown system after trying multiple APIs") + return False + + def reboot_system(self) -> bool: + """Перезагрузка системы""" + # Проверяем, включена ли система + if not self.is_online(force_check=True): + logger.error("Cannot reboot: System is offline") + return False + + logger.info("Attempting to reboot Synology NAS...") + + # Попробуем сначала использовать предпочтительный API для управления питанием + logger.info(f"Trying reboot with {SYNOLOGY_POWER_API} v{SYNOLOGY_API_VERSION}") + + # Пробуем использовать наиболее совместимые API для перезагрузки + # SYNO.Core.System обычно доступен на большинстве систем + logger.info("Trying reboot with SYNO.Core.System API") + result = self._make_api_request("SYNO.Core.System", "reboot", version=1) + + if result is None: + # Если не сработало, пробуем альтернативные методы + logger.info("Trying alternative reboot method with SYNO.DSM.System API") + result = self._make_api_request("SYNO.DSM.System", "reboot", version=1) + + if result is None and SYNOLOGY_POWER_API != "SYNO.Core.System": + # Пробуем настроенный в конфигурации API, если он отличается + logger.info(f"Trying reboot with configured API: {SYNOLOGY_POWER_API}") + methods_to_try = ["restart", "reboot"] + for method in methods_to_try: + result = self._make_api_request(SYNOLOGY_POWER_API, method, version=SYNOLOGY_API_VERSION) + if result is not None: + logger.info(f"Successfully used method {method} for {SYNOLOGY_POWER_API}") + break + + if result is not None: + logger.info(f"Successfully initiated system reboot using {SYNOLOGY_POWER_API}") + + # Даем системе время начать процесс перезагрузки + logger.info("Waiting for reboot to initialize...") + sleep(5) + + # Ждем, пока система станет недоступна (признак перезагрузки) + reboot_started = False + for i in range(12): # Проверяем в течение 60 секунд (12 * 5сек) + if not self.is_online(force_check=True): + logger.info(f"System went offline after {i*5} seconds, reboot in progress") + reboot_started = True + break + logger.debug(f"System still online, waiting... ({i+1}/12)") + sleep(5) + + if reboot_started: + # Ждем, пока система снова станет доступна + logger.info("Waiting for system to come back online...") + return self.wait_for_boot(max_attempts=30, delay=10) + else: + logger.warning("System did not go offline after reboot command, but command was accepted") + # Даже если система не ушла оффлайн, команда могла быть принята + return True + else: + logger.warning(f"Failed to reboot using {SYNOLOGY_POWER_API}, trying alternative methods") + + # Если не сработал основной метод, пробуем резервные варианты + # Проверка всех доступных методов API для перезагрузки + apis_to_try = [ + {"name": "SYNO.Core.System.Power", "method": "restart", "version": 1}, + {"name": "SYNO.DSM.Power", "method": "reboot", "version": 1}, + {"name": "SYNO.Core.System", "method": "reboot", "version": 3}, + {"name": "SYNO.System.Power", "method": "reboot", "version": 1}, + {"name": "SYNO.DSM.System", "method": "reboot", "version": 1}, + {"name": "SYNO.Core.Hardware.NeedReboot", "method": "reboot", "version": 1} + ] + + # Получаем список доступных API + try: + api_info_url = f"{self.base_url}/entry.cgi" + api_info_params = { + "api": "SYNO.API.Info", + "version": "1", + "method": "query", + "query": "SYNO.Core.System.Power,SYNO.DSM.Power,SYNO.Core.System,SYNO.System.Power,SYNO.DSM.System,SYNO.Core.Hardware.NeedReboot" + } + + logger.debug("Checking available reboot APIs") + api_info_response = self.session.get( + api_info_url, + params=api_info_params, + timeout=self.default_timeout, + verify=False + ) + + if api_info_response.status_code == 200: + api_info_data = api_info_response.json() + if api_info_data.get("success"): + available_apis = api_info_data.get("data", {}) + logger.debug(f"Available APIs: {list(available_apis.keys())}") + + # Фильтруем только доступные API + filtered_apis = [] + for api in apis_to_try: + if api["name"] in available_apis: + api_info = available_apis[api["name"]] + # Проверка версии API + if api_info.get("minVersion", 1) <= api["version"] <= api_info.get("maxVersion", 1): + filtered_apis.append(api) + logger.debug(f"Adding {api['name']} to available reboot APIs") + else: + logger.debug(f"API {api['name']} version {api['version']} not supported. Supported: {api_info.get('minVersion')}-{api_info.get('maxVersion')}") + + if filtered_apis: + apis_to_try = filtered_apis + else: + logger.warning("No compatible APIs found, trying all methods as fallback") + else: + logger.warning("Failed to query API info, using default methods") + else: + logger.warning(f"API info request failed with status {api_info_response.status_code}, using default methods") + except Exception as e: + logger.warning(f"Error querying API info: {str(e)}, using default methods") + + # Пробуем все доступные методы по порядку + for api in apis_to_try: + logger.info(f"Trying reboot with {api['name']}.{api['method']} v{api['version']}") + result = self._make_api_request(api["name"], api["method"], version=api["version"]) + + if result is not None: + logger.info(f"Successfully initiated system reboot using {api['name']}") + + # Даем системе время начать процесс перезагрузки + logger.info("Waiting for reboot to initialize...") + sleep(5) + + # Ждем, пока система станет недоступна (признак перезагрузки) + reboot_started = False + for i in range(12): # Проверяем в течение 60 секунд (12 * 5сек) + if not self.is_online(force_check=True): + logger.info(f"System went offline after {i*5} seconds, reboot in progress") + reboot_started = True + break + logger.debug(f"System still online, waiting... ({i+1}/12)") + sleep(5) + + if reboot_started: + # Ждем, пока система снова станет доступна + logger.info("Waiting for system to come back online...") + return self.wait_for_boot(max_attempts=30, delay=10) + else: + logger.warning("System did not go offline after reboot command, but command was accepted") + # Даже если система не ушла оффлайн, команда могла быть принята + return True + else: + logger.warning(f"Failed to reboot using {api['name']}.{api['method']} v{api['version']}") + + logger.error("Failed to reboot system after trying multiple APIs") + return False + + def _get_error_description(self, error_code: int) -> str: + """Получение описания ошибки по коду""" + error_descriptions = { + 100: "Unknown error", + 101: "Invalid parameter", + 102: "API does not exist", + 103: "Method does not exist", + 104: "Version does not support", + 105: "Permission denied", + 106: "Session timeout", + 107: "Session interrupted by duplicate login", + 400: "Invalid credentials", + 401: "Account disabled", + 402: "Permission denied", + 403: "2FA required", + 404: "Failed to authenticate with 2FA" + } + return error_descriptions.get(error_code, "Unknown error code") + + def _check_tcp_connection(self) -> bool: + """Проверка базового TCP-соединения с Synology NAS""" + try: + socket_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + socket_obj.settimeout(SYNOLOGY_TIMEOUT) + result = socket_obj.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT)) + socket_obj.close() + + return result == 0 + except socket.error as e: + logger.error(f"Socket error during connection check: {str(e)}") + return False + except Exception as e: + logger.error(f"Unexpected error during connection check: {str(e)}") + return False + + def is_online(self, force_check=False) -> bool: + """Проверка онлайн-статуса Synology NAS""" + # Используем кэшированное значение, если доступно и не устарело + current_time = time.time() + if not force_check and (current_time - self._last_online_check) < self._online_check_interval: + logger.debug(f"Using cached online status: {self._last_online_status}") + return self._last_online_status + + logger.info("Checking if NAS is online...") + + # Проверяем TCP-соединение + online_status = self._check_tcp_connection() + logger.info(f"Detected Synology NAS online status: {online_status}") + + # Если TCP-соединение успешно и у нас есть действующий SID, + # попробуем более детальную проверку через API + if online_status and self.sid: + logger.info("Trying to fetch more detailed online status through API...") + + # Пробуем разные API для проверки онлайн-статуса + api_checks = [ + {"api": "SYNO.DSM.Info", "version": "2", "method": "getinfo"}, + {"api": "SYNO.Core.System", "version": "1", "method": "info"}, + {"api": "SYNO.Core.System.Status", "version": "1", "method": "get"} + ] + + api_success = False + for api_check in api_checks: + try: + url = f"{self.base_url}/entry.cgi" + params = { + "api": api_check["api"], + "version": api_check["version"], + "method": api_check["method"], + "sid": self.sid + } + + logger.debug(f"Trying online status check with {api_check['api']}") + response = self.session.get( + url, + params=params, + timeout=self.default_timeout, + verify=False + ) + + if response.status_code == 200: + data = response.json() + if data.get("success"): + logger.info(f"API request successful for {api_check['api']}") + logger.info("Synology NAS is online with API access") + api_success = True + break + else: + error_code = data.get("error", {}).get("code", -1) + logger.warning(f"API response indicates an error: {error_code}, but NAS is reachable") + else: + logger.warning(f"API returned status code {response.status_code}") + except Exception as e: + logger.warning(f"API check failed for {api_check['api']}, but TCP connection succeeded: {str(e)}") + + if not api_success: + logger.warning("All API checks failed, but TCP connection is successful") + + # Обновляем кэшированное значение + self._last_online_check = current_time + self._last_online_status = online_status + + return online_status + + def wake_on_lan(self) -> bool: + """Отправка Wake-on-LAN пакета для включения Synology NAS""" + if not SYNOLOGY_MAC: + logger.error("MAC address not configured") + return False + + try: + # Преобразование MAC-адреса в байты + mac_address = SYNOLOGY_MAC.replace(':', '').replace('-', '') + if len(mac_address) != 12: + logger.error(f"Invalid MAC address format: {SYNOLOGY_MAC}") + return False + + try: + mac_bytes = bytes.fromhex(mac_address) + except ValueError as e: + logger.error(f"Failed to parse MAC address: {str(e)}") + logger.error(f"MAC address must contain only hex characters: {SYNOLOGY_MAC}") + return False + + # Создание Magic Packet + magic_packet = b'\xff' * 6 + mac_bytes * 16 + + # Отправка пакета на конкретный адрес + try: + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) + sock.sendto(magic_packet, (SYNOLOGY_HOST, WOL_PORT)) + sock.close() + logger.info(f"Wake-on-LAN packet sent to {SYNOLOGY_MAC} at {SYNOLOGY_HOST}:{WOL_PORT}") + except Exception as e: + logger.error(f"Error sending directed WoL packet: {str(e)}") + return False + + # Для надежности отправляем также широковещательный пакет + try: + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) + + # Используем стандартный широковещательный адрес + broadcast_addr = "255.255.255.255" + sock.sendto(magic_packet, (broadcast_addr, WOL_PORT)) + sock.close() + logger.info(f"Wake-on-LAN broadcast packet sent to {broadcast_addr}:{WOL_PORT}") + except Exception as e: + logger.warning(f"Error sending broadcast WoL packet (non-critical): {str(e)}") + # Не считаем ошибкой, т.к. основной пакет уже отправлен + + return True + + except Exception as e: + logger.error(f"Unexpected error in wake_on_lan: {str(e)}") + return False + + def wait_for_boot(self, max_attempts: int = 30, delay: int = 10) -> bool: + """Ожидание загрузки Synology NAS""" + logger.info(f"Waiting for Synology NAS to boot (timeout: {max_attempts * delay}s)...") + + for attempt in range(max_attempts): + # Принудительно проверяем статус без использования кэша + if self.is_online(force_check=True): + logger.info(f"Synology NAS is online after {attempt + 1} attempts ({(attempt + 1) * delay}s)") + + # Проверяем, что не только сеть доступна, но и API загрузился + api_ready = False + logger.info("Waiting for API services to initialize...") + + for api_check in range(5): # Даем еще до 50 секунд для загрузки API + if self.sid or self.login(): + api_ready = True + logger.info(f"API services are ready after {api_check + 1} attempts") + break + logger.debug(f"API not ready yet, waiting... ({api_check + 1}/5)") + sleep(10) + + if not api_ready: + logger.warning("System is online but API services may not be fully initialized") + + # Дадим дополнительное время для полной загрузки всех сервисов + sleep(delay) + return True + + sleep(delay) + logger.info(f"Waiting for boot... Attempt {attempt + 1}/{max_attempts}") + + logger.error(f"Synology NAS didn't come online after {max_attempts * delay} seconds") + return False + + def power_on(self) -> bool: + """Включение Synology NAS""" + # Принудительная проверка статуса + if self.is_online(force_check=True): + logger.info("Synology NAS is already online") + return True + + logger.info("Powering on Synology NAS via Wake-on-LAN...") + + # Проверяем, настроен ли MAC-адрес + if not SYNOLOGY_MAC: + logger.error("Cannot power on: MAC address not configured in settings") + return False + + # Пробуем отправить несколько WoL пакетов для надежности + success = False + for attempt in range(3): + logger.debug(f"Sending WoL packet, attempt {attempt + 1}/3") + if self.wake_on_lan(): + success = True + break + sleep(1) + + if not success: + logger.error("Failed to send Wake-on-LAN packets") + return False + + # Ожидание загрузки + logger.info("WoL packets sent successfully, waiting for system to boot...") + boot_result = self.wait_for_boot(max_attempts=30, delay=10) + + if boot_result: + # Проверяем доступность API после загрузки + system_status = self.get_system_status() + if system_status.get("status") == "online": + logger.info("System booted successfully with API access") + return True + else: + logger.warning("System appears to be online but API may not be fully ready") + return True + else: + logger.error("System did not come online after WoL") + return False + + def power_off(self) -> bool: + """Выключение Synology NAS""" + if not self.is_online(force_check=True): + logger.info("Synology NAS is already offline") + return True + + logger.info("Powering off Synology NAS...") + result = self.shutdown_system() + + if result: + # Дополнительная проверка, что система действительно выключилась + logger.info("Verifying system is offline...") + for attempt in range(12): # Проверяем в течение 2 минут (12 * 10 сек) + if not self.is_online(force_check=True): + logger.info(f"System confirmed offline after {attempt * 10} seconds") + return True + logger.debug(f"System still shutting down, checking again... ({attempt + 1}/12)") + sleep(10) + + logger.warning("System still appears to be online after shutdown command") + return False + else: + logger.error("Failed to initiate shutdown") + return False + + # Заглушки для расширенных методов + def get_shared_folders(self) -> List[Dict[str, Any]]: + """Получение списка общих папок""" + logger.info("Getting list of shared folders") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for shared folders request") + return [] + + try: + # Запрашиваем список общих папок через FileStation API + result = self._make_api_request("SYNO.FileStation.List", "list_share", version=2) + + if not result: + # Пробуем альтернативный API + logger.info("Trying alternative API for shared folders") + alt_result = self._make_api_request("SYNO.FileStation.List", "list_share", version=1) + if alt_result: + return alt_result.get("shares", []) + return [] + + return result.get("shares", []) + + except Exception as e: + logger.error(f"Error getting shared folders: {str(e)}") + return [] + + def get_system_load(self) -> Dict[str, Any]: + """Получение информации о загрузке системы""" + logger.info("Getting system load information") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for system load request") + return {} + + try: + # Запрашиваем информацию о загрузке системы + result = self._make_api_request("SYNO.Core.System.Utilization", "get", version=1) + + if not result: + # Пробуем альтернативный API + logger.info("Trying alternative API for system load") + alt_result = self._make_api_request("SYNO.Core.System", "info", version=3) + + if not alt_result: + return {} + + # Формируем из частичных данных + return { + "cpu_load": alt_result.get("cpu_usage", 0), + "memory": { + "total": alt_result.get("memory_size", 0), + "used": alt_result.get("memory_usage", 0), + "usage_percent": alt_result.get("memory_usage_percent", 0) + } + } + + # Формируем структурированный результат + return { + "cpu_load": result.get("cpu", {}).get("user", 0) + result.get("cpu", {}).get("system", 0), + "memory": result.get("memory", {}), + "network": result.get("network", {}) + } + + except Exception as e: + logger.error(f"Error getting system load: {str(e)}") + return {} + + def is_online_api(self) -> bool: + """Проверка онлайн-статуса Synology NAS с использованием API""" + if not self.is_online(): + return False + + # Проверяем доступность API через авторизацию + if not self.sid and not self.login(): + return False + + return True + + def get_storage_status(self) -> Dict[str, Any]: + """Получение подробной информации о хранилище""" + logger.info("Getting storage status information") + + # Проверяем доступность NAS и API + if not self.is_online_api(): + logger.error("Cannot get storage status: NAS is not online or API is not accessible") + return {"error": "authentication_failed"} + + try: + # Пробуем получить информацию о хранилище через SYNO.Storage.CGI.Storage API + result = self._make_api_request("SYNO.Storage.CGI.Storage", "load_info", version=1) + + if not result: + # Пробуем альтернативный API + logger.info("Trying alternative API for storage info") + alt_result = self._make_api_request("SYNO.Core.Storage", "info", version=1) + + if not alt_result: + # Пробуем еще один альтернативный API + logger.info("Trying SYNO.Core.System API for storage info") + sys_result = self._make_api_request("SYNO.Core.System", "info", version=3) + + if not sys_result: + return { + "volumes": [], + "disks": [], + "total_size": 0, + "total_used": 0, + "error": "no_data" + } + + # Извлекаем базовую информацию о хранилище из системной информации + return { + "volumes": [], + "disks": [], + "total_size": sys_result.get("hdd_total_size", 0) * 1024**2, # МБ в байты + "total_used": sys_result.get("hdd_total_size", 0) * sys_result.get("hdd_usage", 0) / 100 * 1024**2, + } + + # Обрабатываем данные из альтернативного API + volumes = alt_result.get("volumes", []) + disks = alt_result.get("disks", []) + + else: + # Обрабатываем данные из основного API + volumes = result.get("volumes", []) + disks = result.get("disks", []) + + # Рассчитываем общие размеры + total_size = 0 + total_used = 0 + + for volume in volumes: + volume_size = volume.get("size", {}).get("total", 0) + volume_used = volume.get("size", {}).get("used", 0) + + total_size += volume_size + total_used += volume_used + + return { + "volumes": volumes, + "disks": disks, + "total_size": total_size, + "total_used": total_used + } + + except Exception as e: + logger.error(f"Error in get_storage_status: {str(e)}") + return {"error": str(e)} + + def get_security_status(self) -> Dict[str, Any]: + """Получение информации о состоянии безопасности""" + logger.info("Getting security status information") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for security status request") + return {"success": False, "error": "authentication_failed"} + + try: + # Пробуем получить информацию о безопасности через API Security Scan + result = self._make_api_request("SYNO.Core.SecurityScan.Status", "get", version=1) + + if not result: + # Пробуем альтернативный API + logger.info("Trying alternative API for security status") + # Проверяем статус брандмауэра + firewall_result = self._make_api_request("SYNO.Core.Security.Firewall.Status", "get", version=1) + + # Проверяем статус автоматических обновлений + update_result = self._make_api_request("SYNO.Core.Upgrade.Server.Setting", "get", version=1) + + # Если ни один из API не отвечает + if not firewall_result and not update_result: + # Получаем общую информацию о системе для базовой проверки безопасности + sys_result = self._make_api_request("SYNO.DSM.Info", "getinfo", version=2) + + if not sys_result: + return { + "success": False, + "status": "unknown", + "last_check": None, + "is_secure": False, + "error": "no_security_api" + } + + # Собираем базовые сведения из системной информации + return { + "success": True, + "status": "basic", + "last_check": None, + "is_secure": True, # Предполагаем, что система в целом безопасна + "firewall_enabled": None, + "auto_update": None, + "version_latest": sys_result.get("version_string", "") + } + + # Собираем информацию из доступных результатов + firewall_enabled = firewall_result.get("enabled", False) if firewall_result else None + auto_update = update_result.get("auto_update", False) if update_result else None + + # Определяем, насколько система безопасна + is_secure = True # По умолчанию предполагаем, что система безопасна + if firewall_enabled is not None and not firewall_enabled: + is_secure = False + + return { + "success": True, + "status": "partial", + "last_check": None, + "is_secure": is_secure, + "firewall_enabled": firewall_enabled, + "auto_update": auto_update + } + + # Если основное API отвечает, возвращаем его данные + return { + "success": True, + "status": result.get("status", "unknown"), + "last_check": result.get("last_check", None), + "is_secure": result.get("is_secure", False), + "details": result.get("details", {}) + } + + except Exception as e: + logger.error(f"Error in get_security_status: {str(e)}") + return {"success": False, "error": str(e)} + + def get_processes(self, limit: int = 20) -> List[Dict[str, Any]]: + """Получение списка активных процессов""" + logger.info(f"Getting list of active processes (limit={limit})") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for processes request") + return [] + + try: + # Получаем список процессов через API + result = self._make_api_request("SYNO.Core.System.Process", "list", version=1, + params={"sort_by": "cpu", "order": "DESC", "limit": limit}) + + if not result: + logger.warning("Failed to get process list") + return [] + + return result.get("processes", []) + + except Exception as e: + logger.error(f"Error getting process list: {str(e)}") + return [] + + def get_network_status(self) -> Dict[str, Any]: + """Получение информации о сетевых подключениях""" + logger.info("Getting network status information") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for network status request") + return {} + + try: + # Получаем информацию о сетевых интерфейсах + interface_result = self._make_api_request("SYNO.Core.Network.Interface", "list", version=1) + + # Получаем статистику использования сети + utilization_result = self._make_api_request("SYNO.Core.System.Utilization", "get", version=1) + + interfaces = [] + if interface_result: + interfaces = interface_result.get("interfaces", []) + + network_stats = {} + if utilization_result and "network" in utilization_result: + network_stats = utilization_result.get("network", {}) + + # Объединяем данные + for interface in interfaces: + iface_id = interface.get("id", "") + if iface_id in network_stats: + interface["rx"] = network_stats[iface_id].get("rx", 0) + interface["tx"] = network_stats[iface_id].get("tx", 0) + + return { + "interfaces": interfaces, + "statistics": network_stats + } + + except Exception as e: + logger.error(f"Error getting network status: {str(e)}") + return {} + + def get_system_logs(self, limit: int = 20) -> List[Dict[str, Any]]: + """Получение журналов системы""" + logger.info(f"Getting system logs (limit={limit})") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for system logs request") + return [] + + try: + # Получаем журналы через API + result = self._make_api_request("SYNO.Core.SyslogClient.Log", "list", version=1, + params={"start": 0, "limit": limit, "sort": "time", "dir": "DESC"}) + + if not result: + # Пробуем альтернативный API + logger.info("Trying alternative API for system logs") + alt_result = self._make_api_request("SYNO.Core.Log", "list", version=1, + params={"start": 0, "limit": limit, "sort": "time", "dir": "DESC"}) + + if alt_result: + return alt_result.get("logs", []) + return [] + + return result.get("logs", []) + + except Exception as e: + logger.error(f"Error getting system logs: {str(e)}") + return [] + + def get_power_schedule(self) -> Dict[str, Any]: + """Получение расписания включения/выключения""" + logger.info("Getting power schedule") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for power schedule request") + return {} + + try: + # Получаем расписание через API + result = self._make_api_request("SYNO.Core.Hardware.PowerSchedule", "get", version=1) + + if not result: + return {} + + return result + + except Exception as e: + logger.error(f"Error getting power schedule: {str(e)}") + return {} + + def set_power_schedule(self, schedule_type: str, days: List[int], time: str, enabled: bool = True) -> bool: + """Настройка расписания включения/выключения + + Args: + schedule_type: Тип расписания ('boot' или 'shutdown') + days: Список дней недели (0-6, где 0 - понедельник) + time: Время в формате 'HH:MM' + enabled: Включить или выключить расписание + + Returns: + True если успешно, False в противном случае + """ + logger.info(f"Setting power schedule for {schedule_type} at {time} on days {days}") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for setting power schedule") + return False + + try: + # Получаем текущее расписание + current_schedule = self._make_api_request("SYNO.Core.Hardware.PowerSchedule", "get", version=1) + + if not current_schedule: + logger.error("Failed to get current power schedule") + return False + + # Подготавливаем новое расписание + params = { + "enabled": enabled, + "type": schedule_type, + "day": days, + "time": time + } + + # Устанавливаем новое расписание + result = self._make_api_request("SYNO.Core.Hardware.PowerSchedule", "set", version=1, params=params) + + if not result: + logger.error("Failed to set power schedule") + return False + + logger.info(f"Power schedule for {schedule_type} set successfully") + return True + + except Exception as e: + logger.error(f"Error setting power schedule: {str(e)}") + return False + + def get_temperature_status(self) -> Dict[str, Any]: + """Получение информации о температуре системы и дисков""" + logger.info("Getting temperature status") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for temperature status request") + return {} + + try: + # Получаем информацию о системе для общей температуры + system_info = self._make_api_request("SYNO.DSM.Info", "getinfo", version=2) + + # Получаем информацию о дисках для их температуры + storage_info = self._make_api_request("SYNO.Storage.CGI.Storage", "load_info", version=1) + + system_temp = None + disk_temps = [] + + if system_info: + system_temp = system_info.get("temperature") + + if storage_info: + disks = storage_info.get("disks", []) + for disk in disks: + name = disk.get("name", "unknown") + model = disk.get("model", "unknown") + temp = disk.get("temp", None) + if temp is not None: + disk_temps.append({ + "name": name, + "model": model, + "temperature": temp + }) + + return { + "system_temperature": system_temp, + "disk_temperatures": disk_temps, + "warning": system_info.get("temperature_warn", False) if system_info else False + } + + except Exception as e: + logger.error(f"Error getting temperature status: {str(e)}") + return {} + + def browse_files(self, folder_path: str = "", limit: int = 100) -> Dict[str, Any]: + """Просмотр файлов в указанной директории + + Args: + folder_path: Путь к папке (пустая строка для корневых общих папок) + limit: Максимальное количество элементов для возврата + + Returns: + Словарь с информацией о файлах и папках + """ + logger.info(f"Browsing files in {folder_path or 'root'}") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for file browsing") + return {"success": False, "error": "authentication_failed"} + + try: + if not folder_path: + # Если путь не указан, получаем список общих папок + result = self._make_api_request("SYNO.FileStation.List", "list_share", version=2) + + if not result: + logger.error("Failed to list shared folders") + return {"success": False, "error": "api_error"} + + return { + "success": True, + "items": result.get("shares", []), + "path": "", + "is_root": True + } + else: + # Получаем список файлов в указанной директории + params = { + "folder_path": folder_path, + "limit": limit, + "offset": 0, + "sort_by": "name", + "sort_direction": "ASC" + } + + result = self._make_api_request("SYNO.FileStation.List", "list", version=2, params=params) + + if not result: + logger.error(f"Failed to list files in {folder_path}") + return {"success": False, "error": "api_error"} + + return { + "success": True, + "items": result.get("files", []), + "path": folder_path, + "is_root": False, + "total": result.get("total", 0) + } + + except Exception as e: + logger.error(f"Error browsing files: {str(e)}") + return {"success": False, "error": str(e)} + + def manage_service(self, service_name: str, action: str = "status") -> Dict[str, Any]: + """Управление системным сервисом + + Args: + service_name: Имя сервиса + action: Действие (status/start/stop/restart) + + Returns: + Словарь с результатом операции + """ + logger.info(f"Managing service {service_name}, action: {action}") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for service management") + return {"success": False, "error": "authentication_failed"} + + try: + # Проверяем доступное API для управления сервисами + if action == "status": + result = self._make_api_request("SYNO.Core.Service", "get", version=1, + params={"service": service_name}) + else: + result = self._make_api_request("SYNO.Core.Service", action, version=1, + params={"service": service_name}) + + if not result: + logger.error(f"Failed to {action} service {service_name}") + return {"success": False, "error": "api_error"} + + return { + "success": True, + "service": service_name, + "action": action, + "result": result, + "status": result.get("status") if action == "status" else "completed" + } + + except Exception as e: + logger.error(f"Error managing service {service_name}: {str(e)}") + return {"success": False, "error": str(e)} + + def search_files(self, pattern: str, folder_path: str = "", limit: int = 100) -> Dict[str, Any]: + """Поиск файлов по шаблону + + Args: + pattern: Шаблон для поиска + folder_path: Путь к папке для поиска (пустая строка для всех общих папок) + limit: Максимальное количество результатов + + Returns: + Словарь с найденными файлами + """ + logger.info(f"Searching for {pattern} in {folder_path or 'all shared folders'}") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for file search") + return {"success": False, "error": "authentication_failed"} + + try: + if not folder_path: + # Получаем список всех общих папок для поиска + shares_result = self._make_api_request("SYNO.FileStation.List", "list_share", version=2) + + if not shares_result: + logger.error("Failed to list shared folders for search") + return {"success": False, "error": "api_error"} + + # Формируем список путей для поиска + folder_paths = [share.get("path") for share in shares_result.get("shares", [])] + else: + folder_paths = [folder_path] + + # Запускаем поиск + params = { + "folder_path": folder_paths, + "pattern": pattern, + "limit": limit, + "offset": 0 + } + + result = self._make_api_request("SYNO.FileStation.Search", "start", version=2, params=params) + + if not result: + logger.error(f"Failed to start search for {pattern}") + return {"success": False, "error": "api_error"} + + # Получаем taskid для проверки результатов + taskid = result.get("taskid") + if not taskid: + logger.error("No taskid received for search") + return {"success": False, "error": "no_task_id"} + + # Ожидаем завершения поиска + search_result = {"finished": False, "progress": 0} + for _ in range(10): # Максимум 10 попыток + search_status = self._make_api_request("SYNO.FileStation.Search", "status", version=2, + params={"taskid": taskid}) + + if not search_status: + break + + search_result["progress"] = search_status.get("progress", 0) + + if search_status.get("finished", False): + search_result["finished"] = True + break + + time.sleep(0.5) # Пауза между запросами + + # Получаем результаты поиска + if search_result["finished"]: + list_result = self._make_api_request("SYNO.FileStation.Search", "list", version=2, + params={"taskid": taskid, "limit": limit}) + + if list_result: + files = list_result.get("files", []) + return { + "success": True, + "pattern": pattern, + "results": files, + "total": list_result.get("total", len(files)) + } + + # Если не удалось получить результаты, останавливаем поиск + self._make_api_request("SYNO.FileStation.Search", "stop", version=2, + params={"taskid": taskid}) + + return { + "success": False, + "error": "search_timeout", + "progress": search_result["progress"] + } + + except Exception as e: + logger.error(f"Error searching files: {str(e)}") + return {"success": False, "error": str(e)} + + def get_backup_status(self) -> Dict[str, Any]: + """Получение информации о резервном копировании""" + logger.info("Getting backup status information") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for backup status request") + return {"success": False, "error": "authentication_failed"} + + try: + # Пробуем получить информацию о Hyper Backup + hyper_result = self._make_api_request("SYNO.Backup.Repository", "list", version=1) + + # Пробуем получить информацию о задачах Time Backup + time_result = self._make_api_request("SYNO.BackupService.Task", "list", version=1) + + # Проверяем статус резервного копирования USB + usb_result = self._make_api_request("SYNO.USBCopy", "status", version=1) + + backups = { + "hyper_backup": hyper_result.get("backups", []) if hyper_result else [], + "time_backup": time_result.get("tasks", []) if time_result else [], + "usb_copy": {"enabled": usb_result.get("enable", False) if usb_result else False} + } + + return { + "success": True, + "backups": backups, + "available_apis": { + "hyper_backup": hyper_result is not None, + "time_backup": time_result is not None, + "usb_copy": usb_result is not None + } + } + + except Exception as e: + logger.error(f"Error getting backup status: {str(e)}") + return {"success": False, "error": str(e)} + + def check_for_updates(self) -> Dict[str, Any]: + """Проверка наличия обновлений системы""" + logger.info("Checking for system updates") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for update check") + return {"success": False, "error": "authentication_failed"} + + try: + # Получаем текущую информацию о системе + system_info = self._make_api_request("SYNO.DSM.Info", "getinfo", version=2) + + if not system_info: + logger.error("Failed to get system info for update check") + return {"success": False, "error": "api_error"} + + # Проверяем наличие обновлений + update_result = self._make_api_request("SYNO.Core.Upgrade.Server", "check", version=1) + + # Получаем настройки автоматического обновления + settings_result = self._make_api_request("SYNO.Core.Upgrade.Server.Setting", "get", version=1) + + # Получаем информацию о доступных обновлениях + update_info = self._make_api_request("SYNO.Core.Upgrade.Server.Download", "list", version=1) + + current_version = system_info.get("version_string", "unknown") + auto_update_enabled = settings_result.get("auto_update", False) if settings_result else False + + updates = [] + if update_info and "updates" in update_info: + updates = update_info.get("updates", []) + + update_available = len(updates) > 0 + + return { + "success": True, + "current_version": current_version, + "update_available": update_available, + "auto_update_enabled": auto_update_enabled, + "updates": updates + } + + except Exception as e: + logger.error(f"Error checking for updates: {str(e)}") + return {"success": False, "error": str(e)} + + def get_quota_info(self) -> Dict[str, Any]: + """Получение информации о квотах пользователей""" + logger.info("Getting user quota information") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for quota info request") + return {"success": False, "error": "authentication_failed"} + + try: + # Получаем список пользователей + users_result = self._make_api_request("SYNO.Core.User", "list", version=1) + + if not users_result: + logger.error("Failed to get user list for quota info") + return {"success": False, "error": "api_error"} + + users = users_result.get("users", []) + user_quotas = [] + + # Получаем квоты для каждого пользователя + for user in users: + user_name = user.get("name") + if not user_name: + continue + + quota_result = self._make_api_request("SYNO.Core.Quota", "get", version=1, + params={"user_name": user_name}) + + if quota_result and "quotas" in quota_result: + user_quotas.append({ + "user": user_name, + "quotas": quota_result.get("quotas", []) + }) + + return { + "success": True, + "user_quotas": user_quotas + } + + except Exception as e: + logger.error(f"Error getting quota info: {str(e)}") + return {"success": False, "error": str(e)} diff --git a/.history/src/api/synology_20250830100555.py b/.history/src/api/synology_20250830100555.py new file mode 100644 index 0000000..a7bf02b --- /dev/null +++ b/.history/src/api/synology_20250830100555.py @@ -0,0 +1,1944 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Модуль для взаимодействия с API Synology NAS +""" + +import requests +from requests.adapters import HTTPAdapter +import json +import logging +import time +import urllib3 +from urllib3.util import Retry +from typing import Dict, Any, Optional, List +import socket +import struct +from time import sleep + +from src.config.config import ( + SYNOLOGY_HOST, + SYNOLOGY_PORT, + SYNOLOGY_USERNAME, + SYNOLOGY_PASSWORD, + SYNOLOGY_SECURE, + SYNOLOGY_TIMEOUT, + SYNOLOGY_MAC, + WOL_PORT, + SYNOLOGY_API_VERSION, + SYNOLOGY_POWER_API, + SYNOLOGY_INFO_API +) +from src.api.api_discovery import discover_available_apis, find_compatible_api + +# Отключение предупреждений о небезопасных SSL-соединениях +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + +logger = logging.getLogger(__name__) + +class SynologyAPI: + """Класс для взаимодействия с API Synology NAS""" + + def __init__(self): + """Инициализация класса SynologyAPI""" + logger.info("Creating API with auto-retry and connection pool") + logger.debug(f"Connection details: Host={SYNOLOGY_HOST}, Port={SYNOLOGY_PORT}, Secure={SYNOLOGY_SECURE}") + + self.protocol = "https" if SYNOLOGY_SECURE else "http" + self.base_url = f"{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi" + self.sid = None + self.session = requests.Session() + + # Настройка SSL + if self.protocol == "https": + logger.debug("SSL enabled, disabling certificate verification for internal network") + self.session.verify = False # Отключаем проверку SSL для внутренней сети + + # Добавляем пользовательские заголовки для улучшения совместимости с API + custom_headers = { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36', + 'Accept': 'application/json, text/javascript, */*; q=0.01', + 'Accept-Language': 'ru-RU,ru;q=0.9,en-US;q=0.8,en;q=0.7', + 'X-Requested-With': 'XMLHttpRequest', + 'Connection': 'keep-alive', + 'Referer': f'{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/' + } + self.session.headers.update(custom_headers) + logger.debug("Added browser-like headers for API compatibility") + + # Добавляем повторные попытки для HTTP-запросов + retry_strategy = Retry( + total=5, # Увеличиваем количество попыток + status_forcelist=[429, 500, 502, 503, 504, 404], + allowed_methods=["GET", "POST"], + backoff_factor=1.5, # Увеличиваем задержку между попытками + respect_retry_after_header=True + ) + adapter = HTTPAdapter( + max_retries=retry_strategy, + pool_connections=3, + pool_maxsize=10 + ) + self.session.mount("http://", adapter) + self.session.mount("https://", adapter) + + # Таймауты будут указаны в запросах + self.default_timeout = (SYNOLOGY_TIMEOUT, SYNOLOGY_TIMEOUT*2) + logger.debug(f"Setting default request timeout: {self.default_timeout}") + + # Кэш для хранения результатов запросов + self._cache = {} + self._cache_ttl = {} + self._last_online_check = 0 + self._last_online_status = False + self._online_check_interval = 30 # Интервал для кэширования проверок онлайн-статуса + + # Время последней успешной аутентификации и срок действия сессии + self._last_auth_time = 0 + self._auth_expiry = 3600 # По умолчанию 1 час + + # Информация о доступных API + self._available_apis = {} + self._api_info_ttl = 0 + + # Инициализируем API version resolver для автоматического определения совместимых API + self.api_resolver = None # Будет создан при необходимости + + def login(self) -> bool: + """Авторизация в API Synology NAS""" + # Сбрасываем SID для новой сессии + self.sid = None + + logger.info("Attempting to authenticate with Synology NAS...") + logger.debug(f"Base URL: {self.base_url}") + + # Проверяем доступность NAS перед запросом с помощью прямой TCP-проверки + # Избегаем вызова is_online(), чтобы не создавать рекурсию + online_status = self._check_tcp_connection() + if not online_status: + logger.error("Cannot login: Synology NAS is not reachable") + return False + + # Пробуем различные версии API для аутентификации + # Начинаем с версии 3, которая показала лучшую совместимость в тестах + auth_versions_to_try = [3, 2, 1, 6] # Пробуем сначала более стабильные версии + + for auth_version in auth_versions_to_try: + try: + # Определяем путь к API аутентификации + auth_path = "entry.cgi" # Используем entry.cgi вместо auth.cgi по умолчанию + + # Проверка информации API для определения доступных версий API + api_info_url = f"{self.base_url}/entry.cgi" + api_info_params = { + "api": "SYNO.API.Info", + "version": "1", + "method": "query", + "query": "SYNO.API.Auth" + } + + logger.debug(f"Querying API info for auth version {auth_version}") + try: + api_info_response = self.session.get( + api_info_url, + params=api_info_params, + timeout=self.default_timeout, + verify=False + ) + + if api_info_response.status_code == 200: + api_info_data = api_info_response.json() + if api_info_data.get("success"): + auth_info = api_info_data.get("data", {}).get("SYNO.API.Auth", {}) + max_version = auth_info.get("maxVersion", 6) + min_version = auth_info.get("minVersion", 1) + auth_path = auth_info.get("path", "entry.cgi") + logger.debug(f"Auth API versions: min={min_version}, max={max_version}, path={auth_path}") + + # Проверяем поддержку текущей версии + if auth_version < min_version or auth_version > max_version: + logger.warning(f"Auth API version {auth_version} not supported (min={min_version}, max={max_version}), trying next version") + continue + else: + logger.warning("Failed to query API info, using default auth path") + else: + logger.warning(f"API info request failed with status {api_info_response.status_code}, using default auth path") + except Exception as e: + logger.warning(f"Error querying API info: {str(e)}, using default auth path") + + # Основной запрос авторизации + url = f"{self.base_url}/{auth_path}" + params = { + "api": "SYNO.API.Auth", + "version": str(auth_version), + "method": "login", + "account": SYNOLOGY_USERNAME, + "passwd": SYNOLOGY_PASSWORD, + "session": "SynologyPowerControlBot", + "format": "cookie" + } + + # Для версии 6+ используем немного другой формат + if auth_version >= 6: + params["enable_syno_token"] = "yes" + + logger.debug(f"Sending auth request to {url} with API version {auth_version}") + start_time = time.time() + response = self.session.get( + url, + params=params, + timeout=self.default_timeout, + verify=False + ) + elapsed_time = time.time() - start_time + logger.debug(f"Auth request completed in {elapsed_time:.2f}s with status code {response.status_code}") + + # Проверка статуса HTTP + if response.status_code != 200: + logger.error(f"HTTP error: {response.status_code}") + logger.debug(f"Response content: {response.text[:200]}") + continue # Пробуем следующую версию + + try: + data = response.json() + except json.JSONDecodeError: + logger.error("Failed to decode JSON response") + logger.debug(f"Response content: {response.text[:200]}") + continue # Пробуем следующую версию + + if data.get("success"): + self.sid = data.get("data", {}).get("sid") + self._last_auth_time = time.time() + logger.info(f"Successfully logged in to Synology NAS using API version {auth_version}") + logger.debug(f"Session ID obtained: {self.sid[:5] if self.sid else 'None'}...") + + # Получаем и сохраняем токен SYNO, если он есть + syno_token = data.get("data", {}).get("synotoken") + if syno_token: + self.session.headers.update({'X-SYNO-TOKEN': syno_token}) + logger.debug("Added X-SYNO-TOKEN header for improved API compatibility") + + # Также добавляем SID в cookies для улучшения совместимости + self.session.cookies.update({ + 'id': self.sid, + 'sid': self.sid + }) + logger.debug("Added SID to session cookies for improved compatibility") + + # Проверка валидности полученной сессии с помощью простого запроса + # Будем использовать SYNO.API.Info без проверки сложных методов + + # Даем системе немного времени для инициализации сессии + time.sleep(0.5) + + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to log in with version {auth_version}: Error code {error_code}") + + # Если ошибка связана с версией API, пробуем следующую версию + if error_code in [104, 105]: + logger.warning(f"Auth version {auth_version} not supported, trying next version") + continue + + # Дополнительная диагностика + if error_code == 400: + logger.error("Authentication error: Invalid credentials") + elif error_code == 401: + logger.error("Authentication error: Account disabled") + elif error_code == 402: + logger.error("Authentication error: Permission denied") + elif error_code == 403: + logger.error("Authentication error: 2-factor authentication required") + elif error_code == 404: + logger.error("Authentication error: Failed to authenticate with 2-factor authentication") + + # Если ошибка связана с неверными учетными данными, нет смысла пробовать другие версии API + if error_code in [400, 401, 402, 403, 404]: + return False + + except requests.exceptions.Timeout: + logger.error(f"Connection timeout during auth with version {auth_version}") + continue # Пробуем следующую версию + except requests.exceptions.ConnectionError as e: + logger.error(f"Connection error during auth with version {auth_version}: {str(e)}") + continue # Пробуем следующую версию + except requests.RequestException as e: + logger.error(f"Request error during auth with version {auth_version}: {str(e)}") + continue # Пробуем следующую версию + except Exception as e: + logger.error(f"Unexpected error during login with version {auth_version}: {str(e)}", exc_info=True) + continue # Пробуем следующую версию + + # Если все версии не сработали + logger.error("Failed to authenticate with any API version") + return False + + def _validate_session(self) -> bool: + """Проверяет валидность сессии после авторизации""" + if not self.sid: + return False + + # Попробуем сделать простой запрос для проверки сессии + test_apis = [ + {"api": "SYNO.Core.System", "method": "info", "version": 1}, + {"api": "SYNO.DSM.Info", "method": "getinfo", "version": 1} + ] + + for test_api in test_apis: + try: + url = f"{self.base_url}/entry.cgi" + params = { + "api": test_api["api"], + "version": str(test_api["version"]), + "method": test_api["method"], + "_sid": self.sid + } + + response = self.session.get(url, params=params, timeout=self.default_timeout, verify=False) + + if response.status_code == 200: + data = response.json() + if data.get("success"): + logger.debug(f"Session validation successful using {test_api['api']}") + return True + else: + error_code = data.get("error", {}).get("code", -1) + if error_code != 119: # Не сессия истекла + logger.debug(f"Session validation with {test_api['api']} returned error code {error_code}") + return True # Считаем сессию валидной, если ошибка не связана с истечением сессии + except Exception as e: + logger.warning(f"Error during session validation with {test_api['api']}: {str(e)}") + + logger.warning("Session validation failed with all test APIs") + return False + + def logout(self) -> bool: + """Выход из API Synology NAS""" + if not self.sid: + return True + + try: + url = f"{self.base_url}/auth.cgi" + params = { + "api": "SYNO.API.Auth", + "version": "1", + "method": "logout", + "session": "SynologyPowerControlBot", + "_sid": self.sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + self.sid = None + logger.info("Successfully logged out from Synology NAS") + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to log out from Synology NAS: Error code {error_code}") + return False + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + return False + + def _make_api_request(self, api_name: str, method: str, version: int = 1, + params: Optional[Dict[str, Any]] = None, retry_auth: bool = True, retry_count: int = 0) -> Optional[Dict[str, Any]]: + """Обобщенный метод для выполнения API запросов с обработкой ошибок""" + # Ограничение на количество повторных попыток + if retry_count >= 3: + logger.error(f"Too many retries for {api_name}.{method}, giving up") + return None + + # Проверка наличия авторизации + if not self.sid and not self.login(): + logger.error(f"Not authenticated for API request: {api_name}.{method}") + return None + + # Проверка информации API для определения пути и поддерживаемой версии + api_info_url = f"{self.base_url}/entry.cgi" + api_info_params = { + "api": "SYNO.API.Info", + "version": "1", + "method": "query", + "query": api_name + } + + api_path = "entry.cgi" + try: + logger.debug(f"Querying API info for {api_name}") + api_info_response = self.session.get( + api_info_url, + params=api_info_params, + timeout=self.default_timeout, + verify=False + ) + + if api_info_response.status_code == 200: + api_info_data = api_info_response.json() + if api_info_data.get("success"): + api_info = api_info_data.get("data", {}).get(api_name, {}) + if api_info: + max_version = api_info.get("maxVersion", version) + min_version = api_info.get("minVersion", version) + api_path = api_info.get("path", "entry.cgi") + + # Проверка, поддерживается ли запрошенная версия + if version < min_version: + logger.warning(f"API version {version} for {api_name} is below minimum {min_version}, using {min_version}") + version = min_version + elif version > max_version: + logger.warning(f"API version {version} for {api_name} exceeds maximum {max_version}, using {max_version}") + version = max_version + + logger.debug(f"Using API path: {api_path}, version: {version}") + else: + logger.warning(f"API {api_name} not found in API info, using defaults") + except Exception as e: + logger.warning(f"Error querying API info for {api_name}: {str(e)}, using defaults") + + # Подготовка базовых параметров запроса + base_params = { + "api": api_name, + "version": str(version), + "method": method, + "_sid": self.sid # Используем _sid вместо sid для лучшей совместимости + } + + # Добавление дополнительных параметров, если они заданы + if params: + base_params.update(params) + + url = f"{self.base_url}/{api_path}" + logger.debug(f"API request: {api_name}.{method} v{version} to {url}") + logger.debug(f"Full request params: {base_params}") + + try: + start_time = time.time() + response = self.session.get( + url, + params=base_params, + timeout=self.default_timeout, + verify=False + ) + elapsed_time = time.time() - start_time + logger.debug(f"API request completed in {elapsed_time:.2f}s with status {response.status_code}") + + # Проверка статуса HTTP + if response.status_code != 200: + logger.error(f"HTTP error: {response.status_code} for {api_name}.{method}") + + # Повторная попытка при ошибках соединения + if response.status_code in [500, 502, 503, 504]: + logger.info(f"Server error, retrying request for {api_name}.{method}") + sleep(2) + return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1) + + return None + + try: + data = response.json() + except json.JSONDecodeError: + logger.error(f"Failed to decode JSON response for {api_name}.{method}") + logger.debug(f"Response content: {response.text[:200]}") + + # Повторная попытка при ошибках декодирования + logger.info(f"JSON decode error, retrying request for {api_name}.{method}") + sleep(2) + return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1) + + if data.get("success"): + logger.info(f"API request successful for {api_name}.{method}") + return data.get("data", {}) + else: + error_code = data.get("error", {}).get("code", -1) + error_desc = self._get_error_description(error_code) + logger.error(f"API error for {api_name}.{method}: {error_code} - {error_desc}") + + # Ошибки доступа или прав часто встречаются, но они не критичные + # Например, ошибка 102 означает, что нет прав, но NAS доступен + if error_code in [102, 103, 104, 105]: + logger.warning(f"Permission-related error for {api_name}.{method}: {error_code}") + # Возвращаем пустой словарь вместо None, + # чтобы вызывающий код мог понять, что запрос выполнен + return {} + + # Если ошибка связана с авторизацией и нам разрешено повторить попытку + if error_code in [106, 107, 119] and retry_auth: + logger.info(f"Session error (code {error_code}), creating fresh session...") + self.sid = None # Сбрасываем SID + + # Для ошибки 119 (Session timeout) дадим системе немного времени + if error_code == 119: + logger.info("Session timeout detected, waiting before retry...") + sleep(3) + + if self.login(): + logger.info("Re-authenticated with fresh session, retrying API request...") + # Рекурсивный вызов, но со счетчиком повторов + return self._make_api_request(api_name, method, version, params, False, retry_count + 1) + + # Для некоторых ошибок можно автоматически повторить запрос + if error_code in [408, 429, 500, 502, 503, 504]: + logger.info(f"Temporary error {error_code}, retrying request for {api_name}.{method}") + sleep(2) + return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1) + + return None + + except requests.exceptions.Timeout: + logger.error(f"Request timeout for {api_name}.{method}") + + # Повторная попытка при таймауте + if retry_count < 2: + logger.info(f"Timeout, retrying request for {api_name}.{method}") + sleep(2) + return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1) + + return None + except requests.exceptions.ConnectionError as e: + logger.error(f"Connection error for {api_name}.{method}: {str(e)}") + + # Повторная попытка при ошибке соединения + if retry_count < 2: + logger.info(f"Connection error, retrying request for {api_name}.{method}") + sleep(2) + return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1) + + return None + except Exception as e: + logger.error(f"Unexpected error during {api_name}.{method}: {str(e)}") + return None + + def get_system_status(self) -> Dict[str, Any]: + """Получение статуса системы""" + # Проверяем доступность системы + if not self.is_online(): + logger.info("Device is offline, skipping API request") + return {"status": "offline"} + + # Проверяем, есть ли кэшированный результат + cache_key = "system_status" + current_time = time.time() + if cache_key in self._cache and current_time - self._cache_ttl.get(cache_key, 0) < 60: + logger.debug("Using cached system status") + return self._cache[cache_key] + + # Используем рекомендованный API для получения информации о системе + logger.info(f"Getting system info using {SYNOLOGY_INFO_API} v{SYNOLOGY_API_VERSION}") + + # Для SYNO.DSM.Info метод getinfo, для других обычно используется get или info + if SYNOLOGY_INFO_API == "SYNO.DSM.Info": + method = "getinfo" + else: + method = "get" + + result = self._make_api_request(SYNOLOGY_INFO_API, method, version=SYNOLOGY_API_VERSION) + + if result: + logger.info(f"Successfully retrieved system info using {SYNOLOGY_INFO_API}") + + # Формируем расширенный ответ с дополнительной информацией + system_info = { + "status": "online", + "hostname": result.get("hostname", "unknown"), + "model": result.get("model", "unknown"), + "version": result.get("version", "unknown"), + "uptime": result.get("uptime", 0), + "time": current_time, + "is_online": True, + "api_used": SYNOLOGY_INFO_API + } + + # Сохраняем в кэше + self._cache[cache_key] = system_info + self._cache_ttl[cache_key] = current_time + + return system_info + + # Если основной API не сработал, пробуем резервные варианты + logger.warning(f"Failed to retrieve system info with {SYNOLOGY_INFO_API}, trying fallback APIs") + + # Пробуем резервные API + apis_to_try = [ + {"name": "SYNO.DSM.Info", "method": "getinfo", "version": 2}, + {"name": "SYNO.Core.System", "method": "info", "version": 1}, + {"name": "SYNO.Core.System.Status", "method": "get", "version": 1}, + {"name": "SYNO.Core.System.Info", "method": "get", "version": 1}, + ] + + for api in apis_to_try: + if api["name"] == SYNOLOGY_INFO_API: + continue # Пропускаем уже проверенный API + + logger.info(f"Trying to get system info using {api['name']}.{api['method']} v{api['version']}") + result = self._make_api_request(api["name"], api["method"], version=api["version"]) + + if result: + logger.info(f"Successfully retrieved system info using {api['name']}") + + # Формируем расширенный ответ с дополнительной информацией + system_info = { + "status": "online", + "hostname": result.get("hostname", "unknown"), + "model": result.get("model", "unknown"), + "version": result.get("version", "unknown"), + "uptime": result.get("uptime", 0), + "time": current_time, + "is_online": True, + "api_used": api["name"] + } + + # Сохраняем в кэше + self._cache[cache_key] = system_info + self._cache_ttl[cache_key] = current_time + + return system_info + + # Если все запросы не удались, но система онлайн, возвращаем базовую информацию + logger.warning("Failed to retrieve system info with all API methods") + return { + "status": "error", + "error": "Failed to fetch system information", + "is_online": True, + "time": current_time + } + + def shutdown_system(self) -> bool: + """Выключение системы""" + # Проверяем, включено ли устройство перед попыткой его выключить + if not self.is_online(force_check=True): + logger.info("Synology NAS is already offline, no need to shut down") + return True + + logger.info("Attempting to shutdown Synology NAS...") + + # Попробуем сначала использовать предпочтительный API для управления питанием + logger.info(f"Trying shutdown with {SYNOLOGY_POWER_API} v{SYNOLOGY_API_VERSION}") + + # Для SYNO.Core.Hardware.PowerRecovery используется метод setPowerOnState + # Для других API обычно используется метод shutdown или reboot + if SYNOLOGY_POWER_API == "SYNO.Core.Hardware.PowerRecovery": + # Для этого API нужны специальные параметры + params = {"state": "powerbtn"} # powerbtn имитирует нажатие кнопки питания + result = self._make_api_request(SYNOLOGY_POWER_API, "setPowerOnState", version=SYNOLOGY_API_VERSION, params=params) + else: + # Пробуем стандартный метод + result = self._make_api_request(SYNOLOGY_POWER_API, "shutdown", version=SYNOLOGY_API_VERSION) + + if result is not None: + logger.info(f"Successfully initiated system shutdown using {SYNOLOGY_POWER_API}") + + # Даем системе время начать процесс выключения + logger.info("Waiting for shutdown to initialize...") + sleep(5) + + # Проверяем статус + if not self.is_online(force_check=True): + logger.info("System is now offline. Shutdown confirmed successful.") + return True + else: + logger.info("System still appears to be online, but shutdown may be in progress.") + return True + else: + logger.warning(f"Failed to shutdown using {SYNOLOGY_POWER_API}, trying alternative methods") + + # Если не сработал основной метод, пробуем резервные варианты + # Проверка всех доступных методов API для выключения + apis_to_try = [ + {"name": "SYNO.Core.System", "method": "shutdown", "version": 3}, + {"name": "SYNO.Core.System.Power", "method": "shutdown", "version": 1}, + {"name": "SYNO.DSM.Power", "method": "shutdown", "version": 1}, + {"name": "SYNO.System.Power", "method": "shutdown", "version": 1}, + {"name": "SYNO.DSM.System", "method": "shutdown", "version": 1} + ] + + # Проверяем доступные API + try: + api_info_url = f"{self.base_url}/entry.cgi" + api_info_params = { + "api": "SYNO.API.Info", + "version": "1", + "method": "query", + "query": "SYNO.Core.System.Power,SYNO.DSM.Power,SYNO.Core.System,SYNO.System.Power,SYNO.DSM.System" + } + + logger.debug("Checking available shutdown APIs") + api_info_response = self.session.get( + api_info_url, + params=api_info_params, + timeout=self.default_timeout, + verify=False + ) + + if api_info_response.status_code == 200: + api_info_data = api_info_response.json() + if api_info_data.get("success"): + available_apis = api_info_data.get("data", {}) + logger.debug(f"Available APIs: {list(available_apis.keys())}") + + # Фильтруем только доступные API + filtered_apis = [] + for api in apis_to_try: + if api["name"] in available_apis: + api_info = available_apis[api["name"]] + # Проверка версии API + if api_info.get("minVersion", 1) <= api["version"] <= api_info.get("maxVersion", 1): + filtered_apis.append(api) + logger.debug(f"Adding {api['name']} to available shutdown APIs") + else: + logger.debug(f"API {api['name']} version {api['version']} not supported. Supported: {api_info.get('minVersion')}-{api_info.get('maxVersion')}") + + if filtered_apis: + apis_to_try = filtered_apis + else: + logger.warning("No compatible APIs found, trying all methods as fallback") + else: + logger.warning("Failed to query API info, using default methods") + else: + logger.warning(f"API info request failed with status {api_info_response.status_code}, using default methods") + except Exception as e: + logger.warning(f"Error querying API info: {str(e)}, using default methods") + + # Пробуем все доступные методы по порядку + for api in apis_to_try: + logger.info(f"Trying shutdown with {api['name']}.{api['method']} v{api['version']}") + result = self._make_api_request(api["name"], api["method"], version=api["version"]) + + if result is not None: + logger.info(f"Successfully initiated system shutdown using {api['name']}") + + # Даем системе время начать процесс выключения + logger.info("Waiting for shutdown to initialize...") + sleep(5) + + # Проверяем статус + if not self.is_online(force_check=True): + logger.info("System is now offline. Shutdown confirmed successful.") + return True + else: + logger.info("System still appears to be online, but shutdown may be in progress.") + return True + else: + logger.warning(f"Failed to shutdown using {api['name']}.{api['method']} v{api['version']}") + + # Если ни один метод не сработал, но система стала недоступна + if not self.is_online(force_check=True): + logger.info("System appears to be shutting down despite API errors") + return True + + logger.error("Failed to shutdown system after trying multiple APIs") + return False + + def reboot_system(self) -> bool: + """Перезагрузка системы""" + # Проверяем, включена ли система + if not self.is_online(force_check=True): + logger.error("Cannot reboot: System is offline") + return False + + logger.info("Attempting to reboot Synology NAS...") + + # Попробуем сначала использовать предпочтительный API для управления питанием + logger.info(f"Trying reboot with {SYNOLOGY_POWER_API} v{SYNOLOGY_API_VERSION}") + + # Пробуем использовать наиболее совместимые API для перезагрузки + # SYNO.Core.System обычно доступен на большинстве систем + logger.info("Trying reboot with SYNO.Core.System API") + result = self._make_api_request("SYNO.Core.System", "reboot", version=1) + + if result is None: + # Если не сработало, пробуем альтернативные методы + logger.info("Trying alternative reboot method with SYNO.DSM.System API") + result = self._make_api_request("SYNO.DSM.System", "reboot", version=1) + + if result is None and SYNOLOGY_POWER_API != "SYNO.Core.System": + # Пробуем настроенный в конфигурации API, если он отличается + logger.info(f"Trying reboot with configured API: {SYNOLOGY_POWER_API}") + methods_to_try = ["restart", "reboot"] + for method in methods_to_try: + result = self._make_api_request(SYNOLOGY_POWER_API, method, version=SYNOLOGY_API_VERSION) + if result is not None: + logger.info(f"Successfully used method {method} for {SYNOLOGY_POWER_API}") + break + + if result is not None: + logger.info(f"Successfully initiated system reboot using {SYNOLOGY_POWER_API}") + + # Даем системе время начать процесс перезагрузки + logger.info("Waiting for reboot to initialize...") + sleep(5) + + # Ждем, пока система станет недоступна (признак перезагрузки) + reboot_started = False + for i in range(12): # Проверяем в течение 60 секунд (12 * 5сек) + if not self.is_online(force_check=True): + logger.info(f"System went offline after {i*5} seconds, reboot in progress") + reboot_started = True + break + logger.debug(f"System still online, waiting... ({i+1}/12)") + sleep(5) + + if reboot_started: + # Ждем, пока система снова станет доступна + logger.info("Waiting for system to come back online...") + return self.wait_for_boot(max_attempts=30, delay=10) + else: + logger.warning("System did not go offline after reboot command, but command was accepted") + # Даже если система не ушла оффлайн, команда могла быть принята + return True + else: + logger.warning(f"Failed to reboot using {SYNOLOGY_POWER_API}, trying alternative methods") + + # Если не сработал основной метод, пробуем резервные варианты + # Проверка всех доступных методов API для перезагрузки + apis_to_try = [ + {"name": "SYNO.Core.System.Power", "method": "restart", "version": 1}, + {"name": "SYNO.DSM.Power", "method": "reboot", "version": 1}, + {"name": "SYNO.Core.System", "method": "reboot", "version": 3}, + {"name": "SYNO.System.Power", "method": "reboot", "version": 1}, + {"name": "SYNO.DSM.System", "method": "reboot", "version": 1}, + {"name": "SYNO.Core.Hardware.NeedReboot", "method": "reboot", "version": 1} + ] + + # Получаем список доступных API + try: + api_info_url = f"{self.base_url}/entry.cgi" + api_info_params = { + "api": "SYNO.API.Info", + "version": "1", + "method": "query", + "query": "SYNO.Core.System.Power,SYNO.DSM.Power,SYNO.Core.System,SYNO.System.Power,SYNO.DSM.System,SYNO.Core.Hardware.NeedReboot" + } + + logger.debug("Checking available reboot APIs") + api_info_response = self.session.get( + api_info_url, + params=api_info_params, + timeout=self.default_timeout, + verify=False + ) + + if api_info_response.status_code == 200: + api_info_data = api_info_response.json() + if api_info_data.get("success"): + available_apis = api_info_data.get("data", {}) + logger.debug(f"Available APIs: {list(available_apis.keys())}") + + # Фильтруем только доступные API + filtered_apis = [] + for api in apis_to_try: + if api["name"] in available_apis: + api_info = available_apis[api["name"]] + # Проверка версии API + if api_info.get("minVersion", 1) <= api["version"] <= api_info.get("maxVersion", 1): + filtered_apis.append(api) + logger.debug(f"Adding {api['name']} to available reboot APIs") + else: + logger.debug(f"API {api['name']} version {api['version']} not supported. Supported: {api_info.get('minVersion')}-{api_info.get('maxVersion')}") + + if filtered_apis: + apis_to_try = filtered_apis + else: + logger.warning("No compatible APIs found, trying all methods as fallback") + else: + logger.warning("Failed to query API info, using default methods") + else: + logger.warning(f"API info request failed with status {api_info_response.status_code}, using default methods") + except Exception as e: + logger.warning(f"Error querying API info: {str(e)}, using default methods") + + # Пробуем все доступные методы по порядку + for api in apis_to_try: + logger.info(f"Trying reboot with {api['name']}.{api['method']} v{api['version']}") + result = self._make_api_request(api["name"], api["method"], version=api["version"]) + + if result is not None: + logger.info(f"Successfully initiated system reboot using {api['name']}") + + # Даем системе время начать процесс перезагрузки + logger.info("Waiting for reboot to initialize...") + sleep(5) + + # Ждем, пока система станет недоступна (признак перезагрузки) + reboot_started = False + for i in range(12): # Проверяем в течение 60 секунд (12 * 5сек) + if not self.is_online(force_check=True): + logger.info(f"System went offline after {i*5} seconds, reboot in progress") + reboot_started = True + break + logger.debug(f"System still online, waiting... ({i+1}/12)") + sleep(5) + + if reboot_started: + # Ждем, пока система снова станет доступна + logger.info("Waiting for system to come back online...") + return self.wait_for_boot(max_attempts=30, delay=10) + else: + logger.warning("System did not go offline after reboot command, but command was accepted") + # Даже если система не ушла оффлайн, команда могла быть принята + return True + else: + logger.warning(f"Failed to reboot using {api['name']}.{api['method']} v{api['version']}") + + logger.error("Failed to reboot system after trying multiple APIs") + return False + + def _get_error_description(self, error_code: int) -> str: + """Получение описания ошибки по коду""" + error_descriptions = { + 100: "Unknown error", + 101: "Invalid parameter", + 102: "API does not exist", + 103: "Method does not exist", + 104: "Version does not support", + 105: "Permission denied", + 106: "Session timeout", + 107: "Session interrupted by duplicate login", + 400: "Invalid credentials", + 401: "Account disabled", + 402: "Permission denied", + 403: "2FA required", + 404: "Failed to authenticate with 2FA" + } + return error_descriptions.get(error_code, "Unknown error code") + + def _check_tcp_connection(self) -> bool: + """Проверка базового TCP-соединения с Synology NAS""" + try: + socket_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + socket_obj.settimeout(SYNOLOGY_TIMEOUT) + result = socket_obj.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT)) + socket_obj.close() + + return result == 0 + except socket.error as e: + logger.error(f"Socket error during connection check: {str(e)}") + return False + except Exception as e: + logger.error(f"Unexpected error during connection check: {str(e)}") + return False + + def is_online(self, force_check=False) -> bool: + """Проверка онлайн-статуса Synology NAS""" + # Используем кэшированное значение, если доступно и не устарело + current_time = time.time() + if not force_check and (current_time - self._last_online_check) < self._online_check_interval: + logger.debug(f"Using cached online status: {self._last_online_status}") + return self._last_online_status + + logger.info("Checking if NAS is online...") + + # Проверяем TCP-соединение + online_status = self._check_tcp_connection() + logger.info(f"Detected Synology NAS online status: {online_status}") + + # Если TCP-соединение успешно и у нас есть действующий SID, + # попробуем более детальную проверку через API + if online_status and self.sid: + logger.info("Trying to fetch more detailed online status through API...") + + # Пробуем разные API для проверки онлайн-статуса + api_checks = [ + {"api": "SYNO.DSM.Info", "version": "2", "method": "getinfo"}, + {"api": "SYNO.Core.System", "version": "1", "method": "info"}, + {"api": "SYNO.Core.System.Status", "version": "1", "method": "get"} + ] + + api_success = False + for api_check in api_checks: + try: + url = f"{self.base_url}/entry.cgi" + params = { + "api": api_check["api"], + "version": api_check["version"], + "method": api_check["method"], + "sid": self.sid + } + + logger.debug(f"Trying online status check with {api_check['api']}") + response = self.session.get( + url, + params=params, + timeout=self.default_timeout, + verify=False + ) + + if response.status_code == 200: + data = response.json() + if data.get("success"): + logger.info(f"API request successful for {api_check['api']}") + logger.info("Synology NAS is online with API access") + api_success = True + break + else: + error_code = data.get("error", {}).get("code", -1) + logger.warning(f"API response indicates an error: {error_code}, but NAS is reachable") + else: + logger.warning(f"API returned status code {response.status_code}") + except Exception as e: + logger.warning(f"API check failed for {api_check['api']}, but TCP connection succeeded: {str(e)}") + + if not api_success: + logger.warning("All API checks failed, but TCP connection is successful") + + # Обновляем кэшированное значение + self._last_online_check = current_time + self._last_online_status = online_status + + return online_status + + def wake_on_lan(self) -> bool: + """Отправка Wake-on-LAN пакета для включения Synology NAS""" + if not SYNOLOGY_MAC: + logger.error("MAC address not configured") + return False + + try: + # Преобразование MAC-адреса в байты + mac_address = SYNOLOGY_MAC.replace(':', '').replace('-', '') + if len(mac_address) != 12: + logger.error(f"Invalid MAC address format: {SYNOLOGY_MAC}") + return False + + try: + mac_bytes = bytes.fromhex(mac_address) + except ValueError as e: + logger.error(f"Failed to parse MAC address: {str(e)}") + logger.error(f"MAC address must contain only hex characters: {SYNOLOGY_MAC}") + return False + + # Создание Magic Packet + magic_packet = b'\xff' * 6 + mac_bytes * 16 + + # Отправка пакета на конкретный адрес + try: + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) + sock.sendto(magic_packet, (SYNOLOGY_HOST, WOL_PORT)) + sock.close() + logger.info(f"Wake-on-LAN packet sent to {SYNOLOGY_MAC} at {SYNOLOGY_HOST}:{WOL_PORT}") + except Exception as e: + logger.error(f"Error sending directed WoL packet: {str(e)}") + return False + + # Для надежности отправляем также широковещательный пакет + try: + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) + + # Используем стандартный широковещательный адрес + broadcast_addr = "255.255.255.255" + sock.sendto(magic_packet, (broadcast_addr, WOL_PORT)) + sock.close() + logger.info(f"Wake-on-LAN broadcast packet sent to {broadcast_addr}:{WOL_PORT}") + except Exception as e: + logger.warning(f"Error sending broadcast WoL packet (non-critical): {str(e)}") + # Не считаем ошибкой, т.к. основной пакет уже отправлен + + return True + + except Exception as e: + logger.error(f"Unexpected error in wake_on_lan: {str(e)}") + return False + + def wait_for_boot(self, max_attempts: int = 30, delay: int = 10) -> bool: + """Ожидание загрузки Synology NAS""" + logger.info(f"Waiting for Synology NAS to boot (timeout: {max_attempts * delay}s)...") + + for attempt in range(max_attempts): + # Принудительно проверяем статус без использования кэша + if self.is_online(force_check=True): + logger.info(f"Synology NAS is online after {attempt + 1} attempts ({(attempt + 1) * delay}s)") + + # Проверяем, что не только сеть доступна, но и API загрузился + api_ready = False + logger.info("Waiting for API services to initialize...") + + for api_check in range(5): # Даем еще до 50 секунд для загрузки API + if self.sid or self.login(): + api_ready = True + logger.info(f"API services are ready after {api_check + 1} attempts") + break + logger.debug(f"API not ready yet, waiting... ({api_check + 1}/5)") + sleep(10) + + if not api_ready: + logger.warning("System is online but API services may not be fully initialized") + + # Дадим дополнительное время для полной загрузки всех сервисов + sleep(delay) + return True + + sleep(delay) + logger.info(f"Waiting for boot... Attempt {attempt + 1}/{max_attempts}") + + logger.error(f"Synology NAS didn't come online after {max_attempts * delay} seconds") + return False + + def power_on(self) -> bool: + """Включение Synology NAS""" + # Принудительная проверка статуса + if self.is_online(force_check=True): + logger.info("Synology NAS is already online") + return True + + logger.info("Powering on Synology NAS via Wake-on-LAN...") + + # Проверяем, настроен ли MAC-адрес + if not SYNOLOGY_MAC: + logger.error("Cannot power on: MAC address not configured in settings") + return False + + # Пробуем отправить несколько WoL пакетов для надежности + success = False + for attempt in range(3): + logger.debug(f"Sending WoL packet, attempt {attempt + 1}/3") + if self.wake_on_lan(): + success = True + break + sleep(1) + + if not success: + logger.error("Failed to send Wake-on-LAN packets") + return False + + # Ожидание загрузки + logger.info("WoL packets sent successfully, waiting for system to boot...") + boot_result = self.wait_for_boot(max_attempts=30, delay=10) + + if boot_result: + # Проверяем доступность API после загрузки + system_status = self.get_system_status() + if system_status.get("status") == "online": + logger.info("System booted successfully with API access") + return True + else: + logger.warning("System appears to be online but API may not be fully ready") + return True + else: + logger.error("System did not come online after WoL") + return False + + def power_off(self) -> bool: + """Выключение Synology NAS""" + if not self.is_online(force_check=True): + logger.info("Synology NAS is already offline") + return True + + logger.info("Powering off Synology NAS...") + + # Используем метод shutdown_system с исправленным API + result = False + try: + # Пробуем использовать наиболее совместимый API для выключения + logger.info("Trying shutdown with SYNO.Core.System API") + api_result = self._make_api_request("SYNO.Core.System", "shutdown", version=1) + if api_result is not None: + result = True + logger.info("Successfully initiated system shutdown using SYNO.Core.System API") + except Exception as e: + logger.error(f"Error during shutdown with SYNO.Core.System: {str(e)}") + + # Если не сработало, пробуем альтернативные методы + if not result: + try: + logger.info("Trying alternative shutdown method with SYNO.DSM.System API") + api_result = self._make_api_request("SYNO.DSM.System", "shutdown", version=1) + if api_result is not None: + result = True + logger.info("Successfully initiated system shutdown using SYNO.DSM.System API") + except Exception as e: + logger.error(f"Error during shutdown with SYNO.DSM.System: {str(e)}") + + # Если все еще не сработало, используем оригинальный метод shutdown_system + if not result: + result = self.shutdown_system() + + if result: + # Дополнительная проверка, что система действительно выключилась + logger.info("Verifying system is offline...") + for attempt in range(12): # Проверяем в течение 2 минут (12 * 10 сек) + if not self.is_online(force_check=True): + logger.info(f"System confirmed offline after {attempt * 10} seconds") + return True + logger.debug(f"System still shutting down, checking again... ({attempt + 1}/12)") + sleep(10) + + logger.warning("System still appears to be online after shutdown command") + return False + else: + logger.error("Failed to initiate shutdown") + return False + + # Заглушки для расширенных методов + def get_shared_folders(self) -> List[Dict[str, Any]]: + """Получение списка общих папок""" + logger.info("Getting list of shared folders") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for shared folders request") + return [] + + try: + # Запрашиваем список общих папок через FileStation API + result = self._make_api_request("SYNO.FileStation.List", "list_share", version=2) + + if not result: + # Пробуем альтернативный API + logger.info("Trying alternative API for shared folders") + alt_result = self._make_api_request("SYNO.FileStation.List", "list_share", version=1) + if alt_result: + return alt_result.get("shares", []) + return [] + + return result.get("shares", []) + + except Exception as e: + logger.error(f"Error getting shared folders: {str(e)}") + return [] + + def get_system_load(self) -> Dict[str, Any]: + """Получение информации о загрузке системы""" + logger.info("Getting system load information") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for system load request") + return {} + + try: + # Запрашиваем информацию о загрузке системы + result = self._make_api_request("SYNO.Core.System.Utilization", "get", version=1) + + if not result: + # Пробуем альтернативный API + logger.info("Trying alternative API for system load") + alt_result = self._make_api_request("SYNO.Core.System", "info", version=3) + + if not alt_result: + return {} + + # Формируем из частичных данных + return { + "cpu_load": alt_result.get("cpu_usage", 0), + "memory": { + "total": alt_result.get("memory_size", 0), + "used": alt_result.get("memory_usage", 0), + "usage_percent": alt_result.get("memory_usage_percent", 0) + } + } + + # Формируем структурированный результат + return { + "cpu_load": result.get("cpu", {}).get("user", 0) + result.get("cpu", {}).get("system", 0), + "memory": result.get("memory", {}), + "network": result.get("network", {}) + } + + except Exception as e: + logger.error(f"Error getting system load: {str(e)}") + return {} + + def is_online_api(self) -> bool: + """Проверка онлайн-статуса Synology NAS с использованием API""" + if not self.is_online(): + return False + + # Проверяем доступность API через авторизацию + if not self.sid and not self.login(): + return False + + return True + + def get_storage_status(self) -> Dict[str, Any]: + """Получение подробной информации о хранилище""" + logger.info("Getting storage status information") + + # Проверяем доступность NAS и API + if not self.is_online_api(): + logger.error("Cannot get storage status: NAS is not online or API is not accessible") + return {"error": "authentication_failed"} + + try: + # Пробуем получить информацию о хранилище через SYNO.Storage.CGI.Storage API + result = self._make_api_request("SYNO.Storage.CGI.Storage", "load_info", version=1) + + if not result: + # Пробуем альтернативный API + logger.info("Trying alternative API for storage info") + alt_result = self._make_api_request("SYNO.Core.Storage", "info", version=1) + + if not alt_result: + # Пробуем еще один альтернативный API + logger.info("Trying SYNO.Core.System API for storage info") + sys_result = self._make_api_request("SYNO.Core.System", "info", version=3) + + if not sys_result: + return { + "volumes": [], + "disks": [], + "total_size": 0, + "total_used": 0, + "error": "no_data" + } + + # Извлекаем базовую информацию о хранилище из системной информации + return { + "volumes": [], + "disks": [], + "total_size": sys_result.get("hdd_total_size", 0) * 1024**2, # МБ в байты + "total_used": sys_result.get("hdd_total_size", 0) * sys_result.get("hdd_usage", 0) / 100 * 1024**2, + } + + # Обрабатываем данные из альтернативного API + volumes = alt_result.get("volumes", []) + disks = alt_result.get("disks", []) + + else: + # Обрабатываем данные из основного API + volumes = result.get("volumes", []) + disks = result.get("disks", []) + + # Рассчитываем общие размеры + total_size = 0 + total_used = 0 + + for volume in volumes: + volume_size = volume.get("size", {}).get("total", 0) + volume_used = volume.get("size", {}).get("used", 0) + + total_size += volume_size + total_used += volume_used + + return { + "volumes": volumes, + "disks": disks, + "total_size": total_size, + "total_used": total_used + } + + except Exception as e: + logger.error(f"Error in get_storage_status: {str(e)}") + return {"error": str(e)} + + def get_security_status(self) -> Dict[str, Any]: + """Получение информации о состоянии безопасности""" + logger.info("Getting security status information") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for security status request") + return {"success": False, "error": "authentication_failed"} + + try: + # Пробуем получить информацию о безопасности через API Security Scan + result = self._make_api_request("SYNO.Core.SecurityScan.Status", "get", version=1) + + if not result: + # Пробуем альтернативный API + logger.info("Trying alternative API for security status") + # Проверяем статус брандмауэра + firewall_result = self._make_api_request("SYNO.Core.Security.Firewall.Status", "get", version=1) + + # Проверяем статус автоматических обновлений + update_result = self._make_api_request("SYNO.Core.Upgrade.Server.Setting", "get", version=1) + + # Если ни один из API не отвечает + if not firewall_result and not update_result: + # Получаем общую информацию о системе для базовой проверки безопасности + sys_result = self._make_api_request("SYNO.DSM.Info", "getinfo", version=2) + + if not sys_result: + return { + "success": False, + "status": "unknown", + "last_check": None, + "is_secure": False, + "error": "no_security_api" + } + + # Собираем базовые сведения из системной информации + return { + "success": True, + "status": "basic", + "last_check": None, + "is_secure": True, # Предполагаем, что система в целом безопасна + "firewall_enabled": None, + "auto_update": None, + "version_latest": sys_result.get("version_string", "") + } + + # Собираем информацию из доступных результатов + firewall_enabled = firewall_result.get("enabled", False) if firewall_result else None + auto_update = update_result.get("auto_update", False) if update_result else None + + # Определяем, насколько система безопасна + is_secure = True # По умолчанию предполагаем, что система безопасна + if firewall_enabled is not None and not firewall_enabled: + is_secure = False + + return { + "success": True, + "status": "partial", + "last_check": None, + "is_secure": is_secure, + "firewall_enabled": firewall_enabled, + "auto_update": auto_update + } + + # Если основное API отвечает, возвращаем его данные + return { + "success": True, + "status": result.get("status", "unknown"), + "last_check": result.get("last_check", None), + "is_secure": result.get("is_secure", False), + "details": result.get("details", {}) + } + + except Exception as e: + logger.error(f"Error in get_security_status: {str(e)}") + return {"success": False, "error": str(e)} + + def get_processes(self, limit: int = 20) -> List[Dict[str, Any]]: + """Получение списка активных процессов""" + logger.info(f"Getting list of active processes (limit={limit})") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for processes request") + return [] + + try: + # Получаем список процессов через API + result = self._make_api_request("SYNO.Core.System.Process", "list", version=1, + params={"sort_by": "cpu", "order": "DESC", "limit": limit}) + + if not result: + logger.warning("Failed to get process list") + return [] + + return result.get("processes", []) + + except Exception as e: + logger.error(f"Error getting process list: {str(e)}") + return [] + + def get_network_status(self) -> Dict[str, Any]: + """Получение информации о сетевых подключениях""" + logger.info("Getting network status information") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for network status request") + return {} + + try: + # Получаем информацию о сетевых интерфейсах + interface_result = self._make_api_request("SYNO.Core.Network.Interface", "list", version=1) + + # Получаем статистику использования сети + utilization_result = self._make_api_request("SYNO.Core.System.Utilization", "get", version=1) + + interfaces = [] + if interface_result: + interfaces = interface_result.get("interfaces", []) + + network_stats = {} + if utilization_result and "network" in utilization_result: + network_stats = utilization_result.get("network", {}) + + # Объединяем данные + for interface in interfaces: + iface_id = interface.get("id", "") + if iface_id in network_stats: + interface["rx"] = network_stats[iface_id].get("rx", 0) + interface["tx"] = network_stats[iface_id].get("tx", 0) + + return { + "interfaces": interfaces, + "statistics": network_stats + } + + except Exception as e: + logger.error(f"Error getting network status: {str(e)}") + return {} + + def get_system_logs(self, limit: int = 20) -> List[Dict[str, Any]]: + """Получение журналов системы""" + logger.info(f"Getting system logs (limit={limit})") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for system logs request") + return [] + + try: + # Получаем журналы через API + result = self._make_api_request("SYNO.Core.SyslogClient.Log", "list", version=1, + params={"start": 0, "limit": limit, "sort": "time", "dir": "DESC"}) + + if not result: + # Пробуем альтернативный API + logger.info("Trying alternative API for system logs") + alt_result = self._make_api_request("SYNO.Core.Log", "list", version=1, + params={"start": 0, "limit": limit, "sort": "time", "dir": "DESC"}) + + if alt_result: + return alt_result.get("logs", []) + return [] + + return result.get("logs", []) + + except Exception as e: + logger.error(f"Error getting system logs: {str(e)}") + return [] + + def get_power_schedule(self) -> Dict[str, Any]: + """Получение расписания включения/выключения""" + logger.info("Getting power schedule") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for power schedule request") + return {} + + try: + # Получаем расписание через API + result = self._make_api_request("SYNO.Core.Hardware.PowerSchedule", "get", version=1) + + if not result: + return {} + + return result + + except Exception as e: + logger.error(f"Error getting power schedule: {str(e)}") + return {} + + def set_power_schedule(self, schedule_type: str, days: List[int], time: str, enabled: bool = True) -> bool: + """Настройка расписания включения/выключения + + Args: + schedule_type: Тип расписания ('boot' или 'shutdown') + days: Список дней недели (0-6, где 0 - понедельник) + time: Время в формате 'HH:MM' + enabled: Включить или выключить расписание + + Returns: + True если успешно, False в противном случае + """ + logger.info(f"Setting power schedule for {schedule_type} at {time} on days {days}") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for setting power schedule") + return False + + try: + # Получаем текущее расписание + current_schedule = self._make_api_request("SYNO.Core.Hardware.PowerSchedule", "get", version=1) + + if not current_schedule: + logger.error("Failed to get current power schedule") + return False + + # Подготавливаем новое расписание + params = { + "enabled": enabled, + "type": schedule_type, + "day": days, + "time": time + } + + # Устанавливаем новое расписание + result = self._make_api_request("SYNO.Core.Hardware.PowerSchedule", "set", version=1, params=params) + + if not result: + logger.error("Failed to set power schedule") + return False + + logger.info(f"Power schedule for {schedule_type} set successfully") + return True + + except Exception as e: + logger.error(f"Error setting power schedule: {str(e)}") + return False + + def get_temperature_status(self) -> Dict[str, Any]: + """Получение информации о температуре системы и дисков""" + logger.info("Getting temperature status") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for temperature status request") + return {} + + try: + # Получаем информацию о системе для общей температуры + system_info = self._make_api_request("SYNO.DSM.Info", "getinfo", version=2) + + # Получаем информацию о дисках для их температуры + storage_info = self._make_api_request("SYNO.Storage.CGI.Storage", "load_info", version=1) + + system_temp = None + disk_temps = [] + + if system_info: + system_temp = system_info.get("temperature") + + if storage_info: + disks = storage_info.get("disks", []) + for disk in disks: + name = disk.get("name", "unknown") + model = disk.get("model", "unknown") + temp = disk.get("temp", None) + if temp is not None: + disk_temps.append({ + "name": name, + "model": model, + "temperature": temp + }) + + return { + "system_temperature": system_temp, + "disk_temperatures": disk_temps, + "warning": system_info.get("temperature_warn", False) if system_info else False + } + + except Exception as e: + logger.error(f"Error getting temperature status: {str(e)}") + return {} + + def browse_files(self, folder_path: str = "", limit: int = 100) -> Dict[str, Any]: + """Просмотр файлов в указанной директории + + Args: + folder_path: Путь к папке (пустая строка для корневых общих папок) + limit: Максимальное количество элементов для возврата + + Returns: + Словарь с информацией о файлах и папках + """ + logger.info(f"Browsing files in {folder_path or 'root'}") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for file browsing") + return {"success": False, "error": "authentication_failed"} + + try: + if not folder_path: + # Если путь не указан, получаем список общих папок + result = self._make_api_request("SYNO.FileStation.List", "list_share", version=2) + + if not result: + logger.error("Failed to list shared folders") + return {"success": False, "error": "api_error"} + + return { + "success": True, + "items": result.get("shares", []), + "path": "", + "is_root": True + } + else: + # Получаем список файлов в указанной директории + params = { + "folder_path": folder_path, + "limit": limit, + "offset": 0, + "sort_by": "name", + "sort_direction": "ASC" + } + + result = self._make_api_request("SYNO.FileStation.List", "list", version=2, params=params) + + if not result: + logger.error(f"Failed to list files in {folder_path}") + return {"success": False, "error": "api_error"} + + return { + "success": True, + "items": result.get("files", []), + "path": folder_path, + "is_root": False, + "total": result.get("total", 0) + } + + except Exception as e: + logger.error(f"Error browsing files: {str(e)}") + return {"success": False, "error": str(e)} + + def manage_service(self, service_name: str, action: str = "status") -> Dict[str, Any]: + """Управление системным сервисом + + Args: + service_name: Имя сервиса + action: Действие (status/start/stop/restart) + + Returns: + Словарь с результатом операции + """ + logger.info(f"Managing service {service_name}, action: {action}") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for service management") + return {"success": False, "error": "authentication_failed"} + + try: + # Проверяем доступное API для управления сервисами + if action == "status": + result = self._make_api_request("SYNO.Core.Service", "get", version=1, + params={"service": service_name}) + else: + result = self._make_api_request("SYNO.Core.Service", action, version=1, + params={"service": service_name}) + + if not result: + logger.error(f"Failed to {action} service {service_name}") + return {"success": False, "error": "api_error"} + + return { + "success": True, + "service": service_name, + "action": action, + "result": result, + "status": result.get("status") if action == "status" else "completed" + } + + except Exception as e: + logger.error(f"Error managing service {service_name}: {str(e)}") + return {"success": False, "error": str(e)} + + def search_files(self, pattern: str, folder_path: str = "", limit: int = 100) -> Dict[str, Any]: + """Поиск файлов по шаблону + + Args: + pattern: Шаблон для поиска + folder_path: Путь к папке для поиска (пустая строка для всех общих папок) + limit: Максимальное количество результатов + + Returns: + Словарь с найденными файлами + """ + logger.info(f"Searching for {pattern} in {folder_path or 'all shared folders'}") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for file search") + return {"success": False, "error": "authentication_failed"} + + try: + if not folder_path: + # Получаем список всех общих папок для поиска + shares_result = self._make_api_request("SYNO.FileStation.List", "list_share", version=2) + + if not shares_result: + logger.error("Failed to list shared folders for search") + return {"success": False, "error": "api_error"} + + # Формируем список путей для поиска + folder_paths = [share.get("path") for share in shares_result.get("shares", [])] + else: + folder_paths = [folder_path] + + # Запускаем поиск + params = { + "folder_path": folder_paths, + "pattern": pattern, + "limit": limit, + "offset": 0 + } + + result = self._make_api_request("SYNO.FileStation.Search", "start", version=2, params=params) + + if not result: + logger.error(f"Failed to start search for {pattern}") + return {"success": False, "error": "api_error"} + + # Получаем taskid для проверки результатов + taskid = result.get("taskid") + if not taskid: + logger.error("No taskid received for search") + return {"success": False, "error": "no_task_id"} + + # Ожидаем завершения поиска + search_result = {"finished": False, "progress": 0} + for _ in range(10): # Максимум 10 попыток + search_status = self._make_api_request("SYNO.FileStation.Search", "status", version=2, + params={"taskid": taskid}) + + if not search_status: + break + + search_result["progress"] = search_status.get("progress", 0) + + if search_status.get("finished", False): + search_result["finished"] = True + break + + time.sleep(0.5) # Пауза между запросами + + # Получаем результаты поиска + if search_result["finished"]: + list_result = self._make_api_request("SYNO.FileStation.Search", "list", version=2, + params={"taskid": taskid, "limit": limit}) + + if list_result: + files = list_result.get("files", []) + return { + "success": True, + "pattern": pattern, + "results": files, + "total": list_result.get("total", len(files)) + } + + # Если не удалось получить результаты, останавливаем поиск + self._make_api_request("SYNO.FileStation.Search", "stop", version=2, + params={"taskid": taskid}) + + return { + "success": False, + "error": "search_timeout", + "progress": search_result["progress"] + } + + except Exception as e: + logger.error(f"Error searching files: {str(e)}") + return {"success": False, "error": str(e)} + + def get_backup_status(self) -> Dict[str, Any]: + """Получение информации о резервном копировании""" + logger.info("Getting backup status information") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for backup status request") + return {"success": False, "error": "authentication_failed"} + + try: + # Пробуем получить информацию о Hyper Backup + hyper_result = self._make_api_request("SYNO.Backup.Repository", "list", version=1) + + # Пробуем получить информацию о задачах Time Backup + time_result = self._make_api_request("SYNO.BackupService.Task", "list", version=1) + + # Проверяем статус резервного копирования USB + usb_result = self._make_api_request("SYNO.USBCopy", "status", version=1) + + backups = { + "hyper_backup": hyper_result.get("backups", []) if hyper_result else [], + "time_backup": time_result.get("tasks", []) if time_result else [], + "usb_copy": {"enabled": usb_result.get("enable", False) if usb_result else False} + } + + return { + "success": True, + "backups": backups, + "available_apis": { + "hyper_backup": hyper_result is not None, + "time_backup": time_result is not None, + "usb_copy": usb_result is not None + } + } + + except Exception as e: + logger.error(f"Error getting backup status: {str(e)}") + return {"success": False, "error": str(e)} + + def check_for_updates(self) -> Dict[str, Any]: + """Проверка наличия обновлений системы""" + logger.info("Checking for system updates") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for update check") + return {"success": False, "error": "authentication_failed"} + + try: + # Получаем текущую информацию о системе + system_info = self._make_api_request("SYNO.DSM.Info", "getinfo", version=2) + + if not system_info: + logger.error("Failed to get system info for update check") + return {"success": False, "error": "api_error"} + + # Проверяем наличие обновлений + update_result = self._make_api_request("SYNO.Core.Upgrade.Server", "check", version=1) + + # Получаем настройки автоматического обновления + settings_result = self._make_api_request("SYNO.Core.Upgrade.Server.Setting", "get", version=1) + + # Получаем информацию о доступных обновлениях + update_info = self._make_api_request("SYNO.Core.Upgrade.Server.Download", "list", version=1) + + current_version = system_info.get("version_string", "unknown") + auto_update_enabled = settings_result.get("auto_update", False) if settings_result else False + + updates = [] + if update_info and "updates" in update_info: + updates = update_info.get("updates", []) + + update_available = len(updates) > 0 + + return { + "success": True, + "current_version": current_version, + "update_available": update_available, + "auto_update_enabled": auto_update_enabled, + "updates": updates + } + + except Exception as e: + logger.error(f"Error checking for updates: {str(e)}") + return {"success": False, "error": str(e)} + + def get_quota_info(self) -> Dict[str, Any]: + """Получение информации о квотах пользователей""" + logger.info("Getting user quota information") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for quota info request") + return {"success": False, "error": "authentication_failed"} + + try: + # Получаем список пользователей + users_result = self._make_api_request("SYNO.Core.User", "list", version=1) + + if not users_result: + logger.error("Failed to get user list for quota info") + return {"success": False, "error": "api_error"} + + users = users_result.get("users", []) + user_quotas = [] + + # Получаем квоты для каждого пользователя + for user in users: + user_name = user.get("name") + if not user_name: + continue + + quota_result = self._make_api_request("SYNO.Core.Quota", "get", version=1, + params={"user_name": user_name}) + + if quota_result and "quotas" in quota_result: + user_quotas.append({ + "user": user_name, + "quotas": quota_result.get("quotas", []) + }) + + return { + "success": True, + "user_quotas": user_quotas + } + + except Exception as e: + logger.error(f"Error getting quota info: {str(e)}") + return {"success": False, "error": str(e)} diff --git a/.history/src/api/synology_20250830101023.py b/.history/src/api/synology_20250830101023.py new file mode 100644 index 0000000..ace77ed --- /dev/null +++ b/.history/src/api/synology_20250830101023.py @@ -0,0 +1,1948 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Модуль для взаимодействия с API Synology NAS +""" + +import requests +from requests.adapters import HTTPAdapter +import json +import logging +import time +import urllib3 +from urllib3.util import Retry +from typing import Dict, Any, Optional, List +import socket +import struct +from time import sleep + +from src.config.config import ( + SYNOLOGY_HOST, + SYNOLOGY_PORT, + SYNOLOGY_USERNAME, + SYNOLOGY_PASSWORD, + SYNOLOGY_SECURE, + SYNOLOGY_TIMEOUT, + SYNOLOGY_MAC, + WOL_PORT, + SYNOLOGY_API_VERSION, + SYNOLOGY_POWER_API, + SYNOLOGY_INFO_API +) +from src.api.api_discovery import discover_available_apis, find_compatible_api + +# Отключение предупреждений о небезопасных SSL-соединениях +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + +logger = logging.getLogger(__name__) + +class SynologyAPI: + """Класс для взаимодействия с API Synology NAS""" + + def __init__(self): + """Инициализация класса SynologyAPI""" + logger.info("Creating API with auto-retry and connection pool") + logger.debug(f"Connection details: Host={SYNOLOGY_HOST}, Port={SYNOLOGY_PORT}, Secure={SYNOLOGY_SECURE}") + + self.protocol = "https" if SYNOLOGY_SECURE else "http" + self.base_url = f"{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi" + self.sid = None + self.session = requests.Session() + + # Настройка SSL + if self.protocol == "https": + logger.debug("SSL enabled, disabling certificate verification for internal network") + self.session.verify = False # Отключаем проверку SSL для внутренней сети + + # Добавляем пользовательские заголовки для улучшения совместимости с API + custom_headers = { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36', + 'Accept': 'application/json, text/javascript, */*; q=0.01', + 'Accept-Language': 'ru-RU,ru;q=0.9,en-US;q=0.8,en;q=0.7', + 'X-Requested-With': 'XMLHttpRequest', + 'Connection': 'keep-alive', + 'Referer': f'{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/' + } + self.session.headers.update(custom_headers) + logger.debug("Added browser-like headers for API compatibility") + + # Добавляем повторные попытки для HTTP-запросов + retry_strategy = Retry( + total=5, # Увеличиваем количество попыток + status_forcelist=[429, 500, 502, 503, 504, 404], + allowed_methods=["GET", "POST"], + backoff_factor=1.5, # Увеличиваем задержку между попытками + respect_retry_after_header=True + ) + adapter = HTTPAdapter( + max_retries=retry_strategy, + pool_connections=3, + pool_maxsize=10 + ) + self.session.mount("http://", adapter) + self.session.mount("https://", adapter) + + # Таймауты будут указаны в запросах + self.default_timeout = (SYNOLOGY_TIMEOUT, SYNOLOGY_TIMEOUT*2) + logger.debug(f"Setting default request timeout: {self.default_timeout}") + + # Кэш для хранения результатов запросов + self._cache = {} + self._cache_ttl = {} + self._last_online_check = 0 + self._last_online_status = False + self._online_check_interval = 30 # Интервал для кэширования проверок онлайн-статуса + + # Время последней успешной аутентификации и срок действия сессии + self._last_auth_time = 0 + self._auth_expiry = 3600 # По умолчанию 1 час + + # Информация о доступных API + self._available_apis = {} + self._api_info_ttl = 0 + + # Инициализируем API version resolver для автоматического определения совместимых API + self.api_resolver = None # Будет создан при необходимости + + def login(self) -> bool: + """Авторизация в API Synology NAS""" + # Сбрасываем SID для новой сессии + self.sid = None + + logger.info("Attempting to authenticate with Synology NAS...") + logger.debug(f"Base URL: {self.base_url}") + + # Проверяем доступность NAS перед запросом с помощью прямой TCP-проверки + # Избегаем вызова is_online(), чтобы не создавать рекурсию + online_status = self._check_tcp_connection() + if not online_status: + logger.error("Cannot login: Synology NAS is not reachable") + return False + + # Пробуем различные версии API для аутентификации + # Начинаем с версии 3, которая показала лучшую совместимость в тестах + auth_versions_to_try = [3, 2, 1, 6] # Пробуем сначала более стабильные версии + + for auth_version in auth_versions_to_try: + try: + # Определяем путь к API аутентификации + auth_path = "entry.cgi" # Используем entry.cgi вместо auth.cgi по умолчанию + + # Проверка информации API для определения доступных версий API + api_info_url = f"{self.base_url}/entry.cgi" + api_info_params = { + "api": "SYNO.API.Info", + "version": "1", + "method": "query", + "query": "SYNO.API.Auth" + } + + logger.debug(f"Querying API info for auth version {auth_version}") + try: + api_info_response = self.session.get( + api_info_url, + params=api_info_params, + timeout=self.default_timeout, + verify=False + ) + + if api_info_response.status_code == 200: + api_info_data = api_info_response.json() + if api_info_data.get("success"): + auth_info = api_info_data.get("data", {}).get("SYNO.API.Auth", {}) + max_version = auth_info.get("maxVersion", 6) + min_version = auth_info.get("minVersion", 1) + auth_path = auth_info.get("path", "entry.cgi") + logger.debug(f"Auth API versions: min={min_version}, max={max_version}, path={auth_path}") + + # Проверяем поддержку текущей версии + if auth_version < min_version or auth_version > max_version: + logger.warning(f"Auth API version {auth_version} not supported (min={min_version}, max={max_version}), trying next version") + continue + else: + logger.warning("Failed to query API info, using default auth path") + else: + logger.warning(f"API info request failed with status {api_info_response.status_code}, using default auth path") + except Exception as e: + logger.warning(f"Error querying API info: {str(e)}, using default auth path") + + # Основной запрос авторизации + url = f"{self.base_url}/{auth_path}" + params = { + "api": "SYNO.API.Auth", + "version": str(auth_version), + "method": "login", + "account": SYNOLOGY_USERNAME, + "passwd": SYNOLOGY_PASSWORD, + "session": "SynologyPowerControlBot", + "format": "cookie" + } + + # Для версии 6+ используем немного другой формат + if auth_version >= 6: + params["enable_syno_token"] = "yes" + + logger.debug(f"Sending auth request to {url} with API version {auth_version}") + start_time = time.time() + response = self.session.get( + url, + params=params, + timeout=self.default_timeout, + verify=False + ) + elapsed_time = time.time() - start_time + logger.debug(f"Auth request completed in {elapsed_time:.2f}s with status code {response.status_code}") + + # Проверка статуса HTTP + if response.status_code != 200: + logger.error(f"HTTP error: {response.status_code}") + logger.debug(f"Response content: {response.text[:200]}") + continue # Пробуем следующую версию + + try: + data = response.json() + except json.JSONDecodeError: + logger.error("Failed to decode JSON response") + logger.debug(f"Response content: {response.text[:200]}") + continue # Пробуем следующую версию + + if data.get("success"): + self.sid = data.get("data", {}).get("sid") + self._last_auth_time = time.time() + logger.info(f"Successfully logged in to Synology NAS using API version {auth_version}") + logger.debug(f"Session ID obtained: {self.sid[:5] if self.sid else 'None'}...") + + # Получаем и сохраняем токен SYNO, если он есть + syno_token = data.get("data", {}).get("synotoken") + if syno_token: + self.session.headers.update({'X-SYNO-TOKEN': syno_token}) + logger.debug("Added X-SYNO-TOKEN header for improved API compatibility") + + # Также добавляем SID в cookies для улучшения совместимости + self.session.cookies.update({ + 'id': self.sid, + 'sid': self.sid + }) + logger.debug("Added SID to session cookies for improved compatibility") + + # Проверка валидности полученной сессии с помощью простого запроса + # Будем использовать SYNO.API.Info без проверки сложных методов + + # Даем системе немного времени для инициализации сессии + time.sleep(0.5) + + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to log in with version {auth_version}: Error code {error_code}") + + # Если ошибка связана с версией API, пробуем следующую версию + if error_code in [104, 105]: + logger.warning(f"Auth version {auth_version} not supported, trying next version") + continue + + # Дополнительная диагностика + if error_code == 400: + logger.error("Authentication error: Invalid credentials") + elif error_code == 401: + logger.error("Authentication error: Account disabled") + elif error_code == 402: + logger.error("Authentication error: Permission denied") + elif error_code == 403: + logger.error("Authentication error: 2-factor authentication required") + elif error_code == 404: + logger.error("Authentication error: Failed to authenticate with 2-factor authentication") + + # Если ошибка связана с неверными учетными данными, нет смысла пробовать другие версии API + if error_code in [400, 401, 402, 403, 404]: + return False + + except requests.exceptions.Timeout: + logger.error(f"Connection timeout during auth with version {auth_version}") + continue # Пробуем следующую версию + except requests.exceptions.ConnectionError as e: + logger.error(f"Connection error during auth with version {auth_version}: {str(e)}") + continue # Пробуем следующую версию + except requests.RequestException as e: + logger.error(f"Request error during auth with version {auth_version}: {str(e)}") + continue # Пробуем следующую версию + except Exception as e: + logger.error(f"Unexpected error during login with version {auth_version}: {str(e)}", exc_info=True) + continue # Пробуем следующую версию + + # Если все версии не сработали + logger.error("Failed to authenticate with any API version") + return False + + def _validate_session(self) -> bool: + """Проверяет валидность сессии после авторизации""" + if not self.sid: + return False + + # Попробуем сделать простой запрос для проверки сессии + test_apis = [ + {"api": "SYNO.Core.System", "method": "info", "version": 1}, + {"api": "SYNO.DSM.Info", "method": "getinfo", "version": 1} + ] + + for test_api in test_apis: + try: + url = f"{self.base_url}/entry.cgi" + params = { + "api": test_api["api"], + "version": str(test_api["version"]), + "method": test_api["method"], + "_sid": self.sid + } + + response = self.session.get(url, params=params, timeout=self.default_timeout, verify=False) + + if response.status_code == 200: + data = response.json() + if data.get("success"): + logger.debug(f"Session validation successful using {test_api['api']}") + return True + else: + error_code = data.get("error", {}).get("code", -1) + if error_code != 119: # Не сессия истекла + logger.debug(f"Session validation with {test_api['api']} returned error code {error_code}") + return True # Считаем сессию валидной, если ошибка не связана с истечением сессии + except Exception as e: + logger.warning(f"Error during session validation with {test_api['api']}: {str(e)}") + + logger.warning("Session validation failed with all test APIs") + return False + + def logout(self) -> bool: + """Выход из API Synology NAS""" + if not self.sid: + return True + + try: + url = f"{self.base_url}/auth.cgi" + params = { + "api": "SYNO.API.Auth", + "version": "1", + "method": "logout", + "session": "SynologyPowerControlBot", + "_sid": self.sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + self.sid = None + logger.info("Successfully logged out from Synology NAS") + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to log out from Synology NAS: Error code {error_code}") + return False + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + return False + + def _make_api_request(self, api_name: str, method: str, version: int = 1, + params: Optional[Dict[str, Any]] = None, retry_auth: bool = True, retry_count: int = 0) -> Optional[Dict[str, Any]]: + """Обобщенный метод для выполнения API запросов с обработкой ошибок""" + # Ограничение на количество повторных попыток + if retry_count >= 3: + logger.error(f"Too many retries for {api_name}.{method}, giving up") + return None + + # Проверка наличия авторизации + if not self.sid and not self.login(): + logger.error(f"Not authenticated for API request: {api_name}.{method}") + return None + + # Проверка информации API для определения пути и поддерживаемой версии + api_info_url = f"{self.base_url}/entry.cgi" + api_info_params = { + "api": "SYNO.API.Info", + "version": "1", + "method": "query", + "query": api_name + } + + api_path = "entry.cgi" + try: + logger.debug(f"Querying API info for {api_name}") + api_info_response = self.session.get( + api_info_url, + params=api_info_params, + timeout=self.default_timeout, + verify=False + ) + + if api_info_response.status_code == 200: + api_info_data = api_info_response.json() + if api_info_data.get("success"): + api_info = api_info_data.get("data", {}).get(api_name, {}) + if api_info: + max_version = api_info.get("maxVersion", version) + min_version = api_info.get("minVersion", version) + api_path = api_info.get("path", "entry.cgi") + + # Проверка, поддерживается ли запрошенная версия + if version < min_version: + logger.warning(f"API version {version} for {api_name} is below minimum {min_version}, using {min_version}") + version = min_version + elif version > max_version: + logger.warning(f"API version {version} for {api_name} exceeds maximum {max_version}, using {max_version}") + version = max_version + + logger.debug(f"Using API path: {api_path}, version: {version}") + else: + logger.warning(f"API {api_name} not found in API info, using defaults") + except Exception as e: + logger.warning(f"Error querying API info for {api_name}: {str(e)}, using defaults") + + # Подготовка базовых параметров запроса + base_params = { + "api": api_name, + "version": str(version), + "method": method, + "_sid": self.sid # Используем _sid вместо sid для лучшей совместимости + } + + # Добавление дополнительных параметров, если они заданы + if params: + base_params.update(params) + + url = f"{self.base_url}/{api_path}" + logger.debug(f"API request: {api_name}.{method} v{version} to {url}") + logger.debug(f"Full request params: {base_params}") + + try: + start_time = time.time() + response = self.session.get( + url, + params=base_params, + timeout=self.default_timeout, + verify=False + ) + elapsed_time = time.time() - start_time + logger.debug(f"API request completed in {elapsed_time:.2f}s with status {response.status_code}") + + # Проверка статуса HTTP + if response.status_code != 200: + logger.error(f"HTTP error: {response.status_code} for {api_name}.{method}") + + # Повторная попытка при ошибках соединения + if response.status_code in [500, 502, 503, 504]: + logger.info(f"Server error, retrying request for {api_name}.{method}") + sleep(2) + return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1) + + return None + + try: + data = response.json() + except json.JSONDecodeError: + logger.error(f"Failed to decode JSON response for {api_name}.{method}") + logger.debug(f"Response content: {response.text[:200]}") + + # Повторная попытка при ошибках декодирования + logger.info(f"JSON decode error, retrying request for {api_name}.{method}") + sleep(2) + return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1) + + if data.get("success"): + logger.info(f"API request successful for {api_name}.{method}") + return data.get("data", {}) + else: + error_code = data.get("error", {}).get("code", -1) + error_desc = self._get_error_description(error_code) + logger.error(f"API error for {api_name}.{method}: {error_code} - {error_desc}") + + # Ошибки доступа или прав часто встречаются, но они не критичные + # Например, ошибка 102 означает, что нет прав, но NAS доступен + if error_code in [102, 103, 104, 105]: + logger.warning(f"Permission-related error for {api_name}.{method}: {error_code}") + # Возвращаем пустой словарь вместо None, + # чтобы вызывающий код мог понять, что запрос выполнен + return {} + + # Если ошибка связана с авторизацией и нам разрешено повторить попытку + if error_code in [106, 107, 119] and retry_auth: + logger.info(f"Session error (code {error_code}), creating fresh session...") + self.sid = None # Сбрасываем SID + + # Для ошибки 119 (Session timeout) дадим системе немного времени + if error_code == 119: + logger.info("Session timeout detected, waiting before retry...") + sleep(3) + + if self.login(): + logger.info("Re-authenticated with fresh session, retrying API request...") + # Рекурсивный вызов, но со счетчиком повторов + return self._make_api_request(api_name, method, version, params, False, retry_count + 1) + + # Для некоторых ошибок можно автоматически повторить запрос + if error_code in [408, 429, 500, 502, 503, 504]: + logger.info(f"Temporary error {error_code}, retrying request for {api_name}.{method}") + sleep(2) + return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1) + + return None + + except requests.exceptions.Timeout: + logger.error(f"Request timeout for {api_name}.{method}") + + # Повторная попытка при таймауте + if retry_count < 2: + logger.info(f"Timeout, retrying request for {api_name}.{method}") + sleep(2) + return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1) + + return None + except requests.exceptions.ConnectionError as e: + logger.error(f"Connection error for {api_name}.{method}: {str(e)}") + + # Повторная попытка при ошибке соединения + if retry_count < 2: + logger.info(f"Connection error, retrying request for {api_name}.{method}") + sleep(2) + return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1) + + return None + except Exception as e: + logger.error(f"Unexpected error during {api_name}.{method}: {str(e)}") + return None + + def get_system_status(self) -> Dict[str, Any]: + """Получение статуса системы""" + # Проверяем доступность системы + if not self.is_online(): + logger.info("Device is offline, skipping API request") + return {"status": "offline"} + + # Проверяем, есть ли кэшированный результат + cache_key = "system_status" + current_time = time.time() + if cache_key in self._cache and current_time - self._cache_ttl.get(cache_key, 0) < 60: + logger.debug("Using cached system status") + return self._cache[cache_key] + + # Используем рекомендованный API для получения информации о системе + logger.info(f"Getting system info using {SYNOLOGY_INFO_API} v{SYNOLOGY_API_VERSION}") + + # Для SYNO.DSM.Info метод getinfo, для других обычно используется get или info + if SYNOLOGY_INFO_API == "SYNO.DSM.Info": + method = "getinfo" + else: + method = "get" + + result = self._make_api_request(SYNOLOGY_INFO_API, method, version=SYNOLOGY_API_VERSION) + + if result: + logger.info(f"Successfully retrieved system info using {SYNOLOGY_INFO_API}") + + # Формируем расширенный ответ с дополнительной информацией + system_info = { + "status": "online", + "hostname": result.get("hostname", "unknown"), + "model": result.get("model", "unknown"), + "version": result.get("version", "unknown"), + "uptime": result.get("uptime", 0), + "time": current_time, + "is_online": True, + "api_used": SYNOLOGY_INFO_API + } + + # Сохраняем в кэше + self._cache[cache_key] = system_info + self._cache_ttl[cache_key] = current_time + + return system_info + + # Если основной API не сработал, пробуем резервные варианты + logger.warning(f"Failed to retrieve system info with {SYNOLOGY_INFO_API}, trying fallback APIs") + + # Пробуем резервные API + apis_to_try = [ + {"name": "SYNO.DSM.Info", "method": "getinfo", "version": 2}, + {"name": "SYNO.Core.System", "method": "info", "version": 1}, + {"name": "SYNO.Core.System.Status", "method": "get", "version": 1}, + {"name": "SYNO.Core.System.Info", "method": "get", "version": 1}, + ] + + for api in apis_to_try: + if api["name"] == SYNOLOGY_INFO_API: + continue # Пропускаем уже проверенный API + + logger.info(f"Trying to get system info using {api['name']}.{api['method']} v{api['version']}") + result = self._make_api_request(api["name"], api["method"], version=api["version"]) + + if result: + logger.info(f"Successfully retrieved system info using {api['name']}") + + # Формируем расширенный ответ с дополнительной информацией + system_info = { + "status": "online", + "hostname": result.get("hostname", "unknown"), + "model": result.get("model", "unknown"), + "version": result.get("version", "unknown"), + "uptime": result.get("uptime", 0), + "time": current_time, + "is_online": True, + "api_used": api["name"] + } + + # Сохраняем в кэше + self._cache[cache_key] = system_info + self._cache_ttl[cache_key] = current_time + + return system_info + + # Если все запросы не удались, но система онлайн, возвращаем базовую информацию + logger.warning("Failed to retrieve system info with all API methods") + return { + "status": "error", + "error": "Failed to fetch system information", + "is_online": True, + "time": current_time + } + + def shutdown_system(self) -> bool: + """Выключение системы""" + # Проверяем, включено ли устройство перед попыткой его выключить + if not self.is_online(force_check=True): + logger.info("Synology NAS is already offline, no need to shut down") + return True + + logger.info("Attempting to shutdown Synology NAS...") + + # Попробуем сначала использовать предпочтительный API для управления питанием + logger.info(f"Trying shutdown with {SYNOLOGY_POWER_API} v{SYNOLOGY_API_VERSION}") + + # Для SYNO.Core.Hardware.PowerRecovery используется метод setPowerOnState + # Для других API обычно используется метод shutdown или reboot + if SYNOLOGY_POWER_API == "SYNO.Core.Hardware.PowerRecovery": + # Для этого API нужны специальные параметры + params = {"state": "powerbtn"} # powerbtn имитирует нажатие кнопки питания + result = self._make_api_request(SYNOLOGY_POWER_API, "setPowerOnState", version=SYNOLOGY_API_VERSION, params=params) + else: + # Пробуем стандартный метод + result = self._make_api_request(SYNOLOGY_POWER_API, "shutdown", version=SYNOLOGY_API_VERSION) + + if result is not None: + logger.info(f"Successfully initiated system shutdown using {SYNOLOGY_POWER_API}") + + # Даем системе время начать процесс выключения + logger.info("Waiting for shutdown to initialize...") + sleep(5) + + # Проверяем статус + if not self.is_online(force_check=True): + logger.info("System is now offline. Shutdown confirmed successful.") + return True + else: + logger.info("System still appears to be online, but shutdown may be in progress.") + return True + else: + logger.warning(f"Failed to shutdown using {SYNOLOGY_POWER_API}, trying alternative methods") + + # Если не сработал основной метод, пробуем резервные варианты + # Проверка всех доступных методов API для выключения + apis_to_try = [ + {"name": "SYNO.Core.System", "method": "shutdown", "version": 3}, + {"name": "SYNO.Core.System.Power", "method": "shutdown", "version": 1}, + {"name": "SYNO.DSM.Power", "method": "shutdown", "version": 1}, + {"name": "SYNO.System.Power", "method": "shutdown", "version": 1}, + {"name": "SYNO.DSM.System", "method": "shutdown", "version": 1} + ] + + # Проверяем доступные API + try: + api_info_url = f"{self.base_url}/entry.cgi" + api_info_params = { + "api": "SYNO.API.Info", + "version": "1", + "method": "query", + "query": "SYNO.Core.System.Power,SYNO.DSM.Power,SYNO.Core.System,SYNO.System.Power,SYNO.DSM.System" + } + + logger.debug("Checking available shutdown APIs") + api_info_response = self.session.get( + api_info_url, + params=api_info_params, + timeout=self.default_timeout, + verify=False + ) + + if api_info_response.status_code == 200: + api_info_data = api_info_response.json() + if api_info_data.get("success"): + available_apis = api_info_data.get("data", {}) + logger.debug(f"Available APIs: {list(available_apis.keys())}") + + # Фильтруем только доступные API + filtered_apis = [] + for api in apis_to_try: + if api["name"] in available_apis: + api_info = available_apis[api["name"]] + # Проверка версии API + if api_info.get("minVersion", 1) <= api["version"] <= api_info.get("maxVersion", 1): + filtered_apis.append(api) + logger.debug(f"Adding {api['name']} to available shutdown APIs") + else: + logger.debug(f"API {api['name']} version {api['version']} not supported. Supported: {api_info.get('minVersion')}-{api_info.get('maxVersion')}") + + if filtered_apis: + apis_to_try = filtered_apis + else: + logger.warning("No compatible APIs found, trying all methods as fallback") + else: + logger.warning("Failed to query API info, using default methods") + else: + logger.warning(f"API info request failed with status {api_info_response.status_code}, using default methods") + except Exception as e: + logger.warning(f"Error querying API info: {str(e)}, using default methods") + + # Пробуем все доступные методы по порядку + for api in apis_to_try: + logger.info(f"Trying shutdown with {api['name']}.{api['method']} v{api['version']}") + result = self._make_api_request(api["name"], api["method"], version=api["version"]) + + if result is not None: + logger.info(f"Successfully initiated system shutdown using {api['name']}") + + # Даем системе время начать процесс выключения + logger.info("Waiting for shutdown to initialize...") + sleep(5) + + # Проверяем статус + if not self.is_online(force_check=True): + logger.info("System is now offline. Shutdown confirmed successful.") + return True + else: + logger.info("System still appears to be online, but shutdown may be in progress.") + return True + else: + logger.warning(f"Failed to shutdown using {api['name']}.{api['method']} v{api['version']}") + + # Если ни один метод не сработал, но система стала недоступна + if not self.is_online(force_check=True): + logger.info("System appears to be shutting down despite API errors") + return True + + logger.error("Failed to shutdown system after trying multiple APIs") + return False + + def reboot_system(self) -> bool: + """Перезагрузка системы""" + # Проверяем, включена ли система + if not self.is_online(force_check=True): + logger.error("Cannot reboot: System is offline") + return False + + logger.info("Attempting to reboot Synology NAS...") + + # Попробуем сначала использовать предпочтительный API для управления питанием + logger.info(f"Trying reboot with {SYNOLOGY_POWER_API} v{SYNOLOGY_API_VERSION}") + + # Пробуем использовать наиболее совместимые API для перезагрузки + # SYNO.Core.System обычно доступен на большинстве систем + logger.info("Trying reboot with SYNO.Core.System API") + result = self._make_api_request("SYNO.Core.System", "reboot", version=1) + + if result is None: + # Если не сработало, пробуем альтернативные методы + logger.info("Trying alternative reboot method with SYNO.DSM.System API") + result = self._make_api_request("SYNO.DSM.System", "reboot", version=1) + + if result is None and SYNOLOGY_POWER_API != "SYNO.Core.System": + # Пробуем настроенный в конфигурации API, если он отличается + logger.info(f"Trying reboot with configured API: {SYNOLOGY_POWER_API}") + methods_to_try = ["restart", "reboot"] + for method in methods_to_try: + result = self._make_api_request(SYNOLOGY_POWER_API, method, version=SYNOLOGY_API_VERSION) + if result is not None: + logger.info(f"Successfully used method {method} for {SYNOLOGY_POWER_API}") + break + + if result is not None: + logger.info(f"Successfully initiated system reboot using {SYNOLOGY_POWER_API}") + + # Даем системе время начать процесс перезагрузки + logger.info("Waiting for reboot to initialize...") + sleep(5) + + # Ждем, пока система станет недоступна (признак перезагрузки) + reboot_started = False + for i in range(12): # Проверяем в течение 60 секунд (12 * 5сек) + if not self.is_online(force_check=True): + logger.info(f"System went offline after {i*5} seconds, reboot in progress") + reboot_started = True + break + logger.debug(f"System still online, waiting... ({i+1}/12)") + sleep(5) + + if reboot_started: + # Ждем, пока система снова станет доступна + logger.info("Waiting for system to come back online...") + return self.wait_for_boot(max_attempts=30, delay=10) + else: + logger.warning("System did not go offline after reboot command, but command was accepted") + # Даже если система не ушла оффлайн, команда могла быть принята + return True + else: + logger.warning(f"Failed to reboot using {SYNOLOGY_POWER_API}, trying alternative methods") + + # Если не сработал основной метод, пробуем резервные варианты + # Проверка всех доступных методов API для перезагрузки + apis_to_try = [ + {"name": "SYNO.Core.System.Power", "method": "restart", "version": 1}, + {"name": "SYNO.DSM.Power", "method": "reboot", "version": 1}, + {"name": "SYNO.Core.System", "method": "reboot", "version": 3}, + {"name": "SYNO.System.Power", "method": "reboot", "version": 1}, + {"name": "SYNO.DSM.System", "method": "reboot", "version": 1}, + {"name": "SYNO.Core.Hardware.NeedReboot", "method": "reboot", "version": 1} + ] + + # Получаем список доступных API + try: + api_info_url = f"{self.base_url}/entry.cgi" + api_info_params = { + "api": "SYNO.API.Info", + "version": "1", + "method": "query", + "query": "SYNO.Core.System.Power,SYNO.DSM.Power,SYNO.Core.System,SYNO.System.Power,SYNO.DSM.System,SYNO.Core.Hardware.NeedReboot" + } + + logger.debug("Checking available reboot APIs") + api_info_response = self.session.get( + api_info_url, + params=api_info_params, + timeout=self.default_timeout, + verify=False + ) + + if api_info_response.status_code == 200: + api_info_data = api_info_response.json() + if api_info_data.get("success"): + available_apis = api_info_data.get("data", {}) + logger.debug(f"Available APIs: {list(available_apis.keys())}") + + # Фильтруем только доступные API + filtered_apis = [] + for api in apis_to_try: + if api["name"] in available_apis: + api_info = available_apis[api["name"]] + # Проверка версии API + if api_info.get("minVersion", 1) <= api["version"] <= api_info.get("maxVersion", 1): + filtered_apis.append(api) + logger.debug(f"Adding {api['name']} to available reboot APIs") + else: + logger.debug(f"API {api['name']} version {api['version']} not supported. Supported: {api_info.get('minVersion')}-{api_info.get('maxVersion')}") + + if filtered_apis: + apis_to_try = filtered_apis + else: + logger.warning("No compatible APIs found, trying all methods as fallback") + else: + logger.warning("Failed to query API info, using default methods") + else: + logger.warning(f"API info request failed with status {api_info_response.status_code}, using default methods") + except Exception as e: + logger.warning(f"Error querying API info: {str(e)}, using default methods") + + # Пробуем все доступные методы по порядку + for api in apis_to_try: + logger.info(f"Trying reboot with {api['name']}.{api['method']} v{api['version']}") + result = self._make_api_request(api["name"], api["method"], version=api["version"]) + + if result is not None: + logger.info(f"Successfully initiated system reboot using {api['name']}") + + # Даем системе время начать процесс перезагрузки + logger.info("Waiting for reboot to initialize...") + sleep(5) + + # Ждем, пока система станет недоступна (признак перезагрузки) + reboot_started = False + for i in range(12): # Проверяем в течение 60 секунд (12 * 5сек) + if not self.is_online(force_check=True): + logger.info(f"System went offline after {i*5} seconds, reboot in progress") + reboot_started = True + break + logger.debug(f"System still online, waiting... ({i+1}/12)") + sleep(5) + + if reboot_started: + # Ждем, пока система снова станет доступна + logger.info("Waiting for system to come back online...") + return self.wait_for_boot(max_attempts=30, delay=10) + else: + logger.warning("System did not go offline after reboot command, but command was accepted") + # Даже если система не ушла оффлайн, команда могла быть принята + return True + else: + logger.warning(f"Failed to reboot using {api['name']}.{api['method']} v{api['version']}") + + logger.error("Failed to reboot system after trying multiple APIs") + return False + + def _get_error_description(self, error_code: int) -> str: + """Получение описания ошибки по коду""" + error_descriptions = { + 100: "Unknown error", + 101: "Invalid parameter", + 102: "API does not exist", + 103: "Method does not exist", + 104: "Version does not support", + 105: "Permission denied", + 106: "Session timeout", + 107: "Session interrupted by duplicate login", + 400: "Invalid credentials", + 401: "Account disabled", + 402: "Permission denied", + 403: "2FA required", + 404: "Failed to authenticate with 2FA" + } + return error_descriptions.get(error_code, "Unknown error code") + + def _check_tcp_connection(self) -> bool: + """Проверка базового TCP-соединения с Synology NAS""" + try: + socket_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + socket_obj.settimeout(SYNOLOGY_TIMEOUT) + result = socket_obj.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT)) + socket_obj.close() + + return result == 0 + except socket.error as e: + logger.error(f"Socket error during connection check: {str(e)}") + return False + except Exception as e: + logger.error(f"Unexpected error during connection check: {str(e)}") + return False + + def is_online(self, force_check=False) -> bool: + """Проверка онлайн-статуса Synology NAS""" + # Используем кэшированное значение, если доступно и не устарело + current_time = time.time() + if not force_check and (current_time - self._last_online_check) < self._online_check_interval: + logger.debug(f"Using cached online status: {self._last_online_status}") + return self._last_online_status + + logger.info("Checking if NAS is online...") + + # Проверяем TCP-соединение + online_status = self._check_tcp_connection() + logger.info(f"Detected Synology NAS online status: {online_status}") + + # Если TCP-соединение успешно и у нас есть действующий SID, + # попробуем более детальную проверку через API + if online_status and self.sid: + logger.info("Trying to fetch more detailed online status through API...") + + # Пробуем разные API для проверки онлайн-статуса + api_checks = [ + {"api": "SYNO.DSM.Info", "version": "2", "method": "getinfo"}, + {"api": "SYNO.Core.System", "version": "1", "method": "info"}, + {"api": "SYNO.Core.System.Status", "version": "1", "method": "get"} + ] + + api_success = False + for api_check in api_checks: + try: + url = f"{self.base_url}/entry.cgi" + params = { + "api": api_check["api"], + "version": api_check["version"], + "method": api_check["method"], + "sid": self.sid + } + + logger.debug(f"Trying online status check with {api_check['api']}") + response = self.session.get( + url, + params=params, + timeout=self.default_timeout, + verify=False + ) + + if response.status_code == 200: + data = response.json() + if data.get("success"): + logger.info(f"API request successful for {api_check['api']}") + logger.info("Synology NAS is online with API access") + api_success = True + break + else: + error_code = data.get("error", {}).get("code", -1) + logger.warning(f"API response indicates an error: {error_code}, but NAS is reachable") + else: + logger.warning(f"API returned status code {response.status_code}") + except Exception as e: + logger.warning(f"API check failed for {api_check['api']}, but TCP connection succeeded: {str(e)}") + + if not api_success: + logger.warning("All API checks failed, but TCP connection is successful") + + # Обновляем кэшированное значение + self._last_online_check = current_time + self._last_online_status = online_status + + return online_status + + def wake_on_lan(self) -> bool: + """Отправка Wake-on-LAN пакета для включения Synology NAS""" + if not SYNOLOGY_MAC: + logger.error("MAC address not configured") + return False + + try: + # Преобразование MAC-адреса в байты + mac_address = SYNOLOGY_MAC.replace(':', '').replace('-', '') + if len(mac_address) != 12: + logger.error(f"Invalid MAC address format: {SYNOLOGY_MAC}") + return False + + try: + mac_bytes = bytes.fromhex(mac_address) + except ValueError as e: + logger.error(f"Failed to parse MAC address: {str(e)}") + logger.error(f"MAC address must contain only hex characters: {SYNOLOGY_MAC}") + return False + + # Создание Magic Packet + magic_packet = b'\xff' * 6 + mac_bytes * 16 + + # Отправка пакета на конкретный адрес + try: + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) + sock.sendto(magic_packet, (SYNOLOGY_HOST, WOL_PORT)) + sock.close() + logger.info(f"Wake-on-LAN packet sent to {SYNOLOGY_MAC} at {SYNOLOGY_HOST}:{WOL_PORT}") + except Exception as e: + logger.error(f"Error sending directed WoL packet: {str(e)}") + return False + + # Для надежности отправляем также широковещательный пакет + try: + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) + + # Используем стандартный широковещательный адрес + broadcast_addr = "255.255.255.255" + sock.sendto(magic_packet, (broadcast_addr, WOL_PORT)) + sock.close() + logger.info(f"Wake-on-LAN broadcast packet sent to {broadcast_addr}:{WOL_PORT}") + except Exception as e: + logger.warning(f"Error sending broadcast WoL packet (non-critical): {str(e)}") + # Не считаем ошибкой, т.к. основной пакет уже отправлен + + return True + + except Exception as e: + logger.error(f"Unexpected error in wake_on_lan: {str(e)}") + return False + + def wait_for_boot(self, max_attempts: int = 30, delay: int = 10) -> bool: + """Ожидание загрузки Synology NAS""" + logger.info(f"Waiting for Synology NAS to boot (timeout: {max_attempts * delay}s)...") + + for attempt in range(max_attempts): + # Принудительно проверяем статус без использования кэша + if self.is_online(force_check=True): + logger.info(f"Synology NAS is online after {attempt + 1} attempts ({(attempt + 1) * delay}s)") + + # Проверяем, что не только сеть доступна, но и API загрузился + api_ready = False + logger.info("Waiting for API services to initialize...") + + for api_check in range(5): # Даем еще до 50 секунд для загрузки API + if self.sid or self.login(): + api_ready = True + logger.info(f"API services are ready after {api_check + 1} attempts") + break + logger.debug(f"API not ready yet, waiting... ({api_check + 1}/5)") + sleep(10) + + if not api_ready: + logger.warning("System is online but API services may not be fully initialized") + + # Дадим дополнительное время для полной загрузки всех сервисов + sleep(delay) + return True + + sleep(delay) + logger.info(f"Waiting for boot... Attempt {attempt + 1}/{max_attempts}") + + logger.error(f"Synology NAS didn't come online after {max_attempts * delay} seconds") + return False + + def power_on(self) -> bool: + """Включение Synology NAS""" + # Принудительная проверка статуса + if self.is_online(force_check=True): + logger.info("Synology NAS is already online") + return True + + logger.info("Powering on Synology NAS via Wake-on-LAN...") + + # Проверяем, настроен ли MAC-адрес + if not SYNOLOGY_MAC: + logger.error("Cannot power on: MAC address not configured in settings") + return False + + # Пробуем отправить несколько WoL пакетов для надежности + success = False + for attempt in range(3): + logger.debug(f"Sending WoL packet, attempt {attempt + 1}/3") + if self.wake_on_lan(): + success = True + break + sleep(1) + + if not success: + logger.error("Failed to send Wake-on-LAN packets") + return False + + # Ожидание загрузки + logger.info("WoL packets sent successfully, waiting for system to boot...") + boot_result = self.wait_for_boot(max_attempts=30, delay=10) + + if boot_result: + # Проверяем доступность API после загрузки + system_status = self.get_system_status() + if system_status.get("status") == "online": + logger.info("System booted successfully with API access") + return True + else: + logger.warning("System appears to be online but API may not be fully ready") + return True + else: + logger.error("System did not come online after WoL") + return False + + def power_off(self) -> bool: + """Выключение Synology NAS""" + if not self.is_online(force_check=True): + logger.info("Synology NAS is already offline") + return True + + logger.info("Powering off Synology NAS...") + + # Список API и методов для попытки выключения + apis_to_try = [ + {"api": "SYNO.Core.System", "method": "shutdown", "version": 1}, + {"api": "SYNO.DSM.System", "method": "shutdown", "version": 1}, + {"api": SYNOLOGY_POWER_API, "method": "shutdown", "version": SYNOLOGY_API_VERSION} + ] + + # Перебираем все возможные API и методы + for api_info in apis_to_try: + try: + logger.info(f"Trying shutdown with {api_info['api']} API using method {api_info['method']}") + api_result = self._make_api_request(api_info['api'], api_info['method'], version=api_info['version']) + if api_result is not None: + logger.info(f"Successfully initiated system shutdown using {api_info['api']} API") + + # Даем системе время начать процесс выключения + logger.info("Waiting for shutdown to initialize...") + sleep(5) + + return True + except Exception as e: + logger.error(f"Error during shutdown with {api_info['api']}: {str(e)}") + + # Если все попытки не удались, возвращаем False + logger.error("All shutdown attempts failed") + return False + + # Если все еще не сработало, используем оригинальный метод shutdown_system + if not result: + result = self.shutdown_system() + + if result: + # Дополнительная проверка, что система действительно выключилась + logger.info("Verifying system is offline...") + for attempt in range(12): # Проверяем в течение 2 минут (12 * 10 сек) + if not self.is_online(force_check=True): + logger.info(f"System confirmed offline after {attempt * 10} seconds") + return True + logger.debug(f"System still shutting down, checking again... ({attempt + 1}/12)") + sleep(10) + + logger.warning("System still appears to be online after shutdown command") + return False + else: + logger.error("Failed to initiate shutdown") + return False + + # Заглушки для расширенных методов + def get_shared_folders(self) -> List[Dict[str, Any]]: + """Получение списка общих папок""" + logger.info("Getting list of shared folders") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for shared folders request") + return [] + + try: + # Запрашиваем список общих папок через FileStation API + result = self._make_api_request("SYNO.FileStation.List", "list_share", version=2) + + if not result: + # Пробуем альтернативный API + logger.info("Trying alternative API for shared folders") + alt_result = self._make_api_request("SYNO.FileStation.List", "list_share", version=1) + if alt_result: + return alt_result.get("shares", []) + return [] + + return result.get("shares", []) + + except Exception as e: + logger.error(f"Error getting shared folders: {str(e)}") + return [] + + def get_system_load(self) -> Dict[str, Any]: + """Получение информации о загрузке системы""" + logger.info("Getting system load information") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for system load request") + return {} + + try: + # Запрашиваем информацию о загрузке системы + result = self._make_api_request("SYNO.Core.System.Utilization", "get", version=1) + + if not result: + # Пробуем альтернативный API + logger.info("Trying alternative API for system load") + alt_result = self._make_api_request("SYNO.Core.System", "info", version=3) + + if not alt_result: + return {} + + # Формируем из частичных данных + return { + "cpu_load": alt_result.get("cpu_usage", 0), + "memory": { + "total": alt_result.get("memory_size", 0), + "used": alt_result.get("memory_usage", 0), + "usage_percent": alt_result.get("memory_usage_percent", 0) + } + } + + # Формируем структурированный результат + return { + "cpu_load": result.get("cpu", {}).get("user", 0) + result.get("cpu", {}).get("system", 0), + "memory": result.get("memory", {}), + "network": result.get("network", {}) + } + + except Exception as e: + logger.error(f"Error getting system load: {str(e)}") + return {} + + def is_online_api(self) -> bool: + """Проверка онлайн-статуса Synology NAS с использованием API""" + if not self.is_online(): + return False + + # Проверяем доступность API через авторизацию + if not self.sid and not self.login(): + return False + + return True + + def get_storage_status(self) -> Dict[str, Any]: + """Получение подробной информации о хранилище""" + logger.info("Getting storage status information") + + # Проверяем доступность NAS и API + if not self.is_online_api(): + logger.error("Cannot get storage status: NAS is not online or API is not accessible") + return {"error": "authentication_failed"} + + try: + # Пробуем получить информацию о хранилище через SYNO.Storage.CGI.Storage API + result = self._make_api_request("SYNO.Storage.CGI.Storage", "load_info", version=1) + + if not result: + # Пробуем альтернативный API + logger.info("Trying alternative API for storage info") + alt_result = self._make_api_request("SYNO.Core.Storage", "info", version=1) + + if not alt_result: + # Пробуем еще один альтернативный API + logger.info("Trying SYNO.Core.System API for storage info") + sys_result = self._make_api_request("SYNO.Core.System", "info", version=3) + + if not sys_result: + return { + "volumes": [], + "disks": [], + "total_size": 0, + "total_used": 0, + "error": "no_data" + } + + # Извлекаем базовую информацию о хранилище из системной информации + return { + "volumes": [], + "disks": [], + "total_size": sys_result.get("hdd_total_size", 0) * 1024**2, # МБ в байты + "total_used": sys_result.get("hdd_total_size", 0) * sys_result.get("hdd_usage", 0) / 100 * 1024**2, + } + + # Обрабатываем данные из альтернативного API + volumes = alt_result.get("volumes", []) + disks = alt_result.get("disks", []) + + else: + # Обрабатываем данные из основного API + volumes = result.get("volumes", []) + disks = result.get("disks", []) + + # Рассчитываем общие размеры + total_size = 0 + total_used = 0 + + for volume in volumes: + volume_size = volume.get("size", {}).get("total", 0) + volume_used = volume.get("size", {}).get("used", 0) + + total_size += volume_size + total_used += volume_used + + return { + "volumes": volumes, + "disks": disks, + "total_size": total_size, + "total_used": total_used + } + + except Exception as e: + logger.error(f"Error in get_storage_status: {str(e)}") + return {"error": str(e)} + + def get_security_status(self) -> Dict[str, Any]: + """Получение информации о состоянии безопасности""" + logger.info("Getting security status information") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for security status request") + return {"success": False, "error": "authentication_failed"} + + try: + # Пробуем получить информацию о безопасности через API Security Scan + result = self._make_api_request("SYNO.Core.SecurityScan.Status", "get", version=1) + + if not result: + # Пробуем альтернативный API + logger.info("Trying alternative API for security status") + # Проверяем статус брандмауэра + firewall_result = self._make_api_request("SYNO.Core.Security.Firewall.Status", "get", version=1) + + # Проверяем статус автоматических обновлений + update_result = self._make_api_request("SYNO.Core.Upgrade.Server.Setting", "get", version=1) + + # Если ни один из API не отвечает + if not firewall_result and not update_result: + # Получаем общую информацию о системе для базовой проверки безопасности + sys_result = self._make_api_request("SYNO.DSM.Info", "getinfo", version=2) + + if not sys_result: + return { + "success": False, + "status": "unknown", + "last_check": None, + "is_secure": False, + "error": "no_security_api" + } + + # Собираем базовые сведения из системной информации + return { + "success": True, + "status": "basic", + "last_check": None, + "is_secure": True, # Предполагаем, что система в целом безопасна + "firewall_enabled": None, + "auto_update": None, + "version_latest": sys_result.get("version_string", "") + } + + # Собираем информацию из доступных результатов + firewall_enabled = firewall_result.get("enabled", False) if firewall_result else None + auto_update = update_result.get("auto_update", False) if update_result else None + + # Определяем, насколько система безопасна + is_secure = True # По умолчанию предполагаем, что система безопасна + if firewall_enabled is not None and not firewall_enabled: + is_secure = False + + return { + "success": True, + "status": "partial", + "last_check": None, + "is_secure": is_secure, + "firewall_enabled": firewall_enabled, + "auto_update": auto_update + } + + # Если основное API отвечает, возвращаем его данные + return { + "success": True, + "status": result.get("status", "unknown"), + "last_check": result.get("last_check", None), + "is_secure": result.get("is_secure", False), + "details": result.get("details", {}) + } + + except Exception as e: + logger.error(f"Error in get_security_status: {str(e)}") + return {"success": False, "error": str(e)} + + def get_processes(self, limit: int = 20) -> List[Dict[str, Any]]: + """Получение списка активных процессов""" + logger.info(f"Getting list of active processes (limit={limit})") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for processes request") + return [] + + try: + # Получаем список процессов через API + result = self._make_api_request("SYNO.Core.System.Process", "list", version=1, + params={"sort_by": "cpu", "order": "DESC", "limit": limit}) + + if not result: + logger.warning("Failed to get process list") + return [] + + return result.get("processes", []) + + except Exception as e: + logger.error(f"Error getting process list: {str(e)}") + return [] + + def get_network_status(self) -> Dict[str, Any]: + """Получение информации о сетевых подключениях""" + logger.info("Getting network status information") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for network status request") + return {} + + try: + # Получаем информацию о сетевых интерфейсах + interface_result = self._make_api_request("SYNO.Core.Network.Interface", "list", version=1) + + # Получаем статистику использования сети + utilization_result = self._make_api_request("SYNO.Core.System.Utilization", "get", version=1) + + interfaces = [] + if interface_result: + interfaces = interface_result.get("interfaces", []) + + network_stats = {} + if utilization_result and "network" in utilization_result: + network_stats = utilization_result.get("network", {}) + + # Объединяем данные + for interface in interfaces: + iface_id = interface.get("id", "") + if iface_id in network_stats: + interface["rx"] = network_stats[iface_id].get("rx", 0) + interface["tx"] = network_stats[iface_id].get("tx", 0) + + return { + "interfaces": interfaces, + "statistics": network_stats + } + + except Exception as e: + logger.error(f"Error getting network status: {str(e)}") + return {} + + def get_system_logs(self, limit: int = 20) -> List[Dict[str, Any]]: + """Получение журналов системы""" + logger.info(f"Getting system logs (limit={limit})") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for system logs request") + return [] + + try: + # Получаем журналы через API + result = self._make_api_request("SYNO.Core.SyslogClient.Log", "list", version=1, + params={"start": 0, "limit": limit, "sort": "time", "dir": "DESC"}) + + if not result: + # Пробуем альтернативный API + logger.info("Trying alternative API for system logs") + alt_result = self._make_api_request("SYNO.Core.Log", "list", version=1, + params={"start": 0, "limit": limit, "sort": "time", "dir": "DESC"}) + + if alt_result: + return alt_result.get("logs", []) + return [] + + return result.get("logs", []) + + except Exception as e: + logger.error(f"Error getting system logs: {str(e)}") + return [] + + def get_power_schedule(self) -> Dict[str, Any]: + """Получение расписания включения/выключения""" + logger.info("Getting power schedule") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for power schedule request") + return {} + + try: + # Получаем расписание через API + result = self._make_api_request("SYNO.Core.Hardware.PowerSchedule", "get", version=1) + + if not result: + return {} + + return result + + except Exception as e: + logger.error(f"Error getting power schedule: {str(e)}") + return {} + + def set_power_schedule(self, schedule_type: str, days: List[int], time: str, enabled: bool = True) -> bool: + """Настройка расписания включения/выключения + + Args: + schedule_type: Тип расписания ('boot' или 'shutdown') + days: Список дней недели (0-6, где 0 - понедельник) + time: Время в формате 'HH:MM' + enabled: Включить или выключить расписание + + Returns: + True если успешно, False в противном случае + """ + logger.info(f"Setting power schedule for {schedule_type} at {time} on days {days}") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for setting power schedule") + return False + + try: + # Получаем текущее расписание + current_schedule = self._make_api_request("SYNO.Core.Hardware.PowerSchedule", "get", version=1) + + if not current_schedule: + logger.error("Failed to get current power schedule") + return False + + # Подготавливаем новое расписание + params = { + "enabled": enabled, + "type": schedule_type, + "day": days, + "time": time + } + + # Устанавливаем новое расписание + result = self._make_api_request("SYNO.Core.Hardware.PowerSchedule", "set", version=1, params=params) + + if not result: + logger.error("Failed to set power schedule") + return False + + logger.info(f"Power schedule for {schedule_type} set successfully") + return True + + except Exception as e: + logger.error(f"Error setting power schedule: {str(e)}") + return False + + def get_temperature_status(self) -> Dict[str, Any]: + """Получение информации о температуре системы и дисков""" + logger.info("Getting temperature status") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for temperature status request") + return {} + + try: + # Получаем информацию о системе для общей температуры + system_info = self._make_api_request("SYNO.DSM.Info", "getinfo", version=2) + + # Получаем информацию о дисках для их температуры + storage_info = self._make_api_request("SYNO.Storage.CGI.Storage", "load_info", version=1) + + system_temp = None + disk_temps = [] + + if system_info: + system_temp = system_info.get("temperature") + + if storage_info: + disks = storage_info.get("disks", []) + for disk in disks: + name = disk.get("name", "unknown") + model = disk.get("model", "unknown") + temp = disk.get("temp", None) + if temp is not None: + disk_temps.append({ + "name": name, + "model": model, + "temperature": temp + }) + + return { + "system_temperature": system_temp, + "disk_temperatures": disk_temps, + "warning": system_info.get("temperature_warn", False) if system_info else False + } + + except Exception as e: + logger.error(f"Error getting temperature status: {str(e)}") + return {} + + def browse_files(self, folder_path: str = "", limit: int = 100) -> Dict[str, Any]: + """Просмотр файлов в указанной директории + + Args: + folder_path: Путь к папке (пустая строка для корневых общих папок) + limit: Максимальное количество элементов для возврата + + Returns: + Словарь с информацией о файлах и папках + """ + logger.info(f"Browsing files in {folder_path or 'root'}") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for file browsing") + return {"success": False, "error": "authentication_failed"} + + try: + if not folder_path: + # Если путь не указан, получаем список общих папок + result = self._make_api_request("SYNO.FileStation.List", "list_share", version=2) + + if not result: + logger.error("Failed to list shared folders") + return {"success": False, "error": "api_error"} + + return { + "success": True, + "items": result.get("shares", []), + "path": "", + "is_root": True + } + else: + # Получаем список файлов в указанной директории + params = { + "folder_path": folder_path, + "limit": limit, + "offset": 0, + "sort_by": "name", + "sort_direction": "ASC" + } + + result = self._make_api_request("SYNO.FileStation.List", "list", version=2, params=params) + + if not result: + logger.error(f"Failed to list files in {folder_path}") + return {"success": False, "error": "api_error"} + + return { + "success": True, + "items": result.get("files", []), + "path": folder_path, + "is_root": False, + "total": result.get("total", 0) + } + + except Exception as e: + logger.error(f"Error browsing files: {str(e)}") + return {"success": False, "error": str(e)} + + def manage_service(self, service_name: str, action: str = "status") -> Dict[str, Any]: + """Управление системным сервисом + + Args: + service_name: Имя сервиса + action: Действие (status/start/stop/restart) + + Returns: + Словарь с результатом операции + """ + logger.info(f"Managing service {service_name}, action: {action}") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for service management") + return {"success": False, "error": "authentication_failed"} + + try: + # Проверяем доступное API для управления сервисами + if action == "status": + result = self._make_api_request("SYNO.Core.Service", "get", version=1, + params={"service": service_name}) + else: + result = self._make_api_request("SYNO.Core.Service", action, version=1, + params={"service": service_name}) + + if not result: + logger.error(f"Failed to {action} service {service_name}") + return {"success": False, "error": "api_error"} + + return { + "success": True, + "service": service_name, + "action": action, + "result": result, + "status": result.get("status") if action == "status" else "completed" + } + + except Exception as e: + logger.error(f"Error managing service {service_name}: {str(e)}") + return {"success": False, "error": str(e)} + + def search_files(self, pattern: str, folder_path: str = "", limit: int = 100) -> Dict[str, Any]: + """Поиск файлов по шаблону + + Args: + pattern: Шаблон для поиска + folder_path: Путь к папке для поиска (пустая строка для всех общих папок) + limit: Максимальное количество результатов + + Returns: + Словарь с найденными файлами + """ + logger.info(f"Searching for {pattern} in {folder_path or 'all shared folders'}") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for file search") + return {"success": False, "error": "authentication_failed"} + + try: + if not folder_path: + # Получаем список всех общих папок для поиска + shares_result = self._make_api_request("SYNO.FileStation.List", "list_share", version=2) + + if not shares_result: + logger.error("Failed to list shared folders for search") + return {"success": False, "error": "api_error"} + + # Формируем список путей для поиска + folder_paths = [share.get("path") for share in shares_result.get("shares", [])] + else: + folder_paths = [folder_path] + + # Запускаем поиск + params = { + "folder_path": folder_paths, + "pattern": pattern, + "limit": limit, + "offset": 0 + } + + result = self._make_api_request("SYNO.FileStation.Search", "start", version=2, params=params) + + if not result: + logger.error(f"Failed to start search for {pattern}") + return {"success": False, "error": "api_error"} + + # Получаем taskid для проверки результатов + taskid = result.get("taskid") + if not taskid: + logger.error("No taskid received for search") + return {"success": False, "error": "no_task_id"} + + # Ожидаем завершения поиска + search_result = {"finished": False, "progress": 0} + for _ in range(10): # Максимум 10 попыток + search_status = self._make_api_request("SYNO.FileStation.Search", "status", version=2, + params={"taskid": taskid}) + + if not search_status: + break + + search_result["progress"] = search_status.get("progress", 0) + + if search_status.get("finished", False): + search_result["finished"] = True + break + + time.sleep(0.5) # Пауза между запросами + + # Получаем результаты поиска + if search_result["finished"]: + list_result = self._make_api_request("SYNO.FileStation.Search", "list", version=2, + params={"taskid": taskid, "limit": limit}) + + if list_result: + files = list_result.get("files", []) + return { + "success": True, + "pattern": pattern, + "results": files, + "total": list_result.get("total", len(files)) + } + + # Если не удалось получить результаты, останавливаем поиск + self._make_api_request("SYNO.FileStation.Search", "stop", version=2, + params={"taskid": taskid}) + + return { + "success": False, + "error": "search_timeout", + "progress": search_result["progress"] + } + + except Exception as e: + logger.error(f"Error searching files: {str(e)}") + return {"success": False, "error": str(e)} + + def get_backup_status(self) -> Dict[str, Any]: + """Получение информации о резервном копировании""" + logger.info("Getting backup status information") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for backup status request") + return {"success": False, "error": "authentication_failed"} + + try: + # Пробуем получить информацию о Hyper Backup + hyper_result = self._make_api_request("SYNO.Backup.Repository", "list", version=1) + + # Пробуем получить информацию о задачах Time Backup + time_result = self._make_api_request("SYNO.BackupService.Task", "list", version=1) + + # Проверяем статус резервного копирования USB + usb_result = self._make_api_request("SYNO.USBCopy", "status", version=1) + + backups = { + "hyper_backup": hyper_result.get("backups", []) if hyper_result else [], + "time_backup": time_result.get("tasks", []) if time_result else [], + "usb_copy": {"enabled": usb_result.get("enable", False) if usb_result else False} + } + + return { + "success": True, + "backups": backups, + "available_apis": { + "hyper_backup": hyper_result is not None, + "time_backup": time_result is not None, + "usb_copy": usb_result is not None + } + } + + except Exception as e: + logger.error(f"Error getting backup status: {str(e)}") + return {"success": False, "error": str(e)} + + def check_for_updates(self) -> Dict[str, Any]: + """Проверка наличия обновлений системы""" + logger.info("Checking for system updates") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for update check") + return {"success": False, "error": "authentication_failed"} + + try: + # Получаем текущую информацию о системе + system_info = self._make_api_request("SYNO.DSM.Info", "getinfo", version=2) + + if not system_info: + logger.error("Failed to get system info for update check") + return {"success": False, "error": "api_error"} + + # Проверяем наличие обновлений + update_result = self._make_api_request("SYNO.Core.Upgrade.Server", "check", version=1) + + # Получаем настройки автоматического обновления + settings_result = self._make_api_request("SYNO.Core.Upgrade.Server.Setting", "get", version=1) + + # Получаем информацию о доступных обновлениях + update_info = self._make_api_request("SYNO.Core.Upgrade.Server.Download", "list", version=1) + + current_version = system_info.get("version_string", "unknown") + auto_update_enabled = settings_result.get("auto_update", False) if settings_result else False + + updates = [] + if update_info and "updates" in update_info: + updates = update_info.get("updates", []) + + update_available = len(updates) > 0 + + return { + "success": True, + "current_version": current_version, + "update_available": update_available, + "auto_update_enabled": auto_update_enabled, + "updates": updates + } + + except Exception as e: + logger.error(f"Error checking for updates: {str(e)}") + return {"success": False, "error": str(e)} + + def get_quota_info(self) -> Dict[str, Any]: + """Получение информации о квотах пользователей""" + logger.info("Getting user quota information") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for quota info request") + return {"success": False, "error": "authentication_failed"} + + try: + # Получаем список пользователей + users_result = self._make_api_request("SYNO.Core.User", "list", version=1) + + if not users_result: + logger.error("Failed to get user list for quota info") + return {"success": False, "error": "api_error"} + + users = users_result.get("users", []) + user_quotas = [] + + # Получаем квоты для каждого пользователя + for user in users: + user_name = user.get("name") + if not user_name: + continue + + quota_result = self._make_api_request("SYNO.Core.Quota", "get", version=1, + params={"user_name": user_name}) + + if quota_result and "quotas" in quota_result: + user_quotas.append({ + "user": user_name, + "quotas": quota_result.get("quotas", []) + }) + + return { + "success": True, + "user_quotas": user_quotas + } + + except Exception as e: + logger.error(f"Error getting quota info: {str(e)}") + return {"success": False, "error": str(e)} diff --git a/.history/src/api/synology_20250830101047.py b/.history/src/api/synology_20250830101047.py new file mode 100644 index 0000000..ce496b7 --- /dev/null +++ b/.history/src/api/synology_20250830101047.py @@ -0,0 +1,1961 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Модуль для взаимодействия с API Synology NAS +""" + +import requests +from requests.adapters import HTTPAdapter +import json +import logging +import time +import urllib3 +from urllib3.util import Retry +from typing import Dict, Any, Optional, List +import socket +import struct +from time import sleep + +from src.config.config import ( + SYNOLOGY_HOST, + SYNOLOGY_PORT, + SYNOLOGY_USERNAME, + SYNOLOGY_PASSWORD, + SYNOLOGY_SECURE, + SYNOLOGY_TIMEOUT, + SYNOLOGY_MAC, + WOL_PORT, + SYNOLOGY_API_VERSION, + SYNOLOGY_POWER_API, + SYNOLOGY_INFO_API +) +from src.api.api_discovery import discover_available_apis, find_compatible_api + +# Отключение предупреждений о небезопасных SSL-соединениях +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + +logger = logging.getLogger(__name__) + +class SynologyAPI: + """Класс для взаимодействия с API Synology NAS""" + + def __init__(self): + """Инициализация класса SynologyAPI""" + logger.info("Creating API with auto-retry and connection pool") + logger.debug(f"Connection details: Host={SYNOLOGY_HOST}, Port={SYNOLOGY_PORT}, Secure={SYNOLOGY_SECURE}") + + self.protocol = "https" if SYNOLOGY_SECURE else "http" + self.base_url = f"{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi" + self.sid = None + self.session = requests.Session() + + # Настройка SSL + if self.protocol == "https": + logger.debug("SSL enabled, disabling certificate verification for internal network") + self.session.verify = False # Отключаем проверку SSL для внутренней сети + + # Добавляем пользовательские заголовки для улучшения совместимости с API + custom_headers = { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36', + 'Accept': 'application/json, text/javascript, */*; q=0.01', + 'Accept-Language': 'ru-RU,ru;q=0.9,en-US;q=0.8,en;q=0.7', + 'X-Requested-With': 'XMLHttpRequest', + 'Connection': 'keep-alive', + 'Referer': f'{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/' + } + self.session.headers.update(custom_headers) + logger.debug("Added browser-like headers for API compatibility") + + # Добавляем повторные попытки для HTTP-запросов + retry_strategy = Retry( + total=5, # Увеличиваем количество попыток + status_forcelist=[429, 500, 502, 503, 504, 404], + allowed_methods=["GET", "POST"], + backoff_factor=1.5, # Увеличиваем задержку между попытками + respect_retry_after_header=True + ) + adapter = HTTPAdapter( + max_retries=retry_strategy, + pool_connections=3, + pool_maxsize=10 + ) + self.session.mount("http://", adapter) + self.session.mount("https://", adapter) + + # Таймауты будут указаны в запросах + self.default_timeout = (SYNOLOGY_TIMEOUT, SYNOLOGY_TIMEOUT*2) + logger.debug(f"Setting default request timeout: {self.default_timeout}") + + # Кэш для хранения результатов запросов + self._cache = {} + self._cache_ttl = {} + self._last_online_check = 0 + self._last_online_status = False + self._online_check_interval = 30 # Интервал для кэширования проверок онлайн-статуса + + # Время последней успешной аутентификации и срок действия сессии + self._last_auth_time = 0 + self._auth_expiry = 3600 # По умолчанию 1 час + + # Информация о доступных API + self._available_apis = {} + self._api_info_ttl = 0 + + # Инициализируем API version resolver для автоматического определения совместимых API + self.api_resolver = None # Будет создан при необходимости + + def login(self) -> bool: + """Авторизация в API Synology NAS""" + # Сбрасываем SID для новой сессии + self.sid = None + + logger.info("Attempting to authenticate with Synology NAS...") + logger.debug(f"Base URL: {self.base_url}") + + # Проверяем доступность NAS перед запросом с помощью прямой TCP-проверки + # Избегаем вызова is_online(), чтобы не создавать рекурсию + online_status = self._check_tcp_connection() + if not online_status: + logger.error("Cannot login: Synology NAS is not reachable") + return False + + # Пробуем различные версии API для аутентификации + # Начинаем с версии 3, которая показала лучшую совместимость в тестах + auth_versions_to_try = [3, 2, 1, 6] # Пробуем сначала более стабильные версии + + for auth_version in auth_versions_to_try: + try: + # Определяем путь к API аутентификации + auth_path = "entry.cgi" # Используем entry.cgi вместо auth.cgi по умолчанию + + # Проверка информации API для определения доступных версий API + api_info_url = f"{self.base_url}/entry.cgi" + api_info_params = { + "api": "SYNO.API.Info", + "version": "1", + "method": "query", + "query": "SYNO.API.Auth" + } + + logger.debug(f"Querying API info for auth version {auth_version}") + try: + api_info_response = self.session.get( + api_info_url, + params=api_info_params, + timeout=self.default_timeout, + verify=False + ) + + if api_info_response.status_code == 200: + api_info_data = api_info_response.json() + if api_info_data.get("success"): + auth_info = api_info_data.get("data", {}).get("SYNO.API.Auth", {}) + max_version = auth_info.get("maxVersion", 6) + min_version = auth_info.get("minVersion", 1) + auth_path = auth_info.get("path", "entry.cgi") + logger.debug(f"Auth API versions: min={min_version}, max={max_version}, path={auth_path}") + + # Проверяем поддержку текущей версии + if auth_version < min_version or auth_version > max_version: + logger.warning(f"Auth API version {auth_version} not supported (min={min_version}, max={max_version}), trying next version") + continue + else: + logger.warning("Failed to query API info, using default auth path") + else: + logger.warning(f"API info request failed with status {api_info_response.status_code}, using default auth path") + except Exception as e: + logger.warning(f"Error querying API info: {str(e)}, using default auth path") + + # Основной запрос авторизации + url = f"{self.base_url}/{auth_path}" + params = { + "api": "SYNO.API.Auth", + "version": str(auth_version), + "method": "login", + "account": SYNOLOGY_USERNAME, + "passwd": SYNOLOGY_PASSWORD, + "session": "SynologyPowerControlBot", + "format": "cookie" + } + + # Для версии 6+ используем немного другой формат + if auth_version >= 6: + params["enable_syno_token"] = "yes" + + logger.debug(f"Sending auth request to {url} with API version {auth_version}") + start_time = time.time() + response = self.session.get( + url, + params=params, + timeout=self.default_timeout, + verify=False + ) + elapsed_time = time.time() - start_time + logger.debug(f"Auth request completed in {elapsed_time:.2f}s with status code {response.status_code}") + + # Проверка статуса HTTP + if response.status_code != 200: + logger.error(f"HTTP error: {response.status_code}") + logger.debug(f"Response content: {response.text[:200]}") + continue # Пробуем следующую версию + + try: + data = response.json() + except json.JSONDecodeError: + logger.error("Failed to decode JSON response") + logger.debug(f"Response content: {response.text[:200]}") + continue # Пробуем следующую версию + + if data.get("success"): + self.sid = data.get("data", {}).get("sid") + self._last_auth_time = time.time() + logger.info(f"Successfully logged in to Synology NAS using API version {auth_version}") + logger.debug(f"Session ID obtained: {self.sid[:5] if self.sid else 'None'}...") + + # Получаем и сохраняем токен SYNO, если он есть + syno_token = data.get("data", {}).get("synotoken") + if syno_token: + self.session.headers.update({'X-SYNO-TOKEN': syno_token}) + logger.debug("Added X-SYNO-TOKEN header for improved API compatibility") + + # Также добавляем SID в cookies для улучшения совместимости + self.session.cookies.update({ + 'id': self.sid, + 'sid': self.sid + }) + logger.debug("Added SID to session cookies for improved compatibility") + + # Проверка валидности полученной сессии с помощью простого запроса + # Будем использовать SYNO.API.Info без проверки сложных методов + + # Даем системе немного времени для инициализации сессии + time.sleep(0.5) + + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to log in with version {auth_version}: Error code {error_code}") + + # Если ошибка связана с версией API, пробуем следующую версию + if error_code in [104, 105]: + logger.warning(f"Auth version {auth_version} not supported, trying next version") + continue + + # Дополнительная диагностика + if error_code == 400: + logger.error("Authentication error: Invalid credentials") + elif error_code == 401: + logger.error("Authentication error: Account disabled") + elif error_code == 402: + logger.error("Authentication error: Permission denied") + elif error_code == 403: + logger.error("Authentication error: 2-factor authentication required") + elif error_code == 404: + logger.error("Authentication error: Failed to authenticate with 2-factor authentication") + + # Если ошибка связана с неверными учетными данными, нет смысла пробовать другие версии API + if error_code in [400, 401, 402, 403, 404]: + return False + + except requests.exceptions.Timeout: + logger.error(f"Connection timeout during auth with version {auth_version}") + continue # Пробуем следующую версию + except requests.exceptions.ConnectionError as e: + logger.error(f"Connection error during auth with version {auth_version}: {str(e)}") + continue # Пробуем следующую версию + except requests.RequestException as e: + logger.error(f"Request error during auth with version {auth_version}: {str(e)}") + continue # Пробуем следующую версию + except Exception as e: + logger.error(f"Unexpected error during login with version {auth_version}: {str(e)}", exc_info=True) + continue # Пробуем следующую версию + + # Если все версии не сработали + logger.error("Failed to authenticate with any API version") + return False + + def _validate_session(self) -> bool: + """Проверяет валидность сессии после авторизации""" + if not self.sid: + return False + + # Попробуем сделать простой запрос для проверки сессии + test_apis = [ + {"api": "SYNO.Core.System", "method": "info", "version": 1}, + {"api": "SYNO.DSM.Info", "method": "getinfo", "version": 1} + ] + + for test_api in test_apis: + try: + url = f"{self.base_url}/entry.cgi" + params = { + "api": test_api["api"], + "version": str(test_api["version"]), + "method": test_api["method"], + "_sid": self.sid + } + + response = self.session.get(url, params=params, timeout=self.default_timeout, verify=False) + + if response.status_code == 200: + data = response.json() + if data.get("success"): + logger.debug(f"Session validation successful using {test_api['api']}") + return True + else: + error_code = data.get("error", {}).get("code", -1) + if error_code != 119: # Не сессия истекла + logger.debug(f"Session validation with {test_api['api']} returned error code {error_code}") + return True # Считаем сессию валидной, если ошибка не связана с истечением сессии + except Exception as e: + logger.warning(f"Error during session validation with {test_api['api']}: {str(e)}") + + logger.warning("Session validation failed with all test APIs") + return False + + def logout(self) -> bool: + """Выход из API Synology NAS""" + if not self.sid: + return True + + try: + url = f"{self.base_url}/auth.cgi" + params = { + "api": "SYNO.API.Auth", + "version": "1", + "method": "logout", + "session": "SynologyPowerControlBot", + "_sid": self.sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + self.sid = None + logger.info("Successfully logged out from Synology NAS") + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to log out from Synology NAS: Error code {error_code}") + return False + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + return False + + def _make_api_request(self, api_name: str, method: str, version: int = 1, + params: Optional[Dict[str, Any]] = None, retry_auth: bool = True, retry_count: int = 0) -> Optional[Dict[str, Any]]: + """Обобщенный метод для выполнения API запросов с обработкой ошибок""" + # Ограничение на количество повторных попыток + if retry_count >= 3: + logger.error(f"Too many retries for {api_name}.{method}, giving up") + return None + + # Проверка наличия авторизации + if not self.sid and not self.login(): + logger.error(f"Not authenticated for API request: {api_name}.{method}") + return None + + # Проверка информации API для определения пути и поддерживаемой версии + api_info_url = f"{self.base_url}/entry.cgi" + api_info_params = { + "api": "SYNO.API.Info", + "version": "1", + "method": "query", + "query": api_name + } + + api_path = "entry.cgi" + try: + logger.debug(f"Querying API info for {api_name}") + api_info_response = self.session.get( + api_info_url, + params=api_info_params, + timeout=self.default_timeout, + verify=False + ) + + if api_info_response.status_code == 200: + api_info_data = api_info_response.json() + if api_info_data.get("success"): + api_info = api_info_data.get("data", {}).get(api_name, {}) + if api_info: + max_version = api_info.get("maxVersion", version) + min_version = api_info.get("minVersion", version) + api_path = api_info.get("path", "entry.cgi") + + # Проверка, поддерживается ли запрошенная версия + if version < min_version: + logger.warning(f"API version {version} for {api_name} is below minimum {min_version}, using {min_version}") + version = min_version + elif version > max_version: + logger.warning(f"API version {version} for {api_name} exceeds maximum {max_version}, using {max_version}") + version = max_version + + logger.debug(f"Using API path: {api_path}, version: {version}") + else: + logger.warning(f"API {api_name} not found in API info, using defaults") + except Exception as e: + logger.warning(f"Error querying API info for {api_name}: {str(e)}, using defaults") + + # Подготовка базовых параметров запроса + base_params = { + "api": api_name, + "version": str(version), + "method": method, + "_sid": self.sid # Используем _sid вместо sid для лучшей совместимости + } + + # Добавление дополнительных параметров, если они заданы + if params: + base_params.update(params) + + url = f"{self.base_url}/{api_path}" + logger.debug(f"API request: {api_name}.{method} v{version} to {url}") + logger.debug(f"Full request params: {base_params}") + + try: + start_time = time.time() + response = self.session.get( + url, + params=base_params, + timeout=self.default_timeout, + verify=False + ) + elapsed_time = time.time() - start_time + logger.debug(f"API request completed in {elapsed_time:.2f}s with status {response.status_code}") + + # Проверка статуса HTTP + if response.status_code != 200: + logger.error(f"HTTP error: {response.status_code} for {api_name}.{method}") + + # Повторная попытка при ошибках соединения + if response.status_code in [500, 502, 503, 504]: + logger.info(f"Server error, retrying request for {api_name}.{method}") + sleep(2) + return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1) + + return None + + try: + data = response.json() + except json.JSONDecodeError: + logger.error(f"Failed to decode JSON response for {api_name}.{method}") + logger.debug(f"Response content: {response.text[:200]}") + + # Повторная попытка при ошибках декодирования + logger.info(f"JSON decode error, retrying request for {api_name}.{method}") + sleep(2) + return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1) + + if data.get("success"): + logger.info(f"API request successful for {api_name}.{method}") + return data.get("data", {}) + else: + error_code = data.get("error", {}).get("code", -1) + error_desc = self._get_error_description(error_code) + logger.error(f"API error for {api_name}.{method}: {error_code} - {error_desc}") + + # Ошибки доступа или прав часто встречаются, но они не критичные + # Например, ошибка 102 означает, что нет прав, но NAS доступен + if error_code in [102, 103, 104, 105]: + logger.warning(f"Permission-related error for {api_name}.{method}: {error_code}") + # Возвращаем пустой словарь вместо None, + # чтобы вызывающий код мог понять, что запрос выполнен + return {} + + # Если ошибка связана с авторизацией и нам разрешено повторить попытку + if error_code in [106, 107, 119] and retry_auth: + logger.info(f"Session error (code {error_code}), creating fresh session...") + self.sid = None # Сбрасываем SID + + # Для ошибки 119 (Session timeout) дадим системе немного времени + if error_code == 119: + logger.info("Session timeout detected, waiting before retry...") + sleep(3) + + if self.login(): + logger.info("Re-authenticated with fresh session, retrying API request...") + # Рекурсивный вызов, но со счетчиком повторов + return self._make_api_request(api_name, method, version, params, False, retry_count + 1) + + # Для некоторых ошибок можно автоматически повторить запрос + if error_code in [408, 429, 500, 502, 503, 504]: + logger.info(f"Temporary error {error_code}, retrying request for {api_name}.{method}") + sleep(2) + return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1) + + return None + + except requests.exceptions.Timeout: + logger.error(f"Request timeout for {api_name}.{method}") + + # Повторная попытка при таймауте + if retry_count < 2: + logger.info(f"Timeout, retrying request for {api_name}.{method}") + sleep(2) + return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1) + + return None + except requests.exceptions.ConnectionError as e: + logger.error(f"Connection error for {api_name}.{method}: {str(e)}") + + # Повторная попытка при ошибке соединения + if retry_count < 2: + logger.info(f"Connection error, retrying request for {api_name}.{method}") + sleep(2) + return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1) + + return None + except Exception as e: + logger.error(f"Unexpected error during {api_name}.{method}: {str(e)}") + return None + + def get_system_status(self) -> Dict[str, Any]: + """Получение статуса системы""" + # Проверяем доступность системы + if not self.is_online(): + logger.info("Device is offline, skipping API request") + return {"status": "offline"} + + # Проверяем, есть ли кэшированный результат + cache_key = "system_status" + current_time = time.time() + if cache_key in self._cache and current_time - self._cache_ttl.get(cache_key, 0) < 60: + logger.debug("Using cached system status") + return self._cache[cache_key] + + # Используем рекомендованный API для получения информации о системе + logger.info(f"Getting system info using {SYNOLOGY_INFO_API} v{SYNOLOGY_API_VERSION}") + + # Для SYNO.DSM.Info метод getinfo, для других обычно используется get или info + if SYNOLOGY_INFO_API == "SYNO.DSM.Info": + method = "getinfo" + else: + method = "get" + + result = self._make_api_request(SYNOLOGY_INFO_API, method, version=SYNOLOGY_API_VERSION) + + if result: + logger.info(f"Successfully retrieved system info using {SYNOLOGY_INFO_API}") + + # Формируем расширенный ответ с дополнительной информацией + system_info = { + "status": "online", + "hostname": result.get("hostname", "unknown"), + "model": result.get("model", "unknown"), + "version": result.get("version", "unknown"), + "uptime": result.get("uptime", 0), + "time": current_time, + "is_online": True, + "api_used": SYNOLOGY_INFO_API + } + + # Сохраняем в кэше + self._cache[cache_key] = system_info + self._cache_ttl[cache_key] = current_time + + return system_info + + # Если основной API не сработал, пробуем резервные варианты + logger.warning(f"Failed to retrieve system info with {SYNOLOGY_INFO_API}, trying fallback APIs") + + # Пробуем резервные API + apis_to_try = [ + {"name": "SYNO.DSM.Info", "method": "getinfo", "version": 2}, + {"name": "SYNO.Core.System", "method": "info", "version": 1}, + {"name": "SYNO.Core.System.Status", "method": "get", "version": 1}, + {"name": "SYNO.Core.System.Info", "method": "get", "version": 1}, + ] + + for api in apis_to_try: + if api["name"] == SYNOLOGY_INFO_API: + continue # Пропускаем уже проверенный API + + logger.info(f"Trying to get system info using {api['name']}.{api['method']} v{api['version']}") + result = self._make_api_request(api["name"], api["method"], version=api["version"]) + + if result: + logger.info(f"Successfully retrieved system info using {api['name']}") + + # Формируем расширенный ответ с дополнительной информацией + system_info = { + "status": "online", + "hostname": result.get("hostname", "unknown"), + "model": result.get("model", "unknown"), + "version": result.get("version", "unknown"), + "uptime": result.get("uptime", 0), + "time": current_time, + "is_online": True, + "api_used": api["name"] + } + + # Сохраняем в кэше + self._cache[cache_key] = system_info + self._cache_ttl[cache_key] = current_time + + return system_info + + # Если все запросы не удались, но система онлайн, возвращаем базовую информацию + logger.warning("Failed to retrieve system info with all API methods") + return { + "status": "error", + "error": "Failed to fetch system information", + "is_online": True, + "time": current_time + } + + def shutdown_system(self) -> bool: + """Выключение системы""" + # Проверяем, включено ли устройство перед попыткой его выключить + if not self.is_online(force_check=True): + logger.info("Synology NAS is already offline, no need to shut down") + return True + + logger.info("Attempting to shutdown Synology NAS...") + + # Попробуем сначала использовать предпочтительный API для управления питанием + logger.info(f"Trying shutdown with {SYNOLOGY_POWER_API} v{SYNOLOGY_API_VERSION}") + + # Для SYNO.Core.Hardware.PowerRecovery используется метод setPowerOnState + # Для других API обычно используется метод shutdown или reboot + if SYNOLOGY_POWER_API == "SYNO.Core.Hardware.PowerRecovery": + # Для этого API нужны специальные параметры + params = {"state": "powerbtn"} # powerbtn имитирует нажатие кнопки питания + result = self._make_api_request(SYNOLOGY_POWER_API, "setPowerOnState", version=SYNOLOGY_API_VERSION, params=params) + else: + # Пробуем стандартный метод + result = self._make_api_request(SYNOLOGY_POWER_API, "shutdown", version=SYNOLOGY_API_VERSION) + + if result is not None: + logger.info(f"Successfully initiated system shutdown using {SYNOLOGY_POWER_API}") + + # Даем системе время начать процесс выключения + logger.info("Waiting for shutdown to initialize...") + sleep(5) + + # Проверяем статус + if not self.is_online(force_check=True): + logger.info("System is now offline. Shutdown confirmed successful.") + return True + else: + logger.info("System still appears to be online, but shutdown may be in progress.") + return True + else: + logger.warning(f"Failed to shutdown using {SYNOLOGY_POWER_API}, trying alternative methods") + + # Если не сработал основной метод, пробуем резервные варианты + # Проверка всех доступных методов API для выключения + apis_to_try = [ + {"name": "SYNO.Core.System", "method": "shutdown", "version": 3}, + {"name": "SYNO.Core.System.Power", "method": "shutdown", "version": 1}, + {"name": "SYNO.DSM.Power", "method": "shutdown", "version": 1}, + {"name": "SYNO.System.Power", "method": "shutdown", "version": 1}, + {"name": "SYNO.DSM.System", "method": "shutdown", "version": 1} + ] + + # Проверяем доступные API + try: + api_info_url = f"{self.base_url}/entry.cgi" + api_info_params = { + "api": "SYNO.API.Info", + "version": "1", + "method": "query", + "query": "SYNO.Core.System.Power,SYNO.DSM.Power,SYNO.Core.System,SYNO.System.Power,SYNO.DSM.System" + } + + logger.debug("Checking available shutdown APIs") + api_info_response = self.session.get( + api_info_url, + params=api_info_params, + timeout=self.default_timeout, + verify=False + ) + + if api_info_response.status_code == 200: + api_info_data = api_info_response.json() + if api_info_data.get("success"): + available_apis = api_info_data.get("data", {}) + logger.debug(f"Available APIs: {list(available_apis.keys())}") + + # Фильтруем только доступные API + filtered_apis = [] + for api in apis_to_try: + if api["name"] in available_apis: + api_info = available_apis[api["name"]] + # Проверка версии API + if api_info.get("minVersion", 1) <= api["version"] <= api_info.get("maxVersion", 1): + filtered_apis.append(api) + logger.debug(f"Adding {api['name']} to available shutdown APIs") + else: + logger.debug(f"API {api['name']} version {api['version']} not supported. Supported: {api_info.get('minVersion')}-{api_info.get('maxVersion')}") + + if filtered_apis: + apis_to_try = filtered_apis + else: + logger.warning("No compatible APIs found, trying all methods as fallback") + else: + logger.warning("Failed to query API info, using default methods") + else: + logger.warning(f"API info request failed with status {api_info_response.status_code}, using default methods") + except Exception as e: + logger.warning(f"Error querying API info: {str(e)}, using default methods") + + # Пробуем все доступные методы по порядку + for api in apis_to_try: + logger.info(f"Trying shutdown with {api['name']}.{api['method']} v{api['version']}") + result = self._make_api_request(api["name"], api["method"], version=api["version"]) + + if result is not None: + logger.info(f"Successfully initiated system shutdown using {api['name']}") + + # Даем системе время начать процесс выключения + logger.info("Waiting for shutdown to initialize...") + sleep(5) + + # Проверяем статус + if not self.is_online(force_check=True): + logger.info("System is now offline. Shutdown confirmed successful.") + return True + else: + logger.info("System still appears to be online, but shutdown may be in progress.") + return True + else: + logger.warning(f"Failed to shutdown using {api['name']}.{api['method']} v{api['version']}") + + # Если ни один метод не сработал, но система стала недоступна + if not self.is_online(force_check=True): + logger.info("System appears to be shutting down despite API errors") + return True + + logger.error("Failed to shutdown system after trying multiple APIs") + return False + + def reboot_system(self) -> bool: + """Перезагрузка системы""" + # Проверяем, включена ли система + if not self.is_online(force_check=True): + logger.error("Cannot reboot: System is offline") + return False + + logger.info("Attempting to reboot Synology NAS...") + + # Список API и методов для попытки перезагрузки + apis_to_try = [ + {"api": "SYNO.Core.System", "method": "reboot", "version": 1}, + {"api": "SYNO.DSM.System", "method": "reboot", "version": 1}, + ] + + # Добавляем настроенный в конфигурации API, если он отличается от уже добавленных + if SYNOLOGY_POWER_API not in ["SYNO.Core.System", "SYNO.DSM.System"]: + for method in ["restart", "reboot"]: + apis_to_try.append({ + "api": SYNOLOGY_POWER_API, + "method": method, + "version": SYNOLOGY_API_VERSION + }) + + # Перебираем все возможные API и методы + for api_info in apis_to_try: + try: + logger.info(f"Trying reboot with {api_info['api']} API using method {api_info['method']}") + result = self._make_api_request(api_info['api'], api_info['method'], version=api_info['version']) + if result is not None: + logger.info(f"Successfully initiated system reboot using {api_info['api']} API") + + # Даем системе время начать процесс перезагрузки + logger.info("Waiting for reboot to initialize...") + sleep(5) + + return True + except Exception as e: + logger.error(f"Error during reboot with {api_info['api']}: {str(e)}") + + # Если все попытки не удались, возвращаем False + logger.error("All reboot attempts failed") + return False + break + + if result is not None: + logger.info(f"Successfully initiated system reboot using {SYNOLOGY_POWER_API}") + + # Даем системе время начать процесс перезагрузки + logger.info("Waiting for reboot to initialize...") + sleep(5) + + # Ждем, пока система станет недоступна (признак перезагрузки) + reboot_started = False + for i in range(12): # Проверяем в течение 60 секунд (12 * 5сек) + if not self.is_online(force_check=True): + logger.info(f"System went offline after {i*5} seconds, reboot in progress") + reboot_started = True + break + logger.debug(f"System still online, waiting... ({i+1}/12)") + sleep(5) + + if reboot_started: + # Ждем, пока система снова станет доступна + logger.info("Waiting for system to come back online...") + return self.wait_for_boot(max_attempts=30, delay=10) + else: + logger.warning("System did not go offline after reboot command, but command was accepted") + # Даже если система не ушла оффлайн, команда могла быть принята + return True + else: + logger.warning(f"Failed to reboot using {SYNOLOGY_POWER_API}, trying alternative methods") + + # Если не сработал основной метод, пробуем резервные варианты + # Проверка всех доступных методов API для перезагрузки + apis_to_try = [ + {"name": "SYNO.Core.System.Power", "method": "restart", "version": 1}, + {"name": "SYNO.DSM.Power", "method": "reboot", "version": 1}, + {"name": "SYNO.Core.System", "method": "reboot", "version": 3}, + {"name": "SYNO.System.Power", "method": "reboot", "version": 1}, + {"name": "SYNO.DSM.System", "method": "reboot", "version": 1}, + {"name": "SYNO.Core.Hardware.NeedReboot", "method": "reboot", "version": 1} + ] + + # Получаем список доступных API + try: + api_info_url = f"{self.base_url}/entry.cgi" + api_info_params = { + "api": "SYNO.API.Info", + "version": "1", + "method": "query", + "query": "SYNO.Core.System.Power,SYNO.DSM.Power,SYNO.Core.System,SYNO.System.Power,SYNO.DSM.System,SYNO.Core.Hardware.NeedReboot" + } + + logger.debug("Checking available reboot APIs") + api_info_response = self.session.get( + api_info_url, + params=api_info_params, + timeout=self.default_timeout, + verify=False + ) + + if api_info_response.status_code == 200: + api_info_data = api_info_response.json() + if api_info_data.get("success"): + available_apis = api_info_data.get("data", {}) + logger.debug(f"Available APIs: {list(available_apis.keys())}") + + # Фильтруем только доступные API + filtered_apis = [] + for api in apis_to_try: + if api["name"] in available_apis: + api_info = available_apis[api["name"]] + # Проверка версии API + if api_info.get("minVersion", 1) <= api["version"] <= api_info.get("maxVersion", 1): + filtered_apis.append(api) + logger.debug(f"Adding {api['name']} to available reboot APIs") + else: + logger.debug(f"API {api['name']} version {api['version']} not supported. Supported: {api_info.get('minVersion')}-{api_info.get('maxVersion')}") + + if filtered_apis: + apis_to_try = filtered_apis + else: + logger.warning("No compatible APIs found, trying all methods as fallback") + else: + logger.warning("Failed to query API info, using default methods") + else: + logger.warning(f"API info request failed with status {api_info_response.status_code}, using default methods") + except Exception as e: + logger.warning(f"Error querying API info: {str(e)}, using default methods") + + # Пробуем все доступные методы по порядку + for api in apis_to_try: + logger.info(f"Trying reboot with {api['name']}.{api['method']} v{api['version']}") + result = self._make_api_request(api["name"], api["method"], version=api["version"]) + + if result is not None: + logger.info(f"Successfully initiated system reboot using {api['name']}") + + # Даем системе время начать процесс перезагрузки + logger.info("Waiting for reboot to initialize...") + sleep(5) + + # Ждем, пока система станет недоступна (признак перезагрузки) + reboot_started = False + for i in range(12): # Проверяем в течение 60 секунд (12 * 5сек) + if not self.is_online(force_check=True): + logger.info(f"System went offline after {i*5} seconds, reboot in progress") + reboot_started = True + break + logger.debug(f"System still online, waiting... ({i+1}/12)") + sleep(5) + + if reboot_started: + # Ждем, пока система снова станет доступна + logger.info("Waiting for system to come back online...") + return self.wait_for_boot(max_attempts=30, delay=10) + else: + logger.warning("System did not go offline after reboot command, but command was accepted") + # Даже если система не ушла оффлайн, команда могла быть принята + return True + else: + logger.warning(f"Failed to reboot using {api['name']}.{api['method']} v{api['version']}") + + logger.error("Failed to reboot system after trying multiple APIs") + return False + + def _get_error_description(self, error_code: int) -> str: + """Получение описания ошибки по коду""" + error_descriptions = { + 100: "Unknown error", + 101: "Invalid parameter", + 102: "API does not exist", + 103: "Method does not exist", + 104: "Version does not support", + 105: "Permission denied", + 106: "Session timeout", + 107: "Session interrupted by duplicate login", + 400: "Invalid credentials", + 401: "Account disabled", + 402: "Permission denied", + 403: "2FA required", + 404: "Failed to authenticate with 2FA" + } + return error_descriptions.get(error_code, "Unknown error code") + + def _check_tcp_connection(self) -> bool: + """Проверка базового TCP-соединения с Synology NAS""" + try: + socket_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + socket_obj.settimeout(SYNOLOGY_TIMEOUT) + result = socket_obj.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT)) + socket_obj.close() + + return result == 0 + except socket.error as e: + logger.error(f"Socket error during connection check: {str(e)}") + return False + except Exception as e: + logger.error(f"Unexpected error during connection check: {str(e)}") + return False + + def is_online(self, force_check=False) -> bool: + """Проверка онлайн-статуса Synology NAS""" + # Используем кэшированное значение, если доступно и не устарело + current_time = time.time() + if not force_check and (current_time - self._last_online_check) < self._online_check_interval: + logger.debug(f"Using cached online status: {self._last_online_status}") + return self._last_online_status + + logger.info("Checking if NAS is online...") + + # Проверяем TCP-соединение + online_status = self._check_tcp_connection() + logger.info(f"Detected Synology NAS online status: {online_status}") + + # Если TCP-соединение успешно и у нас есть действующий SID, + # попробуем более детальную проверку через API + if online_status and self.sid: + logger.info("Trying to fetch more detailed online status through API...") + + # Пробуем разные API для проверки онлайн-статуса + api_checks = [ + {"api": "SYNO.DSM.Info", "version": "2", "method": "getinfo"}, + {"api": "SYNO.Core.System", "version": "1", "method": "info"}, + {"api": "SYNO.Core.System.Status", "version": "1", "method": "get"} + ] + + api_success = False + for api_check in api_checks: + try: + url = f"{self.base_url}/entry.cgi" + params = { + "api": api_check["api"], + "version": api_check["version"], + "method": api_check["method"], + "sid": self.sid + } + + logger.debug(f"Trying online status check with {api_check['api']}") + response = self.session.get( + url, + params=params, + timeout=self.default_timeout, + verify=False + ) + + if response.status_code == 200: + data = response.json() + if data.get("success"): + logger.info(f"API request successful for {api_check['api']}") + logger.info("Synology NAS is online with API access") + api_success = True + break + else: + error_code = data.get("error", {}).get("code", -1) + logger.warning(f"API response indicates an error: {error_code}, but NAS is reachable") + else: + logger.warning(f"API returned status code {response.status_code}") + except Exception as e: + logger.warning(f"API check failed for {api_check['api']}, but TCP connection succeeded: {str(e)}") + + if not api_success: + logger.warning("All API checks failed, but TCP connection is successful") + + # Обновляем кэшированное значение + self._last_online_check = current_time + self._last_online_status = online_status + + return online_status + + def wake_on_lan(self) -> bool: + """Отправка Wake-on-LAN пакета для включения Synology NAS""" + if not SYNOLOGY_MAC: + logger.error("MAC address not configured") + return False + + try: + # Преобразование MAC-адреса в байты + mac_address = SYNOLOGY_MAC.replace(':', '').replace('-', '') + if len(mac_address) != 12: + logger.error(f"Invalid MAC address format: {SYNOLOGY_MAC}") + return False + + try: + mac_bytes = bytes.fromhex(mac_address) + except ValueError as e: + logger.error(f"Failed to parse MAC address: {str(e)}") + logger.error(f"MAC address must contain only hex characters: {SYNOLOGY_MAC}") + return False + + # Создание Magic Packet + magic_packet = b'\xff' * 6 + mac_bytes * 16 + + # Отправка пакета на конкретный адрес + try: + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) + sock.sendto(magic_packet, (SYNOLOGY_HOST, WOL_PORT)) + sock.close() + logger.info(f"Wake-on-LAN packet sent to {SYNOLOGY_MAC} at {SYNOLOGY_HOST}:{WOL_PORT}") + except Exception as e: + logger.error(f"Error sending directed WoL packet: {str(e)}") + return False + + # Для надежности отправляем также широковещательный пакет + try: + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) + + # Используем стандартный широковещательный адрес + broadcast_addr = "255.255.255.255" + sock.sendto(magic_packet, (broadcast_addr, WOL_PORT)) + sock.close() + logger.info(f"Wake-on-LAN broadcast packet sent to {broadcast_addr}:{WOL_PORT}") + except Exception as e: + logger.warning(f"Error sending broadcast WoL packet (non-critical): {str(e)}") + # Не считаем ошибкой, т.к. основной пакет уже отправлен + + return True + + except Exception as e: + logger.error(f"Unexpected error in wake_on_lan: {str(e)}") + return False + + def wait_for_boot(self, max_attempts: int = 30, delay: int = 10) -> bool: + """Ожидание загрузки Synology NAS""" + logger.info(f"Waiting for Synology NAS to boot (timeout: {max_attempts * delay}s)...") + + for attempt in range(max_attempts): + # Принудительно проверяем статус без использования кэша + if self.is_online(force_check=True): + logger.info(f"Synology NAS is online after {attempt + 1} attempts ({(attempt + 1) * delay}s)") + + # Проверяем, что не только сеть доступна, но и API загрузился + api_ready = False + logger.info("Waiting for API services to initialize...") + + for api_check in range(5): # Даем еще до 50 секунд для загрузки API + if self.sid or self.login(): + api_ready = True + logger.info(f"API services are ready after {api_check + 1} attempts") + break + logger.debug(f"API not ready yet, waiting... ({api_check + 1}/5)") + sleep(10) + + if not api_ready: + logger.warning("System is online but API services may not be fully initialized") + + # Дадим дополнительное время для полной загрузки всех сервисов + sleep(delay) + return True + + sleep(delay) + logger.info(f"Waiting for boot... Attempt {attempt + 1}/{max_attempts}") + + logger.error(f"Synology NAS didn't come online after {max_attempts * delay} seconds") + return False + + def power_on(self) -> bool: + """Включение Synology NAS""" + # Принудительная проверка статуса + if self.is_online(force_check=True): + logger.info("Synology NAS is already online") + return True + + logger.info("Powering on Synology NAS via Wake-on-LAN...") + + # Проверяем, настроен ли MAC-адрес + if not SYNOLOGY_MAC: + logger.error("Cannot power on: MAC address not configured in settings") + return False + + # Пробуем отправить несколько WoL пакетов для надежности + success = False + for attempt in range(3): + logger.debug(f"Sending WoL packet, attempt {attempt + 1}/3") + if self.wake_on_lan(): + success = True + break + sleep(1) + + if not success: + logger.error("Failed to send Wake-on-LAN packets") + return False + + # Ожидание загрузки + logger.info("WoL packets sent successfully, waiting for system to boot...") + boot_result = self.wait_for_boot(max_attempts=30, delay=10) + + if boot_result: + # Проверяем доступность API после загрузки + system_status = self.get_system_status() + if system_status.get("status") == "online": + logger.info("System booted successfully with API access") + return True + else: + logger.warning("System appears to be online but API may not be fully ready") + return True + else: + logger.error("System did not come online after WoL") + return False + + def power_off(self) -> bool: + """Выключение Synology NAS""" + if not self.is_online(force_check=True): + logger.info("Synology NAS is already offline") + return True + + logger.info("Powering off Synology NAS...") + + # Список API и методов для попытки выключения + apis_to_try = [ + {"api": "SYNO.Core.System", "method": "shutdown", "version": 1}, + {"api": "SYNO.DSM.System", "method": "shutdown", "version": 1}, + {"api": SYNOLOGY_POWER_API, "method": "shutdown", "version": SYNOLOGY_API_VERSION} + ] + + # Перебираем все возможные API и методы + for api_info in apis_to_try: + try: + logger.info(f"Trying shutdown with {api_info['api']} API using method {api_info['method']}") + api_result = self._make_api_request(api_info['api'], api_info['method'], version=api_info['version']) + if api_result is not None: + logger.info(f"Successfully initiated system shutdown using {api_info['api']} API") + + # Даем системе время начать процесс выключения + logger.info("Waiting for shutdown to initialize...") + sleep(5) + + return True + except Exception as e: + logger.error(f"Error during shutdown with {api_info['api']}: {str(e)}") + + # Если все попытки не удались, возвращаем False + logger.error("All shutdown attempts failed") + return False + + # Если все еще не сработало, используем оригинальный метод shutdown_system + if not result: + result = self.shutdown_system() + + if result: + # Дополнительная проверка, что система действительно выключилась + logger.info("Verifying system is offline...") + for attempt in range(12): # Проверяем в течение 2 минут (12 * 10 сек) + if not self.is_online(force_check=True): + logger.info(f"System confirmed offline after {attempt * 10} seconds") + return True + logger.debug(f"System still shutting down, checking again... ({attempt + 1}/12)") + sleep(10) + + logger.warning("System still appears to be online after shutdown command") + return False + else: + logger.error("Failed to initiate shutdown") + return False + + # Заглушки для расширенных методов + def get_shared_folders(self) -> List[Dict[str, Any]]: + """Получение списка общих папок""" + logger.info("Getting list of shared folders") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for shared folders request") + return [] + + try: + # Запрашиваем список общих папок через FileStation API + result = self._make_api_request("SYNO.FileStation.List", "list_share", version=2) + + if not result: + # Пробуем альтернативный API + logger.info("Trying alternative API for shared folders") + alt_result = self._make_api_request("SYNO.FileStation.List", "list_share", version=1) + if alt_result: + return alt_result.get("shares", []) + return [] + + return result.get("shares", []) + + except Exception as e: + logger.error(f"Error getting shared folders: {str(e)}") + return [] + + def get_system_load(self) -> Dict[str, Any]: + """Получение информации о загрузке системы""" + logger.info("Getting system load information") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for system load request") + return {} + + try: + # Запрашиваем информацию о загрузке системы + result = self._make_api_request("SYNO.Core.System.Utilization", "get", version=1) + + if not result: + # Пробуем альтернативный API + logger.info("Trying alternative API for system load") + alt_result = self._make_api_request("SYNO.Core.System", "info", version=3) + + if not alt_result: + return {} + + # Формируем из частичных данных + return { + "cpu_load": alt_result.get("cpu_usage", 0), + "memory": { + "total": alt_result.get("memory_size", 0), + "used": alt_result.get("memory_usage", 0), + "usage_percent": alt_result.get("memory_usage_percent", 0) + } + } + + # Формируем структурированный результат + return { + "cpu_load": result.get("cpu", {}).get("user", 0) + result.get("cpu", {}).get("system", 0), + "memory": result.get("memory", {}), + "network": result.get("network", {}) + } + + except Exception as e: + logger.error(f"Error getting system load: {str(e)}") + return {} + + def is_online_api(self) -> bool: + """Проверка онлайн-статуса Synology NAS с использованием API""" + if not self.is_online(): + return False + + # Проверяем доступность API через авторизацию + if not self.sid and not self.login(): + return False + + return True + + def get_storage_status(self) -> Dict[str, Any]: + """Получение подробной информации о хранилище""" + logger.info("Getting storage status information") + + # Проверяем доступность NAS и API + if not self.is_online_api(): + logger.error("Cannot get storage status: NAS is not online or API is not accessible") + return {"error": "authentication_failed"} + + try: + # Пробуем получить информацию о хранилище через SYNO.Storage.CGI.Storage API + result = self._make_api_request("SYNO.Storage.CGI.Storage", "load_info", version=1) + + if not result: + # Пробуем альтернативный API + logger.info("Trying alternative API for storage info") + alt_result = self._make_api_request("SYNO.Core.Storage", "info", version=1) + + if not alt_result: + # Пробуем еще один альтернативный API + logger.info("Trying SYNO.Core.System API for storage info") + sys_result = self._make_api_request("SYNO.Core.System", "info", version=3) + + if not sys_result: + return { + "volumes": [], + "disks": [], + "total_size": 0, + "total_used": 0, + "error": "no_data" + } + + # Извлекаем базовую информацию о хранилище из системной информации + return { + "volumes": [], + "disks": [], + "total_size": sys_result.get("hdd_total_size", 0) * 1024**2, # МБ в байты + "total_used": sys_result.get("hdd_total_size", 0) * sys_result.get("hdd_usage", 0) / 100 * 1024**2, + } + + # Обрабатываем данные из альтернативного API + volumes = alt_result.get("volumes", []) + disks = alt_result.get("disks", []) + + else: + # Обрабатываем данные из основного API + volumes = result.get("volumes", []) + disks = result.get("disks", []) + + # Рассчитываем общие размеры + total_size = 0 + total_used = 0 + + for volume in volumes: + volume_size = volume.get("size", {}).get("total", 0) + volume_used = volume.get("size", {}).get("used", 0) + + total_size += volume_size + total_used += volume_used + + return { + "volumes": volumes, + "disks": disks, + "total_size": total_size, + "total_used": total_used + } + + except Exception as e: + logger.error(f"Error in get_storage_status: {str(e)}") + return {"error": str(e)} + + def get_security_status(self) -> Dict[str, Any]: + """Получение информации о состоянии безопасности""" + logger.info("Getting security status information") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for security status request") + return {"success": False, "error": "authentication_failed"} + + try: + # Пробуем получить информацию о безопасности через API Security Scan + result = self._make_api_request("SYNO.Core.SecurityScan.Status", "get", version=1) + + if not result: + # Пробуем альтернативный API + logger.info("Trying alternative API for security status") + # Проверяем статус брандмауэра + firewall_result = self._make_api_request("SYNO.Core.Security.Firewall.Status", "get", version=1) + + # Проверяем статус автоматических обновлений + update_result = self._make_api_request("SYNO.Core.Upgrade.Server.Setting", "get", version=1) + + # Если ни один из API не отвечает + if not firewall_result and not update_result: + # Получаем общую информацию о системе для базовой проверки безопасности + sys_result = self._make_api_request("SYNO.DSM.Info", "getinfo", version=2) + + if not sys_result: + return { + "success": False, + "status": "unknown", + "last_check": None, + "is_secure": False, + "error": "no_security_api" + } + + # Собираем базовые сведения из системной информации + return { + "success": True, + "status": "basic", + "last_check": None, + "is_secure": True, # Предполагаем, что система в целом безопасна + "firewall_enabled": None, + "auto_update": None, + "version_latest": sys_result.get("version_string", "") + } + + # Собираем информацию из доступных результатов + firewall_enabled = firewall_result.get("enabled", False) if firewall_result else None + auto_update = update_result.get("auto_update", False) if update_result else None + + # Определяем, насколько система безопасна + is_secure = True # По умолчанию предполагаем, что система безопасна + if firewall_enabled is not None and not firewall_enabled: + is_secure = False + + return { + "success": True, + "status": "partial", + "last_check": None, + "is_secure": is_secure, + "firewall_enabled": firewall_enabled, + "auto_update": auto_update + } + + # Если основное API отвечает, возвращаем его данные + return { + "success": True, + "status": result.get("status", "unknown"), + "last_check": result.get("last_check", None), + "is_secure": result.get("is_secure", False), + "details": result.get("details", {}) + } + + except Exception as e: + logger.error(f"Error in get_security_status: {str(e)}") + return {"success": False, "error": str(e)} + + def get_processes(self, limit: int = 20) -> List[Dict[str, Any]]: + """Получение списка активных процессов""" + logger.info(f"Getting list of active processes (limit={limit})") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for processes request") + return [] + + try: + # Получаем список процессов через API + result = self._make_api_request("SYNO.Core.System.Process", "list", version=1, + params={"sort_by": "cpu", "order": "DESC", "limit": limit}) + + if not result: + logger.warning("Failed to get process list") + return [] + + return result.get("processes", []) + + except Exception as e: + logger.error(f"Error getting process list: {str(e)}") + return [] + + def get_network_status(self) -> Dict[str, Any]: + """Получение информации о сетевых подключениях""" + logger.info("Getting network status information") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for network status request") + return {} + + try: + # Получаем информацию о сетевых интерфейсах + interface_result = self._make_api_request("SYNO.Core.Network.Interface", "list", version=1) + + # Получаем статистику использования сети + utilization_result = self._make_api_request("SYNO.Core.System.Utilization", "get", version=1) + + interfaces = [] + if interface_result: + interfaces = interface_result.get("interfaces", []) + + network_stats = {} + if utilization_result and "network" in utilization_result: + network_stats = utilization_result.get("network", {}) + + # Объединяем данные + for interface in interfaces: + iface_id = interface.get("id", "") + if iface_id in network_stats: + interface["rx"] = network_stats[iface_id].get("rx", 0) + interface["tx"] = network_stats[iface_id].get("tx", 0) + + return { + "interfaces": interfaces, + "statistics": network_stats + } + + except Exception as e: + logger.error(f"Error getting network status: {str(e)}") + return {} + + def get_system_logs(self, limit: int = 20) -> List[Dict[str, Any]]: + """Получение журналов системы""" + logger.info(f"Getting system logs (limit={limit})") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for system logs request") + return [] + + try: + # Получаем журналы через API + result = self._make_api_request("SYNO.Core.SyslogClient.Log", "list", version=1, + params={"start": 0, "limit": limit, "sort": "time", "dir": "DESC"}) + + if not result: + # Пробуем альтернативный API + logger.info("Trying alternative API for system logs") + alt_result = self._make_api_request("SYNO.Core.Log", "list", version=1, + params={"start": 0, "limit": limit, "sort": "time", "dir": "DESC"}) + + if alt_result: + return alt_result.get("logs", []) + return [] + + return result.get("logs", []) + + except Exception as e: + logger.error(f"Error getting system logs: {str(e)}") + return [] + + def get_power_schedule(self) -> Dict[str, Any]: + """Получение расписания включения/выключения""" + logger.info("Getting power schedule") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for power schedule request") + return {} + + try: + # Получаем расписание через API + result = self._make_api_request("SYNO.Core.Hardware.PowerSchedule", "get", version=1) + + if not result: + return {} + + return result + + except Exception as e: + logger.error(f"Error getting power schedule: {str(e)}") + return {} + + def set_power_schedule(self, schedule_type: str, days: List[int], time: str, enabled: bool = True) -> bool: + """Настройка расписания включения/выключения + + Args: + schedule_type: Тип расписания ('boot' или 'shutdown') + days: Список дней недели (0-6, где 0 - понедельник) + time: Время в формате 'HH:MM' + enabled: Включить или выключить расписание + + Returns: + True если успешно, False в противном случае + """ + logger.info(f"Setting power schedule for {schedule_type} at {time} on days {days}") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for setting power schedule") + return False + + try: + # Получаем текущее расписание + current_schedule = self._make_api_request("SYNO.Core.Hardware.PowerSchedule", "get", version=1) + + if not current_schedule: + logger.error("Failed to get current power schedule") + return False + + # Подготавливаем новое расписание + params = { + "enabled": enabled, + "type": schedule_type, + "day": days, + "time": time + } + + # Устанавливаем новое расписание + result = self._make_api_request("SYNO.Core.Hardware.PowerSchedule", "set", version=1, params=params) + + if not result: + logger.error("Failed to set power schedule") + return False + + logger.info(f"Power schedule for {schedule_type} set successfully") + return True + + except Exception as e: + logger.error(f"Error setting power schedule: {str(e)}") + return False + + def get_temperature_status(self) -> Dict[str, Any]: + """Получение информации о температуре системы и дисков""" + logger.info("Getting temperature status") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for temperature status request") + return {} + + try: + # Получаем информацию о системе для общей температуры + system_info = self._make_api_request("SYNO.DSM.Info", "getinfo", version=2) + + # Получаем информацию о дисках для их температуры + storage_info = self._make_api_request("SYNO.Storage.CGI.Storage", "load_info", version=1) + + system_temp = None + disk_temps = [] + + if system_info: + system_temp = system_info.get("temperature") + + if storage_info: + disks = storage_info.get("disks", []) + for disk in disks: + name = disk.get("name", "unknown") + model = disk.get("model", "unknown") + temp = disk.get("temp", None) + if temp is not None: + disk_temps.append({ + "name": name, + "model": model, + "temperature": temp + }) + + return { + "system_temperature": system_temp, + "disk_temperatures": disk_temps, + "warning": system_info.get("temperature_warn", False) if system_info else False + } + + except Exception as e: + logger.error(f"Error getting temperature status: {str(e)}") + return {} + + def browse_files(self, folder_path: str = "", limit: int = 100) -> Dict[str, Any]: + """Просмотр файлов в указанной директории + + Args: + folder_path: Путь к папке (пустая строка для корневых общих папок) + limit: Максимальное количество элементов для возврата + + Returns: + Словарь с информацией о файлах и папках + """ + logger.info(f"Browsing files in {folder_path or 'root'}") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for file browsing") + return {"success": False, "error": "authentication_failed"} + + try: + if not folder_path: + # Если путь не указан, получаем список общих папок + result = self._make_api_request("SYNO.FileStation.List", "list_share", version=2) + + if not result: + logger.error("Failed to list shared folders") + return {"success": False, "error": "api_error"} + + return { + "success": True, + "items": result.get("shares", []), + "path": "", + "is_root": True + } + else: + # Получаем список файлов в указанной директории + params = { + "folder_path": folder_path, + "limit": limit, + "offset": 0, + "sort_by": "name", + "sort_direction": "ASC" + } + + result = self._make_api_request("SYNO.FileStation.List", "list", version=2, params=params) + + if not result: + logger.error(f"Failed to list files in {folder_path}") + return {"success": False, "error": "api_error"} + + return { + "success": True, + "items": result.get("files", []), + "path": folder_path, + "is_root": False, + "total": result.get("total", 0) + } + + except Exception as e: + logger.error(f"Error browsing files: {str(e)}") + return {"success": False, "error": str(e)} + + def manage_service(self, service_name: str, action: str = "status") -> Dict[str, Any]: + """Управление системным сервисом + + Args: + service_name: Имя сервиса + action: Действие (status/start/stop/restart) + + Returns: + Словарь с результатом операции + """ + logger.info(f"Managing service {service_name}, action: {action}") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for service management") + return {"success": False, "error": "authentication_failed"} + + try: + # Проверяем доступное API для управления сервисами + if action == "status": + result = self._make_api_request("SYNO.Core.Service", "get", version=1, + params={"service": service_name}) + else: + result = self._make_api_request("SYNO.Core.Service", action, version=1, + params={"service": service_name}) + + if not result: + logger.error(f"Failed to {action} service {service_name}") + return {"success": False, "error": "api_error"} + + return { + "success": True, + "service": service_name, + "action": action, + "result": result, + "status": result.get("status") if action == "status" else "completed" + } + + except Exception as e: + logger.error(f"Error managing service {service_name}: {str(e)}") + return {"success": False, "error": str(e)} + + def search_files(self, pattern: str, folder_path: str = "", limit: int = 100) -> Dict[str, Any]: + """Поиск файлов по шаблону + + Args: + pattern: Шаблон для поиска + folder_path: Путь к папке для поиска (пустая строка для всех общих папок) + limit: Максимальное количество результатов + + Returns: + Словарь с найденными файлами + """ + logger.info(f"Searching for {pattern} in {folder_path or 'all shared folders'}") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for file search") + return {"success": False, "error": "authentication_failed"} + + try: + if not folder_path: + # Получаем список всех общих папок для поиска + shares_result = self._make_api_request("SYNO.FileStation.List", "list_share", version=2) + + if not shares_result: + logger.error("Failed to list shared folders for search") + return {"success": False, "error": "api_error"} + + # Формируем список путей для поиска + folder_paths = [share.get("path") for share in shares_result.get("shares", [])] + else: + folder_paths = [folder_path] + + # Запускаем поиск + params = { + "folder_path": folder_paths, + "pattern": pattern, + "limit": limit, + "offset": 0 + } + + result = self._make_api_request("SYNO.FileStation.Search", "start", version=2, params=params) + + if not result: + logger.error(f"Failed to start search for {pattern}") + return {"success": False, "error": "api_error"} + + # Получаем taskid для проверки результатов + taskid = result.get("taskid") + if not taskid: + logger.error("No taskid received for search") + return {"success": False, "error": "no_task_id"} + + # Ожидаем завершения поиска + search_result = {"finished": False, "progress": 0} + for _ in range(10): # Максимум 10 попыток + search_status = self._make_api_request("SYNO.FileStation.Search", "status", version=2, + params={"taskid": taskid}) + + if not search_status: + break + + search_result["progress"] = search_status.get("progress", 0) + + if search_status.get("finished", False): + search_result["finished"] = True + break + + time.sleep(0.5) # Пауза между запросами + + # Получаем результаты поиска + if search_result["finished"]: + list_result = self._make_api_request("SYNO.FileStation.Search", "list", version=2, + params={"taskid": taskid, "limit": limit}) + + if list_result: + files = list_result.get("files", []) + return { + "success": True, + "pattern": pattern, + "results": files, + "total": list_result.get("total", len(files)) + } + + # Если не удалось получить результаты, останавливаем поиск + self._make_api_request("SYNO.FileStation.Search", "stop", version=2, + params={"taskid": taskid}) + + return { + "success": False, + "error": "search_timeout", + "progress": search_result["progress"] + } + + except Exception as e: + logger.error(f"Error searching files: {str(e)}") + return {"success": False, "error": str(e)} + + def get_backup_status(self) -> Dict[str, Any]: + """Получение информации о резервном копировании""" + logger.info("Getting backup status information") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for backup status request") + return {"success": False, "error": "authentication_failed"} + + try: + # Пробуем получить информацию о Hyper Backup + hyper_result = self._make_api_request("SYNO.Backup.Repository", "list", version=1) + + # Пробуем получить информацию о задачах Time Backup + time_result = self._make_api_request("SYNO.BackupService.Task", "list", version=1) + + # Проверяем статус резервного копирования USB + usb_result = self._make_api_request("SYNO.USBCopy", "status", version=1) + + backups = { + "hyper_backup": hyper_result.get("backups", []) if hyper_result else [], + "time_backup": time_result.get("tasks", []) if time_result else [], + "usb_copy": {"enabled": usb_result.get("enable", False) if usb_result else False} + } + + return { + "success": True, + "backups": backups, + "available_apis": { + "hyper_backup": hyper_result is not None, + "time_backup": time_result is not None, + "usb_copy": usb_result is not None + } + } + + except Exception as e: + logger.error(f"Error getting backup status: {str(e)}") + return {"success": False, "error": str(e)} + + def check_for_updates(self) -> Dict[str, Any]: + """Проверка наличия обновлений системы""" + logger.info("Checking for system updates") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for update check") + return {"success": False, "error": "authentication_failed"} + + try: + # Получаем текущую информацию о системе + system_info = self._make_api_request("SYNO.DSM.Info", "getinfo", version=2) + + if not system_info: + logger.error("Failed to get system info for update check") + return {"success": False, "error": "api_error"} + + # Проверяем наличие обновлений + update_result = self._make_api_request("SYNO.Core.Upgrade.Server", "check", version=1) + + # Получаем настройки автоматического обновления + settings_result = self._make_api_request("SYNO.Core.Upgrade.Server.Setting", "get", version=1) + + # Получаем информацию о доступных обновлениях + update_info = self._make_api_request("SYNO.Core.Upgrade.Server.Download", "list", version=1) + + current_version = system_info.get("version_string", "unknown") + auto_update_enabled = settings_result.get("auto_update", False) if settings_result else False + + updates = [] + if update_info and "updates" in update_info: + updates = update_info.get("updates", []) + + update_available = len(updates) > 0 + + return { + "success": True, + "current_version": current_version, + "update_available": update_available, + "auto_update_enabled": auto_update_enabled, + "updates": updates + } + + except Exception as e: + logger.error(f"Error checking for updates: {str(e)}") + return {"success": False, "error": str(e)} + + def get_quota_info(self) -> Dict[str, Any]: + """Получение информации о квотах пользователей""" + logger.info("Getting user quota information") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for quota info request") + return {"success": False, "error": "authentication_failed"} + + try: + # Получаем список пользователей + users_result = self._make_api_request("SYNO.Core.User", "list", version=1) + + if not users_result: + logger.error("Failed to get user list for quota info") + return {"success": False, "error": "api_error"} + + users = users_result.get("users", []) + user_quotas = [] + + # Получаем квоты для каждого пользователя + for user in users: + user_name = user.get("name") + if not user_name: + continue + + quota_result = self._make_api_request("SYNO.Core.Quota", "get", version=1, + params={"user_name": user_name}) + + if quota_result and "quotas" in quota_result: + user_quotas.append({ + "user": user_name, + "quotas": quota_result.get("quotas", []) + }) + + return { + "success": True, + "user_quotas": user_quotas + } + + except Exception as e: + logger.error(f"Error getting quota info: {str(e)}") + return {"success": False, "error": str(e)} diff --git a/.history/src/api/synology_20250830101104.py b/.history/src/api/synology_20250830101104.py new file mode 100644 index 0000000..a174012 --- /dev/null +++ b/.history/src/api/synology_20250830101104.py @@ -0,0 +1,1954 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Модуль для взаимодействия с API Synology NAS +""" + +import requests +from requests.adapters import HTTPAdapter +import json +import logging +import time +import urllib3 +from urllib3.util import Retry +from typing import Dict, Any, Optional, List +import socket +import struct +from time import sleep + +from src.config.config import ( + SYNOLOGY_HOST, + SYNOLOGY_PORT, + SYNOLOGY_USERNAME, + SYNOLOGY_PASSWORD, + SYNOLOGY_SECURE, + SYNOLOGY_TIMEOUT, + SYNOLOGY_MAC, + WOL_PORT, + SYNOLOGY_API_VERSION, + SYNOLOGY_POWER_API, + SYNOLOGY_INFO_API +) +from src.api.api_discovery import discover_available_apis, find_compatible_api + +# Отключение предупреждений о небезопасных SSL-соединениях +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + +logger = logging.getLogger(__name__) + +class SynologyAPI: + """Класс для взаимодействия с API Synology NAS""" + + def __init__(self): + """Инициализация класса SynologyAPI""" + logger.info("Creating API with auto-retry and connection pool") + logger.debug(f"Connection details: Host={SYNOLOGY_HOST}, Port={SYNOLOGY_PORT}, Secure={SYNOLOGY_SECURE}") + + self.protocol = "https" if SYNOLOGY_SECURE else "http" + self.base_url = f"{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi" + self.sid = None + self.session = requests.Session() + + # Настройка SSL + if self.protocol == "https": + logger.debug("SSL enabled, disabling certificate verification for internal network") + self.session.verify = False # Отключаем проверку SSL для внутренней сети + + # Добавляем пользовательские заголовки для улучшения совместимости с API + custom_headers = { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36', + 'Accept': 'application/json, text/javascript, */*; q=0.01', + 'Accept-Language': 'ru-RU,ru;q=0.9,en-US;q=0.8,en;q=0.7', + 'X-Requested-With': 'XMLHttpRequest', + 'Connection': 'keep-alive', + 'Referer': f'{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/' + } + self.session.headers.update(custom_headers) + logger.debug("Added browser-like headers for API compatibility") + + # Добавляем повторные попытки для HTTP-запросов + retry_strategy = Retry( + total=5, # Увеличиваем количество попыток + status_forcelist=[429, 500, 502, 503, 504, 404], + allowed_methods=["GET", "POST"], + backoff_factor=1.5, # Увеличиваем задержку между попытками + respect_retry_after_header=True + ) + adapter = HTTPAdapter( + max_retries=retry_strategy, + pool_connections=3, + pool_maxsize=10 + ) + self.session.mount("http://", adapter) + self.session.mount("https://", adapter) + + # Таймауты будут указаны в запросах + self.default_timeout = (SYNOLOGY_TIMEOUT, SYNOLOGY_TIMEOUT*2) + logger.debug(f"Setting default request timeout: {self.default_timeout}") + + # Кэш для хранения результатов запросов + self._cache = {} + self._cache_ttl = {} + self._last_online_check = 0 + self._last_online_status = False + self._online_check_interval = 30 # Интервал для кэширования проверок онлайн-статуса + + # Время последней успешной аутентификации и срок действия сессии + self._last_auth_time = 0 + self._auth_expiry = 3600 # По умолчанию 1 час + + # Информация о доступных API + self._available_apis = {} + self._api_info_ttl = 0 + + # Инициализируем API version resolver для автоматического определения совместимых API + self.api_resolver = None # Будет создан при необходимости + + def login(self) -> bool: + """Авторизация в API Synology NAS""" + # Сбрасываем SID для новой сессии + self.sid = None + + logger.info("Attempting to authenticate with Synology NAS...") + logger.debug(f"Base URL: {self.base_url}") + + # Проверяем доступность NAS перед запросом с помощью прямой TCP-проверки + # Избегаем вызова is_online(), чтобы не создавать рекурсию + online_status = self._check_tcp_connection() + if not online_status: + logger.error("Cannot login: Synology NAS is not reachable") + return False + + # Пробуем различные версии API для аутентификации + # Начинаем с версии 3, которая показала лучшую совместимость в тестах + auth_versions_to_try = [3, 2, 1, 6] # Пробуем сначала более стабильные версии + + for auth_version in auth_versions_to_try: + try: + # Определяем путь к API аутентификации + auth_path = "entry.cgi" # Используем entry.cgi вместо auth.cgi по умолчанию + + # Проверка информации API для определения доступных версий API + api_info_url = f"{self.base_url}/entry.cgi" + api_info_params = { + "api": "SYNO.API.Info", + "version": "1", + "method": "query", + "query": "SYNO.API.Auth" + } + + logger.debug(f"Querying API info for auth version {auth_version}") + try: + api_info_response = self.session.get( + api_info_url, + params=api_info_params, + timeout=self.default_timeout, + verify=False + ) + + if api_info_response.status_code == 200: + api_info_data = api_info_response.json() + if api_info_data.get("success"): + auth_info = api_info_data.get("data", {}).get("SYNO.API.Auth", {}) + max_version = auth_info.get("maxVersion", 6) + min_version = auth_info.get("minVersion", 1) + auth_path = auth_info.get("path", "entry.cgi") + logger.debug(f"Auth API versions: min={min_version}, max={max_version}, path={auth_path}") + + # Проверяем поддержку текущей версии + if auth_version < min_version or auth_version > max_version: + logger.warning(f"Auth API version {auth_version} not supported (min={min_version}, max={max_version}), trying next version") + continue + else: + logger.warning("Failed to query API info, using default auth path") + else: + logger.warning(f"API info request failed with status {api_info_response.status_code}, using default auth path") + except Exception as e: + logger.warning(f"Error querying API info: {str(e)}, using default auth path") + + # Основной запрос авторизации + url = f"{self.base_url}/{auth_path}" + params = { + "api": "SYNO.API.Auth", + "version": str(auth_version), + "method": "login", + "account": SYNOLOGY_USERNAME, + "passwd": SYNOLOGY_PASSWORD, + "session": "SynologyPowerControlBot", + "format": "cookie" + } + + # Для версии 6+ используем немного другой формат + if auth_version >= 6: + params["enable_syno_token"] = "yes" + + logger.debug(f"Sending auth request to {url} with API version {auth_version}") + start_time = time.time() + response = self.session.get( + url, + params=params, + timeout=self.default_timeout, + verify=False + ) + elapsed_time = time.time() - start_time + logger.debug(f"Auth request completed in {elapsed_time:.2f}s with status code {response.status_code}") + + # Проверка статуса HTTP + if response.status_code != 200: + logger.error(f"HTTP error: {response.status_code}") + logger.debug(f"Response content: {response.text[:200]}") + continue # Пробуем следующую версию + + try: + data = response.json() + except json.JSONDecodeError: + logger.error("Failed to decode JSON response") + logger.debug(f"Response content: {response.text[:200]}") + continue # Пробуем следующую версию + + if data.get("success"): + self.sid = data.get("data", {}).get("sid") + self._last_auth_time = time.time() + logger.info(f"Successfully logged in to Synology NAS using API version {auth_version}") + logger.debug(f"Session ID obtained: {self.sid[:5] if self.sid else 'None'}...") + + # Получаем и сохраняем токен SYNO, если он есть + syno_token = data.get("data", {}).get("synotoken") + if syno_token: + self.session.headers.update({'X-SYNO-TOKEN': syno_token}) + logger.debug("Added X-SYNO-TOKEN header for improved API compatibility") + + # Также добавляем SID в cookies для улучшения совместимости + self.session.cookies.update({ + 'id': self.sid, + 'sid': self.sid + }) + logger.debug("Added SID to session cookies for improved compatibility") + + # Проверка валидности полученной сессии с помощью простого запроса + # Будем использовать SYNO.API.Info без проверки сложных методов + + # Даем системе немного времени для инициализации сессии + time.sleep(0.5) + + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to log in with version {auth_version}: Error code {error_code}") + + # Если ошибка связана с версией API, пробуем следующую версию + if error_code in [104, 105]: + logger.warning(f"Auth version {auth_version} not supported, trying next version") + continue + + # Дополнительная диагностика + if error_code == 400: + logger.error("Authentication error: Invalid credentials") + elif error_code == 401: + logger.error("Authentication error: Account disabled") + elif error_code == 402: + logger.error("Authentication error: Permission denied") + elif error_code == 403: + logger.error("Authentication error: 2-factor authentication required") + elif error_code == 404: + logger.error("Authentication error: Failed to authenticate with 2-factor authentication") + + # Если ошибка связана с неверными учетными данными, нет смысла пробовать другие версии API + if error_code in [400, 401, 402, 403, 404]: + return False + + except requests.exceptions.Timeout: + logger.error(f"Connection timeout during auth with version {auth_version}") + continue # Пробуем следующую версию + except requests.exceptions.ConnectionError as e: + logger.error(f"Connection error during auth with version {auth_version}: {str(e)}") + continue # Пробуем следующую версию + except requests.RequestException as e: + logger.error(f"Request error during auth with version {auth_version}: {str(e)}") + continue # Пробуем следующую версию + except Exception as e: + logger.error(f"Unexpected error during login with version {auth_version}: {str(e)}", exc_info=True) + continue # Пробуем следующую версию + + # Если все версии не сработали + logger.error("Failed to authenticate with any API version") + return False + + def _validate_session(self) -> bool: + """Проверяет валидность сессии после авторизации""" + if not self.sid: + return False + + # Попробуем сделать простой запрос для проверки сессии + test_apis = [ + {"api": "SYNO.Core.System", "method": "info", "version": 1}, + {"api": "SYNO.DSM.Info", "method": "getinfo", "version": 1} + ] + + for test_api in test_apis: + try: + url = f"{self.base_url}/entry.cgi" + params = { + "api": test_api["api"], + "version": str(test_api["version"]), + "method": test_api["method"], + "_sid": self.sid + } + + response = self.session.get(url, params=params, timeout=self.default_timeout, verify=False) + + if response.status_code == 200: + data = response.json() + if data.get("success"): + logger.debug(f"Session validation successful using {test_api['api']}") + return True + else: + error_code = data.get("error", {}).get("code", -1) + if error_code != 119: # Не сессия истекла + logger.debug(f"Session validation with {test_api['api']} returned error code {error_code}") + return True # Считаем сессию валидной, если ошибка не связана с истечением сессии + except Exception as e: + logger.warning(f"Error during session validation with {test_api['api']}: {str(e)}") + + logger.warning("Session validation failed with all test APIs") + return False + + def logout(self) -> bool: + """Выход из API Synology NAS""" + if not self.sid: + return True + + try: + url = f"{self.base_url}/auth.cgi" + params = { + "api": "SYNO.API.Auth", + "version": "1", + "method": "logout", + "session": "SynologyPowerControlBot", + "_sid": self.sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + self.sid = None + logger.info("Successfully logged out from Synology NAS") + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to log out from Synology NAS: Error code {error_code}") + return False + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + return False + + def _make_api_request(self, api_name: str, method: str, version: int = 1, + params: Optional[Dict[str, Any]] = None, retry_auth: bool = True, retry_count: int = 0) -> Optional[Dict[str, Any]]: + """Обобщенный метод для выполнения API запросов с обработкой ошибок""" + # Ограничение на количество повторных попыток + if retry_count >= 3: + logger.error(f"Too many retries for {api_name}.{method}, giving up") + return None + + # Проверка наличия авторизации + if not self.sid and not self.login(): + logger.error(f"Not authenticated for API request: {api_name}.{method}") + return None + + # Проверка информации API для определения пути и поддерживаемой версии + api_info_url = f"{self.base_url}/entry.cgi" + api_info_params = { + "api": "SYNO.API.Info", + "version": "1", + "method": "query", + "query": api_name + } + + api_path = "entry.cgi" + try: + logger.debug(f"Querying API info for {api_name}") + api_info_response = self.session.get( + api_info_url, + params=api_info_params, + timeout=self.default_timeout, + verify=False + ) + + if api_info_response.status_code == 200: + api_info_data = api_info_response.json() + if api_info_data.get("success"): + api_info = api_info_data.get("data", {}).get(api_name, {}) + if api_info: + max_version = api_info.get("maxVersion", version) + min_version = api_info.get("minVersion", version) + api_path = api_info.get("path", "entry.cgi") + + # Проверка, поддерживается ли запрошенная версия + if version < min_version: + logger.warning(f"API version {version} for {api_name} is below minimum {min_version}, using {min_version}") + version = min_version + elif version > max_version: + logger.warning(f"API version {version} for {api_name} exceeds maximum {max_version}, using {max_version}") + version = max_version + + logger.debug(f"Using API path: {api_path}, version: {version}") + else: + logger.warning(f"API {api_name} not found in API info, using defaults") + except Exception as e: + logger.warning(f"Error querying API info for {api_name}: {str(e)}, using defaults") + + # Подготовка базовых параметров запроса + base_params = { + "api": api_name, + "version": str(version), + "method": method, + "_sid": self.sid # Используем _sid вместо sid для лучшей совместимости + } + + # Добавление дополнительных параметров, если они заданы + if params: + base_params.update(params) + + url = f"{self.base_url}/{api_path}" + logger.debug(f"API request: {api_name}.{method} v{version} to {url}") + logger.debug(f"Full request params: {base_params}") + + try: + start_time = time.time() + response = self.session.get( + url, + params=base_params, + timeout=self.default_timeout, + verify=False + ) + elapsed_time = time.time() - start_time + logger.debug(f"API request completed in {elapsed_time:.2f}s with status {response.status_code}") + + # Проверка статуса HTTP + if response.status_code != 200: + logger.error(f"HTTP error: {response.status_code} for {api_name}.{method}") + + # Повторная попытка при ошибках соединения + if response.status_code in [500, 502, 503, 504]: + logger.info(f"Server error, retrying request for {api_name}.{method}") + sleep(2) + return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1) + + return None + + try: + data = response.json() + except json.JSONDecodeError: + logger.error(f"Failed to decode JSON response for {api_name}.{method}") + logger.debug(f"Response content: {response.text[:200]}") + + # Повторная попытка при ошибках декодирования + logger.info(f"JSON decode error, retrying request for {api_name}.{method}") + sleep(2) + return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1) + + if data.get("success"): + logger.info(f"API request successful for {api_name}.{method}") + return data.get("data", {}) + else: + error_code = data.get("error", {}).get("code", -1) + error_desc = self._get_error_description(error_code) + logger.error(f"API error for {api_name}.{method}: {error_code} - {error_desc}") + + # Ошибки доступа или прав часто встречаются, но они не критичные + # Например, ошибка 102 означает, что нет прав, но NAS доступен + if error_code in [102, 103, 104, 105]: + logger.warning(f"Permission-related error for {api_name}.{method}: {error_code}") + # Возвращаем пустой словарь вместо None, + # чтобы вызывающий код мог понять, что запрос выполнен + return {} + + # Если ошибка связана с авторизацией и нам разрешено повторить попытку + if error_code in [106, 107, 119] and retry_auth: + logger.info(f"Session error (code {error_code}), creating fresh session...") + self.sid = None # Сбрасываем SID + + # Для ошибки 119 (Session timeout) дадим системе немного времени + if error_code == 119: + logger.info("Session timeout detected, waiting before retry...") + sleep(3) + + if self.login(): + logger.info("Re-authenticated with fresh session, retrying API request...") + # Рекурсивный вызов, но со счетчиком повторов + return self._make_api_request(api_name, method, version, params, False, retry_count + 1) + + # Для некоторых ошибок можно автоматически повторить запрос + if error_code in [408, 429, 500, 502, 503, 504]: + logger.info(f"Temporary error {error_code}, retrying request for {api_name}.{method}") + sleep(2) + return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1) + + return None + + except requests.exceptions.Timeout: + logger.error(f"Request timeout for {api_name}.{method}") + + # Повторная попытка при таймауте + if retry_count < 2: + logger.info(f"Timeout, retrying request for {api_name}.{method}") + sleep(2) + return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1) + + return None + except requests.exceptions.ConnectionError as e: + logger.error(f"Connection error for {api_name}.{method}: {str(e)}") + + # Повторная попытка при ошибке соединения + if retry_count < 2: + logger.info(f"Connection error, retrying request for {api_name}.{method}") + sleep(2) + return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1) + + return None + except Exception as e: + logger.error(f"Unexpected error during {api_name}.{method}: {str(e)}") + return None + + def get_system_status(self) -> Dict[str, Any]: + """Получение статуса системы""" + # Проверяем доступность системы + if not self.is_online(): + logger.info("Device is offline, skipping API request") + return {"status": "offline"} + + # Проверяем, есть ли кэшированный результат + cache_key = "system_status" + current_time = time.time() + if cache_key in self._cache and current_time - self._cache_ttl.get(cache_key, 0) < 60: + logger.debug("Using cached system status") + return self._cache[cache_key] + + # Используем рекомендованный API для получения информации о системе + logger.info(f"Getting system info using {SYNOLOGY_INFO_API} v{SYNOLOGY_API_VERSION}") + + # Для SYNO.DSM.Info метод getinfo, для других обычно используется get или info + if SYNOLOGY_INFO_API == "SYNO.DSM.Info": + method = "getinfo" + else: + method = "get" + + result = self._make_api_request(SYNOLOGY_INFO_API, method, version=SYNOLOGY_API_VERSION) + + if result: + logger.info(f"Successfully retrieved system info using {SYNOLOGY_INFO_API}") + + # Формируем расширенный ответ с дополнительной информацией + system_info = { + "status": "online", + "hostname": result.get("hostname", "unknown"), + "model": result.get("model", "unknown"), + "version": result.get("version", "unknown"), + "uptime": result.get("uptime", 0), + "time": current_time, + "is_online": True, + "api_used": SYNOLOGY_INFO_API + } + + # Сохраняем в кэше + self._cache[cache_key] = system_info + self._cache_ttl[cache_key] = current_time + + return system_info + + # Если основной API не сработал, пробуем резервные варианты + logger.warning(f"Failed to retrieve system info with {SYNOLOGY_INFO_API}, trying fallback APIs") + + # Пробуем резервные API + apis_to_try = [ + {"name": "SYNO.DSM.Info", "method": "getinfo", "version": 2}, + {"name": "SYNO.Core.System", "method": "info", "version": 1}, + {"name": "SYNO.Core.System.Status", "method": "get", "version": 1}, + {"name": "SYNO.Core.System.Info", "method": "get", "version": 1}, + ] + + for api in apis_to_try: + if api["name"] == SYNOLOGY_INFO_API: + continue # Пропускаем уже проверенный API + + logger.info(f"Trying to get system info using {api['name']}.{api['method']} v{api['version']}") + result = self._make_api_request(api["name"], api["method"], version=api["version"]) + + if result: + logger.info(f"Successfully retrieved system info using {api['name']}") + + # Формируем расширенный ответ с дополнительной информацией + system_info = { + "status": "online", + "hostname": result.get("hostname", "unknown"), + "model": result.get("model", "unknown"), + "version": result.get("version", "unknown"), + "uptime": result.get("uptime", 0), + "time": current_time, + "is_online": True, + "api_used": api["name"] + } + + # Сохраняем в кэше + self._cache[cache_key] = system_info + self._cache_ttl[cache_key] = current_time + + return system_info + + # Если все запросы не удались, но система онлайн, возвращаем базовую информацию + logger.warning("Failed to retrieve system info with all API methods") + return { + "status": "error", + "error": "Failed to fetch system information", + "is_online": True, + "time": current_time + } + + def shutdown_system(self) -> bool: + """Выключение системы""" + # Проверяем, включено ли устройство перед попыткой его выключить + if not self.is_online(force_check=True): + logger.info("Synology NAS is already offline, no need to shut down") + return True + + logger.info("Attempting to shutdown Synology NAS...") + + # Попробуем сначала использовать предпочтительный API для управления питанием + logger.info(f"Trying shutdown with {SYNOLOGY_POWER_API} v{SYNOLOGY_API_VERSION}") + + # Для SYNO.Core.Hardware.PowerRecovery используется метод setPowerOnState + # Для других API обычно используется метод shutdown или reboot + if SYNOLOGY_POWER_API == "SYNO.Core.Hardware.PowerRecovery": + # Для этого API нужны специальные параметры + params = {"state": "powerbtn"} # powerbtn имитирует нажатие кнопки питания + result = self._make_api_request(SYNOLOGY_POWER_API, "setPowerOnState", version=SYNOLOGY_API_VERSION, params=params) + else: + # Пробуем стандартный метод + result = self._make_api_request(SYNOLOGY_POWER_API, "shutdown", version=SYNOLOGY_API_VERSION) + + if result is not None: + logger.info(f"Successfully initiated system shutdown using {SYNOLOGY_POWER_API}") + + # Даем системе время начать процесс выключения + logger.info("Waiting for shutdown to initialize...") + sleep(5) + + # Проверяем статус + if not self.is_online(force_check=True): + logger.info("System is now offline. Shutdown confirmed successful.") + return True + else: + logger.info("System still appears to be online, but shutdown may be in progress.") + return True + else: + logger.warning(f"Failed to shutdown using {SYNOLOGY_POWER_API}, trying alternative methods") + + # Если не сработал основной метод, пробуем резервные варианты + # Проверка всех доступных методов API для выключения + apis_to_try = [ + {"name": "SYNO.Core.System", "method": "shutdown", "version": 3}, + {"name": "SYNO.Core.System.Power", "method": "shutdown", "version": 1}, + {"name": "SYNO.DSM.Power", "method": "shutdown", "version": 1}, + {"name": "SYNO.System.Power", "method": "shutdown", "version": 1}, + {"name": "SYNO.DSM.System", "method": "shutdown", "version": 1} + ] + + # Проверяем доступные API + try: + api_info_url = f"{self.base_url}/entry.cgi" + api_info_params = { + "api": "SYNO.API.Info", + "version": "1", + "method": "query", + "query": "SYNO.Core.System.Power,SYNO.DSM.Power,SYNO.Core.System,SYNO.System.Power,SYNO.DSM.System" + } + + logger.debug("Checking available shutdown APIs") + api_info_response = self.session.get( + api_info_url, + params=api_info_params, + timeout=self.default_timeout, + verify=False + ) + + if api_info_response.status_code == 200: + api_info_data = api_info_response.json() + if api_info_data.get("success"): + available_apis = api_info_data.get("data", {}) + logger.debug(f"Available APIs: {list(available_apis.keys())}") + + # Фильтруем только доступные API + filtered_apis = [] + for api in apis_to_try: + if api["name"] in available_apis: + api_info = available_apis[api["name"]] + # Проверка версии API + if api_info.get("minVersion", 1) <= api["version"] <= api_info.get("maxVersion", 1): + filtered_apis.append(api) + logger.debug(f"Adding {api['name']} to available shutdown APIs") + else: + logger.debug(f"API {api['name']} version {api['version']} not supported. Supported: {api_info.get('minVersion')}-{api_info.get('maxVersion')}") + + if filtered_apis: + apis_to_try = filtered_apis + else: + logger.warning("No compatible APIs found, trying all methods as fallback") + else: + logger.warning("Failed to query API info, using default methods") + else: + logger.warning(f"API info request failed with status {api_info_response.status_code}, using default methods") + except Exception as e: + logger.warning(f"Error querying API info: {str(e)}, using default methods") + + # Пробуем все доступные методы по порядку + for api in apis_to_try: + logger.info(f"Trying shutdown with {api['name']}.{api['method']} v{api['version']}") + result = self._make_api_request(api["name"], api["method"], version=api["version"]) + + if result is not None: + logger.info(f"Successfully initiated system shutdown using {api['name']}") + + # Даем системе время начать процесс выключения + logger.info("Waiting for shutdown to initialize...") + sleep(5) + + # Проверяем статус + if not self.is_online(force_check=True): + logger.info("System is now offline. Shutdown confirmed successful.") + return True + else: + logger.info("System still appears to be online, but shutdown may be in progress.") + return True + else: + logger.warning(f"Failed to shutdown using {api['name']}.{api['method']} v{api['version']}") + + # Если ни один метод не сработал, но система стала недоступна + if not self.is_online(force_check=True): + logger.info("System appears to be shutting down despite API errors") + return True + + logger.error("Failed to shutdown system after trying multiple APIs") + return False + + def reboot_system(self) -> bool: + """Перезагрузка системы""" + # Проверяем, включена ли система + if not self.is_online(force_check=True): + logger.error("Cannot reboot: System is offline") + return False + + logger.info("Attempting to reboot Synology NAS...") + + # Список API и методов для попытки перезагрузки + apis_to_try = [ + {"api": "SYNO.Core.System", "method": "reboot", "version": 1}, + {"api": "SYNO.DSM.System", "method": "reboot", "version": 1}, + ] + + # Добавляем настроенный в конфигурации API, если он отличается от уже добавленных + if SYNOLOGY_POWER_API not in ["SYNO.Core.System", "SYNO.DSM.System"]: + for method in ["restart", "reboot"]: + apis_to_try.append({ + "api": SYNOLOGY_POWER_API, + "method": method, + "version": SYNOLOGY_API_VERSION + }) + + # Перебираем все возможные API и методы + for api_info in apis_to_try: + try: + logger.info(f"Trying reboot with {api_info['api']} API using method {api_info['method']}") + result = self._make_api_request(api_info['api'], api_info['method'], version=api_info['version']) + if result is not None: + logger.info(f"Successfully initiated system reboot using {api_info['api']} API") + + # Даем системе время начать процесс перезагрузки + logger.info("Waiting for reboot to initialize...") + sleep(5) + + return True + except Exception as e: + logger.error(f"Error during reboot with {api_info['api']}: {str(e)}") + + # Если все попытки не удались, возвращаем False + logger.error("All reboot attempts failed") + return False + sleep(5) + + # Ждем, пока система станет недоступна (признак перезагрузки) + reboot_started = False + for i in range(12): # Проверяем в течение 60 секунд (12 * 5сек) + if not self.is_online(force_check=True): + logger.info(f"System went offline after {i*5} seconds, reboot in progress") + reboot_started = True + break + logger.debug(f"System still online, waiting... ({i+1}/12)") + sleep(5) + + if reboot_started: + # Ждем, пока система снова станет доступна + logger.info("Waiting for system to come back online...") + return self.wait_for_boot(max_attempts=30, delay=10) + else: + logger.warning("System did not go offline after reboot command, but command was accepted") + # Даже если система не ушла оффлайн, команда могла быть принята + return True + else: + logger.warning(f"Failed to reboot using {SYNOLOGY_POWER_API}, trying alternative methods") + + # Если не сработал основной метод, пробуем резервные варианты + # Проверка всех доступных методов API для перезагрузки + apis_to_try = [ + {"name": "SYNO.Core.System.Power", "method": "restart", "version": 1}, + {"name": "SYNO.DSM.Power", "method": "reboot", "version": 1}, + {"name": "SYNO.Core.System", "method": "reboot", "version": 3}, + {"name": "SYNO.System.Power", "method": "reboot", "version": 1}, + {"name": "SYNO.DSM.System", "method": "reboot", "version": 1}, + {"name": "SYNO.Core.Hardware.NeedReboot", "method": "reboot", "version": 1} + ] + + # Получаем список доступных API + try: + api_info_url = f"{self.base_url}/entry.cgi" + api_info_params = { + "api": "SYNO.API.Info", + "version": "1", + "method": "query", + "query": "SYNO.Core.System.Power,SYNO.DSM.Power,SYNO.Core.System,SYNO.System.Power,SYNO.DSM.System,SYNO.Core.Hardware.NeedReboot" + } + + logger.debug("Checking available reboot APIs") + api_info_response = self.session.get( + api_info_url, + params=api_info_params, + timeout=self.default_timeout, + verify=False + ) + + if api_info_response.status_code == 200: + api_info_data = api_info_response.json() + if api_info_data.get("success"): + available_apis = api_info_data.get("data", {}) + logger.debug(f"Available APIs: {list(available_apis.keys())}") + + # Фильтруем только доступные API + filtered_apis = [] + for api in apis_to_try: + if api["name"] in available_apis: + api_info = available_apis[api["name"]] + # Проверка версии API + if api_info.get("minVersion", 1) <= api["version"] <= api_info.get("maxVersion", 1): + filtered_apis.append(api) + logger.debug(f"Adding {api['name']} to available reboot APIs") + else: + logger.debug(f"API {api['name']} version {api['version']} not supported. Supported: {api_info.get('minVersion')}-{api_info.get('maxVersion')}") + + if filtered_apis: + apis_to_try = filtered_apis + else: + logger.warning("No compatible APIs found, trying all methods as fallback") + else: + logger.warning("Failed to query API info, using default methods") + else: + logger.warning(f"API info request failed with status {api_info_response.status_code}, using default methods") + except Exception as e: + logger.warning(f"Error querying API info: {str(e)}, using default methods") + + # Пробуем все доступные методы по порядку + for api in apis_to_try: + logger.info(f"Trying reboot with {api['name']}.{api['method']} v{api['version']}") + result = self._make_api_request(api["name"], api["method"], version=api["version"]) + + if result is not None: + logger.info(f"Successfully initiated system reboot using {api['name']}") + + # Даем системе время начать процесс перезагрузки + logger.info("Waiting for reboot to initialize...") + sleep(5) + + # Ждем, пока система станет недоступна (признак перезагрузки) + reboot_started = False + for i in range(12): # Проверяем в течение 60 секунд (12 * 5сек) + if not self.is_online(force_check=True): + logger.info(f"System went offline after {i*5} seconds, reboot in progress") + reboot_started = True + break + logger.debug(f"System still online, waiting... ({i+1}/12)") + sleep(5) + + if reboot_started: + # Ждем, пока система снова станет доступна + logger.info("Waiting for system to come back online...") + return self.wait_for_boot(max_attempts=30, delay=10) + else: + logger.warning("System did not go offline after reboot command, but command was accepted") + # Даже если система не ушла оффлайн, команда могла быть принята + return True + else: + logger.warning(f"Failed to reboot using {api['name']}.{api['method']} v{api['version']}") + + logger.error("Failed to reboot system after trying multiple APIs") + return False + + def _get_error_description(self, error_code: int) -> str: + """Получение описания ошибки по коду""" + error_descriptions = { + 100: "Unknown error", + 101: "Invalid parameter", + 102: "API does not exist", + 103: "Method does not exist", + 104: "Version does not support", + 105: "Permission denied", + 106: "Session timeout", + 107: "Session interrupted by duplicate login", + 400: "Invalid credentials", + 401: "Account disabled", + 402: "Permission denied", + 403: "2FA required", + 404: "Failed to authenticate with 2FA" + } + return error_descriptions.get(error_code, "Unknown error code") + + def _check_tcp_connection(self) -> bool: + """Проверка базового TCP-соединения с Synology NAS""" + try: + socket_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + socket_obj.settimeout(SYNOLOGY_TIMEOUT) + result = socket_obj.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT)) + socket_obj.close() + + return result == 0 + except socket.error as e: + logger.error(f"Socket error during connection check: {str(e)}") + return False + except Exception as e: + logger.error(f"Unexpected error during connection check: {str(e)}") + return False + + def is_online(self, force_check=False) -> bool: + """Проверка онлайн-статуса Synology NAS""" + # Используем кэшированное значение, если доступно и не устарело + current_time = time.time() + if not force_check and (current_time - self._last_online_check) < self._online_check_interval: + logger.debug(f"Using cached online status: {self._last_online_status}") + return self._last_online_status + + logger.info("Checking if NAS is online...") + + # Проверяем TCP-соединение + online_status = self._check_tcp_connection() + logger.info(f"Detected Synology NAS online status: {online_status}") + + # Если TCP-соединение успешно и у нас есть действующий SID, + # попробуем более детальную проверку через API + if online_status and self.sid: + logger.info("Trying to fetch more detailed online status through API...") + + # Пробуем разные API для проверки онлайн-статуса + api_checks = [ + {"api": "SYNO.DSM.Info", "version": "2", "method": "getinfo"}, + {"api": "SYNO.Core.System", "version": "1", "method": "info"}, + {"api": "SYNO.Core.System.Status", "version": "1", "method": "get"} + ] + + api_success = False + for api_check in api_checks: + try: + url = f"{self.base_url}/entry.cgi" + params = { + "api": api_check["api"], + "version": api_check["version"], + "method": api_check["method"], + "sid": self.sid + } + + logger.debug(f"Trying online status check with {api_check['api']}") + response = self.session.get( + url, + params=params, + timeout=self.default_timeout, + verify=False + ) + + if response.status_code == 200: + data = response.json() + if data.get("success"): + logger.info(f"API request successful for {api_check['api']}") + logger.info("Synology NAS is online with API access") + api_success = True + break + else: + error_code = data.get("error", {}).get("code", -1) + logger.warning(f"API response indicates an error: {error_code}, but NAS is reachable") + else: + logger.warning(f"API returned status code {response.status_code}") + except Exception as e: + logger.warning(f"API check failed for {api_check['api']}, but TCP connection succeeded: {str(e)}") + + if not api_success: + logger.warning("All API checks failed, but TCP connection is successful") + + # Обновляем кэшированное значение + self._last_online_check = current_time + self._last_online_status = online_status + + return online_status + + def wake_on_lan(self) -> bool: + """Отправка Wake-on-LAN пакета для включения Synology NAS""" + if not SYNOLOGY_MAC: + logger.error("MAC address not configured") + return False + + try: + # Преобразование MAC-адреса в байты + mac_address = SYNOLOGY_MAC.replace(':', '').replace('-', '') + if len(mac_address) != 12: + logger.error(f"Invalid MAC address format: {SYNOLOGY_MAC}") + return False + + try: + mac_bytes = bytes.fromhex(mac_address) + except ValueError as e: + logger.error(f"Failed to parse MAC address: {str(e)}") + logger.error(f"MAC address must contain only hex characters: {SYNOLOGY_MAC}") + return False + + # Создание Magic Packet + magic_packet = b'\xff' * 6 + mac_bytes * 16 + + # Отправка пакета на конкретный адрес + try: + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) + sock.sendto(magic_packet, (SYNOLOGY_HOST, WOL_PORT)) + sock.close() + logger.info(f"Wake-on-LAN packet sent to {SYNOLOGY_MAC} at {SYNOLOGY_HOST}:{WOL_PORT}") + except Exception as e: + logger.error(f"Error sending directed WoL packet: {str(e)}") + return False + + # Для надежности отправляем также широковещательный пакет + try: + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) + + # Используем стандартный широковещательный адрес + broadcast_addr = "255.255.255.255" + sock.sendto(magic_packet, (broadcast_addr, WOL_PORT)) + sock.close() + logger.info(f"Wake-on-LAN broadcast packet sent to {broadcast_addr}:{WOL_PORT}") + except Exception as e: + logger.warning(f"Error sending broadcast WoL packet (non-critical): {str(e)}") + # Не считаем ошибкой, т.к. основной пакет уже отправлен + + return True + + except Exception as e: + logger.error(f"Unexpected error in wake_on_lan: {str(e)}") + return False + + def wait_for_boot(self, max_attempts: int = 30, delay: int = 10) -> bool: + """Ожидание загрузки Synology NAS""" + logger.info(f"Waiting for Synology NAS to boot (timeout: {max_attempts * delay}s)...") + + for attempt in range(max_attempts): + # Принудительно проверяем статус без использования кэша + if self.is_online(force_check=True): + logger.info(f"Synology NAS is online after {attempt + 1} attempts ({(attempt + 1) * delay}s)") + + # Проверяем, что не только сеть доступна, но и API загрузился + api_ready = False + logger.info("Waiting for API services to initialize...") + + for api_check in range(5): # Даем еще до 50 секунд для загрузки API + if self.sid or self.login(): + api_ready = True + logger.info(f"API services are ready after {api_check + 1} attempts") + break + logger.debug(f"API not ready yet, waiting... ({api_check + 1}/5)") + sleep(10) + + if not api_ready: + logger.warning("System is online but API services may not be fully initialized") + + # Дадим дополнительное время для полной загрузки всех сервисов + sleep(delay) + return True + + sleep(delay) + logger.info(f"Waiting for boot... Attempt {attempt + 1}/{max_attempts}") + + logger.error(f"Synology NAS didn't come online after {max_attempts * delay} seconds") + return False + + def power_on(self) -> bool: + """Включение Synology NAS""" + # Принудительная проверка статуса + if self.is_online(force_check=True): + logger.info("Synology NAS is already online") + return True + + logger.info("Powering on Synology NAS via Wake-on-LAN...") + + # Проверяем, настроен ли MAC-адрес + if not SYNOLOGY_MAC: + logger.error("Cannot power on: MAC address not configured in settings") + return False + + # Пробуем отправить несколько WoL пакетов для надежности + success = False + for attempt in range(3): + logger.debug(f"Sending WoL packet, attempt {attempt + 1}/3") + if self.wake_on_lan(): + success = True + break + sleep(1) + + if not success: + logger.error("Failed to send Wake-on-LAN packets") + return False + + # Ожидание загрузки + logger.info("WoL packets sent successfully, waiting for system to boot...") + boot_result = self.wait_for_boot(max_attempts=30, delay=10) + + if boot_result: + # Проверяем доступность API после загрузки + system_status = self.get_system_status() + if system_status.get("status") == "online": + logger.info("System booted successfully with API access") + return True + else: + logger.warning("System appears to be online but API may not be fully ready") + return True + else: + logger.error("System did not come online after WoL") + return False + + def power_off(self) -> bool: + """Выключение Synology NAS""" + if not self.is_online(force_check=True): + logger.info("Synology NAS is already offline") + return True + + logger.info("Powering off Synology NAS...") + + # Список API и методов для попытки выключения + apis_to_try = [ + {"api": "SYNO.Core.System", "method": "shutdown", "version": 1}, + {"api": "SYNO.DSM.System", "method": "shutdown", "version": 1}, + {"api": SYNOLOGY_POWER_API, "method": "shutdown", "version": SYNOLOGY_API_VERSION} + ] + + # Перебираем все возможные API и методы + for api_info in apis_to_try: + try: + logger.info(f"Trying shutdown with {api_info['api']} API using method {api_info['method']}") + api_result = self._make_api_request(api_info['api'], api_info['method'], version=api_info['version']) + if api_result is not None: + logger.info(f"Successfully initiated system shutdown using {api_info['api']} API") + + # Даем системе время начать процесс выключения + logger.info("Waiting for shutdown to initialize...") + sleep(5) + + return True + except Exception as e: + logger.error(f"Error during shutdown with {api_info['api']}: {str(e)}") + + # Если все попытки не удались, возвращаем False + logger.error("All shutdown attempts failed") + return False + + # Если все еще не сработало, используем оригинальный метод shutdown_system + if not result: + result = self.shutdown_system() + + if result: + # Дополнительная проверка, что система действительно выключилась + logger.info("Verifying system is offline...") + for attempt in range(12): # Проверяем в течение 2 минут (12 * 10 сек) + if not self.is_online(force_check=True): + logger.info(f"System confirmed offline after {attempt * 10} seconds") + return True + logger.debug(f"System still shutting down, checking again... ({attempt + 1}/12)") + sleep(10) + + logger.warning("System still appears to be online after shutdown command") + return False + else: + logger.error("Failed to initiate shutdown") + return False + + # Заглушки для расширенных методов + def get_shared_folders(self) -> List[Dict[str, Any]]: + """Получение списка общих папок""" + logger.info("Getting list of shared folders") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for shared folders request") + return [] + + try: + # Запрашиваем список общих папок через FileStation API + result = self._make_api_request("SYNO.FileStation.List", "list_share", version=2) + + if not result: + # Пробуем альтернативный API + logger.info("Trying alternative API for shared folders") + alt_result = self._make_api_request("SYNO.FileStation.List", "list_share", version=1) + if alt_result: + return alt_result.get("shares", []) + return [] + + return result.get("shares", []) + + except Exception as e: + logger.error(f"Error getting shared folders: {str(e)}") + return [] + + def get_system_load(self) -> Dict[str, Any]: + """Получение информации о загрузке системы""" + logger.info("Getting system load information") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for system load request") + return {} + + try: + # Запрашиваем информацию о загрузке системы + result = self._make_api_request("SYNO.Core.System.Utilization", "get", version=1) + + if not result: + # Пробуем альтернативный API + logger.info("Trying alternative API for system load") + alt_result = self._make_api_request("SYNO.Core.System", "info", version=3) + + if not alt_result: + return {} + + # Формируем из частичных данных + return { + "cpu_load": alt_result.get("cpu_usage", 0), + "memory": { + "total": alt_result.get("memory_size", 0), + "used": alt_result.get("memory_usage", 0), + "usage_percent": alt_result.get("memory_usage_percent", 0) + } + } + + # Формируем структурированный результат + return { + "cpu_load": result.get("cpu", {}).get("user", 0) + result.get("cpu", {}).get("system", 0), + "memory": result.get("memory", {}), + "network": result.get("network", {}) + } + + except Exception as e: + logger.error(f"Error getting system load: {str(e)}") + return {} + + def is_online_api(self) -> bool: + """Проверка онлайн-статуса Synology NAS с использованием API""" + if not self.is_online(): + return False + + # Проверяем доступность API через авторизацию + if not self.sid and not self.login(): + return False + + return True + + def get_storage_status(self) -> Dict[str, Any]: + """Получение подробной информации о хранилище""" + logger.info("Getting storage status information") + + # Проверяем доступность NAS и API + if not self.is_online_api(): + logger.error("Cannot get storage status: NAS is not online or API is not accessible") + return {"error": "authentication_failed"} + + try: + # Пробуем получить информацию о хранилище через SYNO.Storage.CGI.Storage API + result = self._make_api_request("SYNO.Storage.CGI.Storage", "load_info", version=1) + + if not result: + # Пробуем альтернативный API + logger.info("Trying alternative API for storage info") + alt_result = self._make_api_request("SYNO.Core.Storage", "info", version=1) + + if not alt_result: + # Пробуем еще один альтернативный API + logger.info("Trying SYNO.Core.System API for storage info") + sys_result = self._make_api_request("SYNO.Core.System", "info", version=3) + + if not sys_result: + return { + "volumes": [], + "disks": [], + "total_size": 0, + "total_used": 0, + "error": "no_data" + } + + # Извлекаем базовую информацию о хранилище из системной информации + return { + "volumes": [], + "disks": [], + "total_size": sys_result.get("hdd_total_size", 0) * 1024**2, # МБ в байты + "total_used": sys_result.get("hdd_total_size", 0) * sys_result.get("hdd_usage", 0) / 100 * 1024**2, + } + + # Обрабатываем данные из альтернативного API + volumes = alt_result.get("volumes", []) + disks = alt_result.get("disks", []) + + else: + # Обрабатываем данные из основного API + volumes = result.get("volumes", []) + disks = result.get("disks", []) + + # Рассчитываем общие размеры + total_size = 0 + total_used = 0 + + for volume in volumes: + volume_size = volume.get("size", {}).get("total", 0) + volume_used = volume.get("size", {}).get("used", 0) + + total_size += volume_size + total_used += volume_used + + return { + "volumes": volumes, + "disks": disks, + "total_size": total_size, + "total_used": total_used + } + + except Exception as e: + logger.error(f"Error in get_storage_status: {str(e)}") + return {"error": str(e)} + + def get_security_status(self) -> Dict[str, Any]: + """Получение информации о состоянии безопасности""" + logger.info("Getting security status information") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for security status request") + return {"success": False, "error": "authentication_failed"} + + try: + # Пробуем получить информацию о безопасности через API Security Scan + result = self._make_api_request("SYNO.Core.SecurityScan.Status", "get", version=1) + + if not result: + # Пробуем альтернативный API + logger.info("Trying alternative API for security status") + # Проверяем статус брандмауэра + firewall_result = self._make_api_request("SYNO.Core.Security.Firewall.Status", "get", version=1) + + # Проверяем статус автоматических обновлений + update_result = self._make_api_request("SYNO.Core.Upgrade.Server.Setting", "get", version=1) + + # Если ни один из API не отвечает + if not firewall_result and not update_result: + # Получаем общую информацию о системе для базовой проверки безопасности + sys_result = self._make_api_request("SYNO.DSM.Info", "getinfo", version=2) + + if not sys_result: + return { + "success": False, + "status": "unknown", + "last_check": None, + "is_secure": False, + "error": "no_security_api" + } + + # Собираем базовые сведения из системной информации + return { + "success": True, + "status": "basic", + "last_check": None, + "is_secure": True, # Предполагаем, что система в целом безопасна + "firewall_enabled": None, + "auto_update": None, + "version_latest": sys_result.get("version_string", "") + } + + # Собираем информацию из доступных результатов + firewall_enabled = firewall_result.get("enabled", False) if firewall_result else None + auto_update = update_result.get("auto_update", False) if update_result else None + + # Определяем, насколько система безопасна + is_secure = True # По умолчанию предполагаем, что система безопасна + if firewall_enabled is not None and not firewall_enabled: + is_secure = False + + return { + "success": True, + "status": "partial", + "last_check": None, + "is_secure": is_secure, + "firewall_enabled": firewall_enabled, + "auto_update": auto_update + } + + # Если основное API отвечает, возвращаем его данные + return { + "success": True, + "status": result.get("status", "unknown"), + "last_check": result.get("last_check", None), + "is_secure": result.get("is_secure", False), + "details": result.get("details", {}) + } + + except Exception as e: + logger.error(f"Error in get_security_status: {str(e)}") + return {"success": False, "error": str(e)} + + def get_processes(self, limit: int = 20) -> List[Dict[str, Any]]: + """Получение списка активных процессов""" + logger.info(f"Getting list of active processes (limit={limit})") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for processes request") + return [] + + try: + # Получаем список процессов через API + result = self._make_api_request("SYNO.Core.System.Process", "list", version=1, + params={"sort_by": "cpu", "order": "DESC", "limit": limit}) + + if not result: + logger.warning("Failed to get process list") + return [] + + return result.get("processes", []) + + except Exception as e: + logger.error(f"Error getting process list: {str(e)}") + return [] + + def get_network_status(self) -> Dict[str, Any]: + """Получение информации о сетевых подключениях""" + logger.info("Getting network status information") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for network status request") + return {} + + try: + # Получаем информацию о сетевых интерфейсах + interface_result = self._make_api_request("SYNO.Core.Network.Interface", "list", version=1) + + # Получаем статистику использования сети + utilization_result = self._make_api_request("SYNO.Core.System.Utilization", "get", version=1) + + interfaces = [] + if interface_result: + interfaces = interface_result.get("interfaces", []) + + network_stats = {} + if utilization_result and "network" in utilization_result: + network_stats = utilization_result.get("network", {}) + + # Объединяем данные + for interface in interfaces: + iface_id = interface.get("id", "") + if iface_id in network_stats: + interface["rx"] = network_stats[iface_id].get("rx", 0) + interface["tx"] = network_stats[iface_id].get("tx", 0) + + return { + "interfaces": interfaces, + "statistics": network_stats + } + + except Exception as e: + logger.error(f"Error getting network status: {str(e)}") + return {} + + def get_system_logs(self, limit: int = 20) -> List[Dict[str, Any]]: + """Получение журналов системы""" + logger.info(f"Getting system logs (limit={limit})") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for system logs request") + return [] + + try: + # Получаем журналы через API + result = self._make_api_request("SYNO.Core.SyslogClient.Log", "list", version=1, + params={"start": 0, "limit": limit, "sort": "time", "dir": "DESC"}) + + if not result: + # Пробуем альтернативный API + logger.info("Trying alternative API for system logs") + alt_result = self._make_api_request("SYNO.Core.Log", "list", version=1, + params={"start": 0, "limit": limit, "sort": "time", "dir": "DESC"}) + + if alt_result: + return alt_result.get("logs", []) + return [] + + return result.get("logs", []) + + except Exception as e: + logger.error(f"Error getting system logs: {str(e)}") + return [] + + def get_power_schedule(self) -> Dict[str, Any]: + """Получение расписания включения/выключения""" + logger.info("Getting power schedule") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for power schedule request") + return {} + + try: + # Получаем расписание через API + result = self._make_api_request("SYNO.Core.Hardware.PowerSchedule", "get", version=1) + + if not result: + return {} + + return result + + except Exception as e: + logger.error(f"Error getting power schedule: {str(e)}") + return {} + + def set_power_schedule(self, schedule_type: str, days: List[int], time: str, enabled: bool = True) -> bool: + """Настройка расписания включения/выключения + + Args: + schedule_type: Тип расписания ('boot' или 'shutdown') + days: Список дней недели (0-6, где 0 - понедельник) + time: Время в формате 'HH:MM' + enabled: Включить или выключить расписание + + Returns: + True если успешно, False в противном случае + """ + logger.info(f"Setting power schedule for {schedule_type} at {time} on days {days}") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for setting power schedule") + return False + + try: + # Получаем текущее расписание + current_schedule = self._make_api_request("SYNO.Core.Hardware.PowerSchedule", "get", version=1) + + if not current_schedule: + logger.error("Failed to get current power schedule") + return False + + # Подготавливаем новое расписание + params = { + "enabled": enabled, + "type": schedule_type, + "day": days, + "time": time + } + + # Устанавливаем новое расписание + result = self._make_api_request("SYNO.Core.Hardware.PowerSchedule", "set", version=1, params=params) + + if not result: + logger.error("Failed to set power schedule") + return False + + logger.info(f"Power schedule for {schedule_type} set successfully") + return True + + except Exception as e: + logger.error(f"Error setting power schedule: {str(e)}") + return False + + def get_temperature_status(self) -> Dict[str, Any]: + """Получение информации о температуре системы и дисков""" + logger.info("Getting temperature status") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for temperature status request") + return {} + + try: + # Получаем информацию о системе для общей температуры + system_info = self._make_api_request("SYNO.DSM.Info", "getinfo", version=2) + + # Получаем информацию о дисках для их температуры + storage_info = self._make_api_request("SYNO.Storage.CGI.Storage", "load_info", version=1) + + system_temp = None + disk_temps = [] + + if system_info: + system_temp = system_info.get("temperature") + + if storage_info: + disks = storage_info.get("disks", []) + for disk in disks: + name = disk.get("name", "unknown") + model = disk.get("model", "unknown") + temp = disk.get("temp", None) + if temp is not None: + disk_temps.append({ + "name": name, + "model": model, + "temperature": temp + }) + + return { + "system_temperature": system_temp, + "disk_temperatures": disk_temps, + "warning": system_info.get("temperature_warn", False) if system_info else False + } + + except Exception as e: + logger.error(f"Error getting temperature status: {str(e)}") + return {} + + def browse_files(self, folder_path: str = "", limit: int = 100) -> Dict[str, Any]: + """Просмотр файлов в указанной директории + + Args: + folder_path: Путь к папке (пустая строка для корневых общих папок) + limit: Максимальное количество элементов для возврата + + Returns: + Словарь с информацией о файлах и папках + """ + logger.info(f"Browsing files in {folder_path or 'root'}") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for file browsing") + return {"success": False, "error": "authentication_failed"} + + try: + if not folder_path: + # Если путь не указан, получаем список общих папок + result = self._make_api_request("SYNO.FileStation.List", "list_share", version=2) + + if not result: + logger.error("Failed to list shared folders") + return {"success": False, "error": "api_error"} + + return { + "success": True, + "items": result.get("shares", []), + "path": "", + "is_root": True + } + else: + # Получаем список файлов в указанной директории + params = { + "folder_path": folder_path, + "limit": limit, + "offset": 0, + "sort_by": "name", + "sort_direction": "ASC" + } + + result = self._make_api_request("SYNO.FileStation.List", "list", version=2, params=params) + + if not result: + logger.error(f"Failed to list files in {folder_path}") + return {"success": False, "error": "api_error"} + + return { + "success": True, + "items": result.get("files", []), + "path": folder_path, + "is_root": False, + "total": result.get("total", 0) + } + + except Exception as e: + logger.error(f"Error browsing files: {str(e)}") + return {"success": False, "error": str(e)} + + def manage_service(self, service_name: str, action: str = "status") -> Dict[str, Any]: + """Управление системным сервисом + + Args: + service_name: Имя сервиса + action: Действие (status/start/stop/restart) + + Returns: + Словарь с результатом операции + """ + logger.info(f"Managing service {service_name}, action: {action}") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for service management") + return {"success": False, "error": "authentication_failed"} + + try: + # Проверяем доступное API для управления сервисами + if action == "status": + result = self._make_api_request("SYNO.Core.Service", "get", version=1, + params={"service": service_name}) + else: + result = self._make_api_request("SYNO.Core.Service", action, version=1, + params={"service": service_name}) + + if not result: + logger.error(f"Failed to {action} service {service_name}") + return {"success": False, "error": "api_error"} + + return { + "success": True, + "service": service_name, + "action": action, + "result": result, + "status": result.get("status") if action == "status" else "completed" + } + + except Exception as e: + logger.error(f"Error managing service {service_name}: {str(e)}") + return {"success": False, "error": str(e)} + + def search_files(self, pattern: str, folder_path: str = "", limit: int = 100) -> Dict[str, Any]: + """Поиск файлов по шаблону + + Args: + pattern: Шаблон для поиска + folder_path: Путь к папке для поиска (пустая строка для всех общих папок) + limit: Максимальное количество результатов + + Returns: + Словарь с найденными файлами + """ + logger.info(f"Searching for {pattern} in {folder_path or 'all shared folders'}") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for file search") + return {"success": False, "error": "authentication_failed"} + + try: + if not folder_path: + # Получаем список всех общих папок для поиска + shares_result = self._make_api_request("SYNO.FileStation.List", "list_share", version=2) + + if not shares_result: + logger.error("Failed to list shared folders for search") + return {"success": False, "error": "api_error"} + + # Формируем список путей для поиска + folder_paths = [share.get("path") for share in shares_result.get("shares", [])] + else: + folder_paths = [folder_path] + + # Запускаем поиск + params = { + "folder_path": folder_paths, + "pattern": pattern, + "limit": limit, + "offset": 0 + } + + result = self._make_api_request("SYNO.FileStation.Search", "start", version=2, params=params) + + if not result: + logger.error(f"Failed to start search for {pattern}") + return {"success": False, "error": "api_error"} + + # Получаем taskid для проверки результатов + taskid = result.get("taskid") + if not taskid: + logger.error("No taskid received for search") + return {"success": False, "error": "no_task_id"} + + # Ожидаем завершения поиска + search_result = {"finished": False, "progress": 0} + for _ in range(10): # Максимум 10 попыток + search_status = self._make_api_request("SYNO.FileStation.Search", "status", version=2, + params={"taskid": taskid}) + + if not search_status: + break + + search_result["progress"] = search_status.get("progress", 0) + + if search_status.get("finished", False): + search_result["finished"] = True + break + + time.sleep(0.5) # Пауза между запросами + + # Получаем результаты поиска + if search_result["finished"]: + list_result = self._make_api_request("SYNO.FileStation.Search", "list", version=2, + params={"taskid": taskid, "limit": limit}) + + if list_result: + files = list_result.get("files", []) + return { + "success": True, + "pattern": pattern, + "results": files, + "total": list_result.get("total", len(files)) + } + + # Если не удалось получить результаты, останавливаем поиск + self._make_api_request("SYNO.FileStation.Search", "stop", version=2, + params={"taskid": taskid}) + + return { + "success": False, + "error": "search_timeout", + "progress": search_result["progress"] + } + + except Exception as e: + logger.error(f"Error searching files: {str(e)}") + return {"success": False, "error": str(e)} + + def get_backup_status(self) -> Dict[str, Any]: + """Получение информации о резервном копировании""" + logger.info("Getting backup status information") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for backup status request") + return {"success": False, "error": "authentication_failed"} + + try: + # Пробуем получить информацию о Hyper Backup + hyper_result = self._make_api_request("SYNO.Backup.Repository", "list", version=1) + + # Пробуем получить информацию о задачах Time Backup + time_result = self._make_api_request("SYNO.BackupService.Task", "list", version=1) + + # Проверяем статус резервного копирования USB + usb_result = self._make_api_request("SYNO.USBCopy", "status", version=1) + + backups = { + "hyper_backup": hyper_result.get("backups", []) if hyper_result else [], + "time_backup": time_result.get("tasks", []) if time_result else [], + "usb_copy": {"enabled": usb_result.get("enable", False) if usb_result else False} + } + + return { + "success": True, + "backups": backups, + "available_apis": { + "hyper_backup": hyper_result is not None, + "time_backup": time_result is not None, + "usb_copy": usb_result is not None + } + } + + except Exception as e: + logger.error(f"Error getting backup status: {str(e)}") + return {"success": False, "error": str(e)} + + def check_for_updates(self) -> Dict[str, Any]: + """Проверка наличия обновлений системы""" + logger.info("Checking for system updates") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for update check") + return {"success": False, "error": "authentication_failed"} + + try: + # Получаем текущую информацию о системе + system_info = self._make_api_request("SYNO.DSM.Info", "getinfo", version=2) + + if not system_info: + logger.error("Failed to get system info for update check") + return {"success": False, "error": "api_error"} + + # Проверяем наличие обновлений + update_result = self._make_api_request("SYNO.Core.Upgrade.Server", "check", version=1) + + # Получаем настройки автоматического обновления + settings_result = self._make_api_request("SYNO.Core.Upgrade.Server.Setting", "get", version=1) + + # Получаем информацию о доступных обновлениях + update_info = self._make_api_request("SYNO.Core.Upgrade.Server.Download", "list", version=1) + + current_version = system_info.get("version_string", "unknown") + auto_update_enabled = settings_result.get("auto_update", False) if settings_result else False + + updates = [] + if update_info and "updates" in update_info: + updates = update_info.get("updates", []) + + update_available = len(updates) > 0 + + return { + "success": True, + "current_version": current_version, + "update_available": update_available, + "auto_update_enabled": auto_update_enabled, + "updates": updates + } + + except Exception as e: + logger.error(f"Error checking for updates: {str(e)}") + return {"success": False, "error": str(e)} + + def get_quota_info(self) -> Dict[str, Any]: + """Получение информации о квотах пользователей""" + logger.info("Getting user quota information") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for quota info request") + return {"success": False, "error": "authentication_failed"} + + try: + # Получаем список пользователей + users_result = self._make_api_request("SYNO.Core.User", "list", version=1) + + if not users_result: + logger.error("Failed to get user list for quota info") + return {"success": False, "error": "api_error"} + + users = users_result.get("users", []) + user_quotas = [] + + # Получаем квоты для каждого пользователя + for user in users: + user_name = user.get("name") + if not user_name: + continue + + quota_result = self._make_api_request("SYNO.Core.Quota", "get", version=1, + params={"user_name": user_name}) + + if quota_result and "quotas" in quota_result: + user_quotas.append({ + "user": user_name, + "quotas": quota_result.get("quotas", []) + }) + + return { + "success": True, + "user_quotas": user_quotas + } + + except Exception as e: + logger.error(f"Error getting quota info: {str(e)}") + return {"success": False, "error": str(e)} diff --git a/.history/src/api/synology_20250830101145.py b/.history/src/api/synology_20250830101145.py new file mode 100644 index 0000000..53baace --- /dev/null +++ b/.history/src/api/synology_20250830101145.py @@ -0,0 +1,1944 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Модуль для взаимодействия с API Synology NAS +""" + +import requests +from requests.adapters import HTTPAdapter +import json +import logging +import time +import urllib3 +from urllib3.util import Retry +from typing import Dict, Any, Optional, List +import socket +import struct +from time import sleep + +from src.config.config import ( + SYNOLOGY_HOST, + SYNOLOGY_PORT, + SYNOLOGY_USERNAME, + SYNOLOGY_PASSWORD, + SYNOLOGY_SECURE, + SYNOLOGY_TIMEOUT, + SYNOLOGY_MAC, + WOL_PORT, + SYNOLOGY_API_VERSION, + SYNOLOGY_POWER_API, + SYNOLOGY_INFO_API +) +from src.api.api_discovery import discover_available_apis, find_compatible_api + +# Отключение предупреждений о небезопасных SSL-соединениях +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + +logger = logging.getLogger(__name__) + +class SynologyAPI: + """Класс для взаимодействия с API Synology NAS""" + + def __init__(self): + """Инициализация класса SynologyAPI""" + logger.info("Creating API with auto-retry and connection pool") + logger.debug(f"Connection details: Host={SYNOLOGY_HOST}, Port={SYNOLOGY_PORT}, Secure={SYNOLOGY_SECURE}") + + self.protocol = "https" if SYNOLOGY_SECURE else "http" + self.base_url = f"{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi" + self.sid = None + self.session = requests.Session() + + # Настройка SSL + if self.protocol == "https": + logger.debug("SSL enabled, disabling certificate verification for internal network") + self.session.verify = False # Отключаем проверку SSL для внутренней сети + + # Добавляем пользовательские заголовки для улучшения совместимости с API + custom_headers = { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36', + 'Accept': 'application/json, text/javascript, */*; q=0.01', + 'Accept-Language': 'ru-RU,ru;q=0.9,en-US;q=0.8,en;q=0.7', + 'X-Requested-With': 'XMLHttpRequest', + 'Connection': 'keep-alive', + 'Referer': f'{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/' + } + self.session.headers.update(custom_headers) + logger.debug("Added browser-like headers for API compatibility") + + # Добавляем повторные попытки для HTTP-запросов + retry_strategy = Retry( + total=5, # Увеличиваем количество попыток + status_forcelist=[429, 500, 502, 503, 504, 404], + allowed_methods=["GET", "POST"], + backoff_factor=1.5, # Увеличиваем задержку между попытками + respect_retry_after_header=True + ) + adapter = HTTPAdapter( + max_retries=retry_strategy, + pool_connections=3, + pool_maxsize=10 + ) + self.session.mount("http://", adapter) + self.session.mount("https://", adapter) + + # Таймауты будут указаны в запросах + self.default_timeout = (SYNOLOGY_TIMEOUT, SYNOLOGY_TIMEOUT*2) + logger.debug(f"Setting default request timeout: {self.default_timeout}") + + # Кэш для хранения результатов запросов + self._cache = {} + self._cache_ttl = {} + self._last_online_check = 0 + self._last_online_status = False + self._online_check_interval = 30 # Интервал для кэширования проверок онлайн-статуса + + # Время последней успешной аутентификации и срок действия сессии + self._last_auth_time = 0 + self._auth_expiry = 3600 # По умолчанию 1 час + + # Информация о доступных API + self._available_apis = {} + self._api_info_ttl = 0 + + # Инициализируем API version resolver для автоматического определения совместимых API + self.api_resolver = None # Будет создан при необходимости + + def login(self) -> bool: + """Авторизация в API Synology NAS""" + # Сбрасываем SID для новой сессии + self.sid = None + + logger.info("Attempting to authenticate with Synology NAS...") + logger.debug(f"Base URL: {self.base_url}") + + # Проверяем доступность NAS перед запросом с помощью прямой TCP-проверки + # Избегаем вызова is_online(), чтобы не создавать рекурсию + online_status = self._check_tcp_connection() + if not online_status: + logger.error("Cannot login: Synology NAS is not reachable") + return False + + # Пробуем различные версии API для аутентификации + # Начинаем с версии 3, которая показала лучшую совместимость в тестах + auth_versions_to_try = [3, 2, 1, 6] # Пробуем сначала более стабильные версии + + for auth_version in auth_versions_to_try: + try: + # Определяем путь к API аутентификации + auth_path = "entry.cgi" # Используем entry.cgi вместо auth.cgi по умолчанию + + # Проверка информации API для определения доступных версий API + api_info_url = f"{self.base_url}/entry.cgi" + api_info_params = { + "api": "SYNO.API.Info", + "version": "1", + "method": "query", + "query": "SYNO.API.Auth" + } + + logger.debug(f"Querying API info for auth version {auth_version}") + try: + api_info_response = self.session.get( + api_info_url, + params=api_info_params, + timeout=self.default_timeout, + verify=False + ) + + if api_info_response.status_code == 200: + api_info_data = api_info_response.json() + if api_info_data.get("success"): + auth_info = api_info_data.get("data", {}).get("SYNO.API.Auth", {}) + max_version = auth_info.get("maxVersion", 6) + min_version = auth_info.get("minVersion", 1) + auth_path = auth_info.get("path", "entry.cgi") + logger.debug(f"Auth API versions: min={min_version}, max={max_version}, path={auth_path}") + + # Проверяем поддержку текущей версии + if auth_version < min_version or auth_version > max_version: + logger.warning(f"Auth API version {auth_version} not supported (min={min_version}, max={max_version}), trying next version") + continue + else: + logger.warning("Failed to query API info, using default auth path") + else: + logger.warning(f"API info request failed with status {api_info_response.status_code}, using default auth path") + except Exception as e: + logger.warning(f"Error querying API info: {str(e)}, using default auth path") + + # Основной запрос авторизации + url = f"{self.base_url}/{auth_path}" + params = { + "api": "SYNO.API.Auth", + "version": str(auth_version), + "method": "login", + "account": SYNOLOGY_USERNAME, + "passwd": SYNOLOGY_PASSWORD, + "session": "SynologyPowerControlBot", + "format": "cookie" + } + + # Для версии 6+ используем немного другой формат + if auth_version >= 6: + params["enable_syno_token"] = "yes" + + logger.debug(f"Sending auth request to {url} with API version {auth_version}") + start_time = time.time() + response = self.session.get( + url, + params=params, + timeout=self.default_timeout, + verify=False + ) + elapsed_time = time.time() - start_time + logger.debug(f"Auth request completed in {elapsed_time:.2f}s with status code {response.status_code}") + + # Проверка статуса HTTP + if response.status_code != 200: + logger.error(f"HTTP error: {response.status_code}") + logger.debug(f"Response content: {response.text[:200]}") + continue # Пробуем следующую версию + + try: + data = response.json() + except json.JSONDecodeError: + logger.error("Failed to decode JSON response") + logger.debug(f"Response content: {response.text[:200]}") + continue # Пробуем следующую версию + + if data.get("success"): + self.sid = data.get("data", {}).get("sid") + self._last_auth_time = time.time() + logger.info(f"Successfully logged in to Synology NAS using API version {auth_version}") + logger.debug(f"Session ID obtained: {self.sid[:5] if self.sid else 'None'}...") + + # Получаем и сохраняем токен SYNO, если он есть + syno_token = data.get("data", {}).get("synotoken") + if syno_token: + self.session.headers.update({'X-SYNO-TOKEN': syno_token}) + logger.debug("Added X-SYNO-TOKEN header for improved API compatibility") + + # Также добавляем SID в cookies для улучшения совместимости + self.session.cookies.update({ + 'id': self.sid, + 'sid': self.sid + }) + logger.debug("Added SID to session cookies for improved compatibility") + + # Проверка валидности полученной сессии с помощью простого запроса + # Будем использовать SYNO.API.Info без проверки сложных методов + + # Даем системе немного времени для инициализации сессии + time.sleep(0.5) + + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to log in with version {auth_version}: Error code {error_code}") + + # Если ошибка связана с версией API, пробуем следующую версию + if error_code in [104, 105]: + logger.warning(f"Auth version {auth_version} not supported, trying next version") + continue + + # Дополнительная диагностика + if error_code == 400: + logger.error("Authentication error: Invalid credentials") + elif error_code == 401: + logger.error("Authentication error: Account disabled") + elif error_code == 402: + logger.error("Authentication error: Permission denied") + elif error_code == 403: + logger.error("Authentication error: 2-factor authentication required") + elif error_code == 404: + logger.error("Authentication error: Failed to authenticate with 2-factor authentication") + + # Если ошибка связана с неверными учетными данными, нет смысла пробовать другие версии API + if error_code in [400, 401, 402, 403, 404]: + return False + + except requests.exceptions.Timeout: + logger.error(f"Connection timeout during auth with version {auth_version}") + continue # Пробуем следующую версию + except requests.exceptions.ConnectionError as e: + logger.error(f"Connection error during auth with version {auth_version}: {str(e)}") + continue # Пробуем следующую версию + except requests.RequestException as e: + logger.error(f"Request error during auth with version {auth_version}: {str(e)}") + continue # Пробуем следующую версию + except Exception as e: + logger.error(f"Unexpected error during login with version {auth_version}: {str(e)}", exc_info=True) + continue # Пробуем следующую версию + + # Если все версии не сработали + logger.error("Failed to authenticate with any API version") + return False + + def _validate_session(self) -> bool: + """Проверяет валидность сессии после авторизации""" + if not self.sid: + return False + + # Попробуем сделать простой запрос для проверки сессии + test_apis = [ + {"api": "SYNO.Core.System", "method": "info", "version": 1}, + {"api": "SYNO.DSM.Info", "method": "getinfo", "version": 1} + ] + + for test_api in test_apis: + try: + url = f"{self.base_url}/entry.cgi" + params = { + "api": test_api["api"], + "version": str(test_api["version"]), + "method": test_api["method"], + "_sid": self.sid + } + + response = self.session.get(url, params=params, timeout=self.default_timeout, verify=False) + + if response.status_code == 200: + data = response.json() + if data.get("success"): + logger.debug(f"Session validation successful using {test_api['api']}") + return True + else: + error_code = data.get("error", {}).get("code", -1) + if error_code != 119: # Не сессия истекла + logger.debug(f"Session validation with {test_api['api']} returned error code {error_code}") + return True # Считаем сессию валидной, если ошибка не связана с истечением сессии + except Exception as e: + logger.warning(f"Error during session validation with {test_api['api']}: {str(e)}") + + logger.warning("Session validation failed with all test APIs") + return False + + def logout(self) -> bool: + """Выход из API Synology NAS""" + if not self.sid: + return True + + try: + url = f"{self.base_url}/auth.cgi" + params = { + "api": "SYNO.API.Auth", + "version": "1", + "method": "logout", + "session": "SynologyPowerControlBot", + "_sid": self.sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + self.sid = None + logger.info("Successfully logged out from Synology NAS") + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to log out from Synology NAS: Error code {error_code}") + return False + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + return False + + def _make_api_request(self, api_name: str, method: str, version: int = 1, + params: Optional[Dict[str, Any]] = None, retry_auth: bool = True, retry_count: int = 0) -> Optional[Dict[str, Any]]: + """Обобщенный метод для выполнения API запросов с обработкой ошибок""" + # Ограничение на количество повторных попыток + if retry_count >= 3: + logger.error(f"Too many retries for {api_name}.{method}, giving up") + return None + + # Проверка наличия авторизации + if not self.sid and not self.login(): + logger.error(f"Not authenticated for API request: {api_name}.{method}") + return None + + # Проверка информации API для определения пути и поддерживаемой версии + api_info_url = f"{self.base_url}/entry.cgi" + api_info_params = { + "api": "SYNO.API.Info", + "version": "1", + "method": "query", + "query": api_name + } + + api_path = "entry.cgi" + try: + logger.debug(f"Querying API info for {api_name}") + api_info_response = self.session.get( + api_info_url, + params=api_info_params, + timeout=self.default_timeout, + verify=False + ) + + if api_info_response.status_code == 200: + api_info_data = api_info_response.json() + if api_info_data.get("success"): + api_info = api_info_data.get("data", {}).get(api_name, {}) + if api_info: + max_version = api_info.get("maxVersion", version) + min_version = api_info.get("minVersion", version) + api_path = api_info.get("path", "entry.cgi") + + # Проверка, поддерживается ли запрошенная версия + if version < min_version: + logger.warning(f"API version {version} for {api_name} is below minimum {min_version}, using {min_version}") + version = min_version + elif version > max_version: + logger.warning(f"API version {version} for {api_name} exceeds maximum {max_version}, using {max_version}") + version = max_version + + logger.debug(f"Using API path: {api_path}, version: {version}") + else: + logger.warning(f"API {api_name} not found in API info, using defaults") + except Exception as e: + logger.warning(f"Error querying API info for {api_name}: {str(e)}, using defaults") + + # Подготовка базовых параметров запроса + base_params = { + "api": api_name, + "version": str(version), + "method": method, + "_sid": self.sid # Используем _sid вместо sid для лучшей совместимости + } + + # Добавление дополнительных параметров, если они заданы + if params: + base_params.update(params) + + url = f"{self.base_url}/{api_path}" + logger.debug(f"API request: {api_name}.{method} v{version} to {url}") + logger.debug(f"Full request params: {base_params}") + + try: + start_time = time.time() + response = self.session.get( + url, + params=base_params, + timeout=self.default_timeout, + verify=False + ) + elapsed_time = time.time() - start_time + logger.debug(f"API request completed in {elapsed_time:.2f}s with status {response.status_code}") + + # Проверка статуса HTTP + if response.status_code != 200: + logger.error(f"HTTP error: {response.status_code} for {api_name}.{method}") + + # Повторная попытка при ошибках соединения + if response.status_code in [500, 502, 503, 504]: + logger.info(f"Server error, retrying request for {api_name}.{method}") + sleep(2) + return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1) + + return None + + try: + data = response.json() + except json.JSONDecodeError: + logger.error(f"Failed to decode JSON response for {api_name}.{method}") + logger.debug(f"Response content: {response.text[:200]}") + + # Повторная попытка при ошибках декодирования + logger.info(f"JSON decode error, retrying request for {api_name}.{method}") + sleep(2) + return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1) + + if data.get("success"): + logger.info(f"API request successful for {api_name}.{method}") + return data.get("data", {}) + else: + error_code = data.get("error", {}).get("code", -1) + error_desc = self._get_error_description(error_code) + logger.error(f"API error for {api_name}.{method}: {error_code} - {error_desc}") + + # Ошибки доступа или прав часто встречаются, но они не критичные + # Например, ошибка 102 означает, что нет прав, но NAS доступен + if error_code in [102, 103, 104, 105]: + logger.warning(f"Permission-related error for {api_name}.{method}: {error_code}") + # Возвращаем пустой словарь вместо None, + # чтобы вызывающий код мог понять, что запрос выполнен + return {} + + # Если ошибка связана с авторизацией и нам разрешено повторить попытку + if error_code in [106, 107, 119] and retry_auth: + logger.info(f"Session error (code {error_code}), creating fresh session...") + self.sid = None # Сбрасываем SID + + # Для ошибки 119 (Session timeout) дадим системе немного времени + if error_code == 119: + logger.info("Session timeout detected, waiting before retry...") + sleep(3) + + if self.login(): + logger.info("Re-authenticated with fresh session, retrying API request...") + # Рекурсивный вызов, но со счетчиком повторов + return self._make_api_request(api_name, method, version, params, False, retry_count + 1) + + # Для некоторых ошибок можно автоматически повторить запрос + if error_code in [408, 429, 500, 502, 503, 504]: + logger.info(f"Temporary error {error_code}, retrying request for {api_name}.{method}") + sleep(2) + return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1) + + return None + + except requests.exceptions.Timeout: + logger.error(f"Request timeout for {api_name}.{method}") + + # Повторная попытка при таймауте + if retry_count < 2: + logger.info(f"Timeout, retrying request for {api_name}.{method}") + sleep(2) + return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1) + + return None + except requests.exceptions.ConnectionError as e: + logger.error(f"Connection error for {api_name}.{method}: {str(e)}") + + # Повторная попытка при ошибке соединения + if retry_count < 2: + logger.info(f"Connection error, retrying request for {api_name}.{method}") + sleep(2) + return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1) + + return None + except Exception as e: + logger.error(f"Unexpected error during {api_name}.{method}: {str(e)}") + return None + + def get_system_status(self) -> Dict[str, Any]: + """Получение статуса системы""" + # Проверяем доступность системы + if not self.is_online(): + logger.info("Device is offline, skipping API request") + return {"status": "offline"} + + # Проверяем, есть ли кэшированный результат + cache_key = "system_status" + current_time = time.time() + if cache_key in self._cache and current_time - self._cache_ttl.get(cache_key, 0) < 60: + logger.debug("Using cached system status") + return self._cache[cache_key] + + # Используем рекомендованный API для получения информации о системе + logger.info(f"Getting system info using {SYNOLOGY_INFO_API} v{SYNOLOGY_API_VERSION}") + + # Для SYNO.DSM.Info метод getinfo, для других обычно используется get или info + if SYNOLOGY_INFO_API == "SYNO.DSM.Info": + method = "getinfo" + else: + method = "get" + + result = self._make_api_request(SYNOLOGY_INFO_API, method, version=SYNOLOGY_API_VERSION) + + if result: + logger.info(f"Successfully retrieved system info using {SYNOLOGY_INFO_API}") + + # Формируем расширенный ответ с дополнительной информацией + system_info = { + "status": "online", + "hostname": result.get("hostname", "unknown"), + "model": result.get("model", "unknown"), + "version": result.get("version", "unknown"), + "uptime": result.get("uptime", 0), + "time": current_time, + "is_online": True, + "api_used": SYNOLOGY_INFO_API + } + + # Сохраняем в кэше + self._cache[cache_key] = system_info + self._cache_ttl[cache_key] = current_time + + return system_info + + # Если основной API не сработал, пробуем резервные варианты + logger.warning(f"Failed to retrieve system info with {SYNOLOGY_INFO_API}, trying fallback APIs") + + # Пробуем резервные API + apis_to_try = [ + {"name": "SYNO.DSM.Info", "method": "getinfo", "version": 2}, + {"name": "SYNO.Core.System", "method": "info", "version": 1}, + {"name": "SYNO.Core.System.Status", "method": "get", "version": 1}, + {"name": "SYNO.Core.System.Info", "method": "get", "version": 1}, + ] + + for api in apis_to_try: + if api["name"] == SYNOLOGY_INFO_API: + continue # Пропускаем уже проверенный API + + logger.info(f"Trying to get system info using {api['name']}.{api['method']} v{api['version']}") + result = self._make_api_request(api["name"], api["method"], version=api["version"]) + + if result: + logger.info(f"Successfully retrieved system info using {api['name']}") + + # Формируем расширенный ответ с дополнительной информацией + system_info = { + "status": "online", + "hostname": result.get("hostname", "unknown"), + "model": result.get("model", "unknown"), + "version": result.get("version", "unknown"), + "uptime": result.get("uptime", 0), + "time": current_time, + "is_online": True, + "api_used": api["name"] + } + + # Сохраняем в кэше + self._cache[cache_key] = system_info + self._cache_ttl[cache_key] = current_time + + return system_info + + # Если все запросы не удались, но система онлайн, возвращаем базовую информацию + logger.warning("Failed to retrieve system info with all API methods") + return { + "status": "error", + "error": "Failed to fetch system information", + "is_online": True, + "time": current_time + } + + def shutdown_system(self) -> bool: + """Выключение системы""" + # Проверяем, включено ли устройство перед попыткой его выключить + if not self.is_online(force_check=True): + logger.info("Synology NAS is already offline, no need to shut down") + return True + + logger.info("Attempting to shutdown Synology NAS...") + + # Попробуем сначала использовать предпочтительный API для управления питанием + logger.info(f"Trying shutdown with {SYNOLOGY_POWER_API} v{SYNOLOGY_API_VERSION}") + + # Для SYNO.Core.Hardware.PowerRecovery используется метод setPowerOnState + # Для других API обычно используется метод shutdown или reboot + if SYNOLOGY_POWER_API == "SYNO.Core.Hardware.PowerRecovery": + # Для этого API нужны специальные параметры + params = {"state": "powerbtn"} # powerbtn имитирует нажатие кнопки питания + result = self._make_api_request(SYNOLOGY_POWER_API, "setPowerOnState", version=SYNOLOGY_API_VERSION, params=params) + else: + # Пробуем стандартный метод + result = self._make_api_request(SYNOLOGY_POWER_API, "shutdown", version=SYNOLOGY_API_VERSION) + + if result is not None: + logger.info(f"Successfully initiated system shutdown using {SYNOLOGY_POWER_API}") + + # Даем системе время начать процесс выключения + logger.info("Waiting for shutdown to initialize...") + sleep(5) + + # Проверяем статус + if not self.is_online(force_check=True): + logger.info("System is now offline. Shutdown confirmed successful.") + return True + else: + logger.info("System still appears to be online, but shutdown may be in progress.") + return True + else: + logger.warning(f"Failed to shutdown using {SYNOLOGY_POWER_API}, trying alternative methods") + + # Если не сработал основной метод, пробуем резервные варианты + # Проверка всех доступных методов API для выключения + apis_to_try = [ + {"name": "SYNO.Core.System", "method": "shutdown", "version": 3}, + {"name": "SYNO.Core.System.Power", "method": "shutdown", "version": 1}, + {"name": "SYNO.DSM.Power", "method": "shutdown", "version": 1}, + {"name": "SYNO.System.Power", "method": "shutdown", "version": 1}, + {"name": "SYNO.DSM.System", "method": "shutdown", "version": 1} + ] + + # Проверяем доступные API + try: + api_info_url = f"{self.base_url}/entry.cgi" + api_info_params = { + "api": "SYNO.API.Info", + "version": "1", + "method": "query", + "query": "SYNO.Core.System.Power,SYNO.DSM.Power,SYNO.Core.System,SYNO.System.Power,SYNO.DSM.System" + } + + logger.debug("Checking available shutdown APIs") + api_info_response = self.session.get( + api_info_url, + params=api_info_params, + timeout=self.default_timeout, + verify=False + ) + + if api_info_response.status_code == 200: + api_info_data = api_info_response.json() + if api_info_data.get("success"): + available_apis = api_info_data.get("data", {}) + logger.debug(f"Available APIs: {list(available_apis.keys())}") + + # Фильтруем только доступные API + filtered_apis = [] + for api in apis_to_try: + if api["name"] in available_apis: + api_info = available_apis[api["name"]] + # Проверка версии API + if api_info.get("minVersion", 1) <= api["version"] <= api_info.get("maxVersion", 1): + filtered_apis.append(api) + logger.debug(f"Adding {api['name']} to available shutdown APIs") + else: + logger.debug(f"API {api['name']} version {api['version']} not supported. Supported: {api_info.get('minVersion')}-{api_info.get('maxVersion')}") + + if filtered_apis: + apis_to_try = filtered_apis + else: + logger.warning("No compatible APIs found, trying all methods as fallback") + else: + logger.warning("Failed to query API info, using default methods") + else: + logger.warning(f"API info request failed with status {api_info_response.status_code}, using default methods") + except Exception as e: + logger.warning(f"Error querying API info: {str(e)}, using default methods") + + # Пробуем все доступные методы по порядку + for api in apis_to_try: + logger.info(f"Trying shutdown with {api['name']}.{api['method']} v{api['version']}") + result = self._make_api_request(api["name"], api["method"], version=api["version"]) + + if result is not None: + logger.info(f"Successfully initiated system shutdown using {api['name']}") + + # Даем системе время начать процесс выключения + logger.info("Waiting for shutdown to initialize...") + sleep(5) + + # Проверяем статус + if not self.is_online(force_check=True): + logger.info("System is now offline. Shutdown confirmed successful.") + return True + else: + logger.info("System still appears to be online, but shutdown may be in progress.") + return True + else: + logger.warning(f"Failed to shutdown using {api['name']}.{api['method']} v{api['version']}") + + # Если ни один метод не сработал, но система стала недоступна + if not self.is_online(force_check=True): + logger.info("System appears to be shutting down despite API errors") + return True + + logger.error("Failed to shutdown system after trying multiple APIs") + return False + + def reboot_system(self) -> bool: + """Перезагрузка системы""" + # Проверяем, включена ли система + if not self.is_online(force_check=True): + logger.error("Cannot reboot: System is offline") + return False + + logger.info("Attempting to reboot Synology NAS...") + + # Список API и методов для попытки перезагрузки + apis_to_try = [ + {"api": "SYNO.Core.System", "method": "reboot", "version": 1}, + {"api": "SYNO.DSM.System", "method": "reboot", "version": 1}, + {"api": "SYNO.Core.System.Power", "method": "restart", "version": 1}, + {"api": "SYNO.DSM.Power", "method": "reboot", "version": 1}, + {"api": "SYNO.Core.System", "method": "reboot", "version": 3}, + {"api": "SYNO.System.Power", "method": "reboot", "version": 1}, + {"api": "SYNO.Core.Hardware.NeedReboot", "method": "reboot", "version": 1} + ] + + # Добавляем настроенный в конфигурации API, если он отличается от уже добавленных + already_added = [item["api"] for item in apis_to_try] + if SYNOLOGY_POWER_API not in already_added: + for method in ["restart", "reboot"]: + apis_to_try.append({ + "api": SYNOLOGY_POWER_API, + "method": method, + "version": SYNOLOGY_API_VERSION + }) + + # Перебираем все возможные API и методы + for api_info in apis_to_try: + try: + logger.info(f"Trying reboot with {api_info['api']} API using method {api_info['method']}") + result = self._make_api_request(api_info['api'], api_info['method'], version=api_info['version']) + if result is not None: + logger.info(f"Successfully initiated system reboot using {api_info['api']} API") + + # Даем системе время начать процесс перезагрузки + logger.info("Waiting for reboot to initialize...") + sleep(5) + + # Ждем, пока система станет недоступна (признак перезагрузки) + reboot_started = False + for i in range(12): # Проверяем в течение 60 секунд (12 * 5сек) + if not self.is_online(force_check=True): + logger.info(f"System went offline after {i*5} seconds, reboot in progress") + reboot_started = True + break + logger.debug(f"System still online, waiting... ({i+1}/12)") + sleep(5) + + if reboot_started: + # Успешный вызов API и система ушла оффлайн - это признак успешной перезагрузки + return True + else: + # Успешный вызов API, но система не ушла оффлайн + logger.warning("System did not go offline after reboot command, but command was accepted") + # Даже если система не ушла оффлайн, команда могла быть принята + return True + except Exception as e: + logger.error(f"Error during reboot with {api_info['api']}: {str(e)}") + + # Если все попытки не удались, возвращаем False + logger.error("All reboot attempts failed") + return False + + # Получаем список доступных API + try: + api_info_url = f"{self.base_url}/entry.cgi" + api_info_params = { + "api": "SYNO.API.Info", + "version": "1", + "method": "query", + "query": "SYNO.Core.System.Power,SYNO.DSM.Power,SYNO.Core.System,SYNO.System.Power,SYNO.DSM.System,SYNO.Core.Hardware.NeedReboot" + } + + logger.debug("Checking available reboot APIs") + api_info_response = self.session.get( + api_info_url, + params=api_info_params, + timeout=self.default_timeout, + verify=False + ) + + if api_info_response.status_code == 200: + api_info_data = api_info_response.json() + if api_info_data.get("success"): + available_apis = api_info_data.get("data", {}) + logger.debug(f"Available APIs: {list(available_apis.keys())}") + + # Фильтруем только доступные API + filtered_apis = [] + for api in apis_to_try: + if api["name"] in available_apis: + api_info = available_apis[api["name"]] + # Проверка версии API + if api_info.get("minVersion", 1) <= api["version"] <= api_info.get("maxVersion", 1): + filtered_apis.append(api) + logger.debug(f"Adding {api['name']} to available reboot APIs") + else: + logger.debug(f"API {api['name']} version {api['version']} not supported. Supported: {api_info.get('minVersion')}-{api_info.get('maxVersion')}") + + if filtered_apis: + apis_to_try = filtered_apis + else: + logger.warning("No compatible APIs found, trying all methods as fallback") + else: + logger.warning("Failed to query API info, using default methods") + else: + logger.warning(f"API info request failed with status {api_info_response.status_code}, using default methods") + except Exception as e: + logger.warning(f"Error querying API info: {str(e)}, using default methods") + + # Пробуем все доступные методы по порядку + for api in apis_to_try: + logger.info(f"Trying reboot with {api['name']}.{api['method']} v{api['version']}") + result = self._make_api_request(api["name"], api["method"], version=api["version"]) + + if result is not None: + logger.info(f"Successfully initiated system reboot using {api['name']}") + + # Даем системе время начать процесс перезагрузки + logger.info("Waiting for reboot to initialize...") + sleep(5) + + # Ждем, пока система станет недоступна (признак перезагрузки) + reboot_started = False + for i in range(12): # Проверяем в течение 60 секунд (12 * 5сек) + if not self.is_online(force_check=True): + logger.info(f"System went offline after {i*5} seconds, reboot in progress") + reboot_started = True + break + logger.debug(f"System still online, waiting... ({i+1}/12)") + sleep(5) + + if reboot_started: + # Ждем, пока система снова станет доступна + logger.info("Waiting for system to come back online...") + return self.wait_for_boot(max_attempts=30, delay=10) + else: + logger.warning("System did not go offline after reboot command, but command was accepted") + # Даже если система не ушла оффлайн, команда могла быть принята + return True + else: + logger.warning(f"Failed to reboot using {api['name']}.{api['method']} v{api['version']}") + + logger.error("Failed to reboot system after trying multiple APIs") + return False + + def _get_error_description(self, error_code: int) -> str: + """Получение описания ошибки по коду""" + error_descriptions = { + 100: "Unknown error", + 101: "Invalid parameter", + 102: "API does not exist", + 103: "Method does not exist", + 104: "Version does not support", + 105: "Permission denied", + 106: "Session timeout", + 107: "Session interrupted by duplicate login", + 400: "Invalid credentials", + 401: "Account disabled", + 402: "Permission denied", + 403: "2FA required", + 404: "Failed to authenticate with 2FA" + } + return error_descriptions.get(error_code, "Unknown error code") + + def _check_tcp_connection(self) -> bool: + """Проверка базового TCP-соединения с Synology NAS""" + try: + socket_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + socket_obj.settimeout(SYNOLOGY_TIMEOUT) + result = socket_obj.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT)) + socket_obj.close() + + return result == 0 + except socket.error as e: + logger.error(f"Socket error during connection check: {str(e)}") + return False + except Exception as e: + logger.error(f"Unexpected error during connection check: {str(e)}") + return False + + def is_online(self, force_check=False) -> bool: + """Проверка онлайн-статуса Synology NAS""" + # Используем кэшированное значение, если доступно и не устарело + current_time = time.time() + if not force_check and (current_time - self._last_online_check) < self._online_check_interval: + logger.debug(f"Using cached online status: {self._last_online_status}") + return self._last_online_status + + logger.info("Checking if NAS is online...") + + # Проверяем TCP-соединение + online_status = self._check_tcp_connection() + logger.info(f"Detected Synology NAS online status: {online_status}") + + # Если TCP-соединение успешно и у нас есть действующий SID, + # попробуем более детальную проверку через API + if online_status and self.sid: + logger.info("Trying to fetch more detailed online status through API...") + + # Пробуем разные API для проверки онлайн-статуса + api_checks = [ + {"api": "SYNO.DSM.Info", "version": "2", "method": "getinfo"}, + {"api": "SYNO.Core.System", "version": "1", "method": "info"}, + {"api": "SYNO.Core.System.Status", "version": "1", "method": "get"} + ] + + api_success = False + for api_check in api_checks: + try: + url = f"{self.base_url}/entry.cgi" + params = { + "api": api_check["api"], + "version": api_check["version"], + "method": api_check["method"], + "sid": self.sid + } + + logger.debug(f"Trying online status check with {api_check['api']}") + response = self.session.get( + url, + params=params, + timeout=self.default_timeout, + verify=False + ) + + if response.status_code == 200: + data = response.json() + if data.get("success"): + logger.info(f"API request successful for {api_check['api']}") + logger.info("Synology NAS is online with API access") + api_success = True + break + else: + error_code = data.get("error", {}).get("code", -1) + logger.warning(f"API response indicates an error: {error_code}, but NAS is reachable") + else: + logger.warning(f"API returned status code {response.status_code}") + except Exception as e: + logger.warning(f"API check failed for {api_check['api']}, but TCP connection succeeded: {str(e)}") + + if not api_success: + logger.warning("All API checks failed, but TCP connection is successful") + + # Обновляем кэшированное значение + self._last_online_check = current_time + self._last_online_status = online_status + + return online_status + + def wake_on_lan(self) -> bool: + """Отправка Wake-on-LAN пакета для включения Synology NAS""" + if not SYNOLOGY_MAC: + logger.error("MAC address not configured") + return False + + try: + # Преобразование MAC-адреса в байты + mac_address = SYNOLOGY_MAC.replace(':', '').replace('-', '') + if len(mac_address) != 12: + logger.error(f"Invalid MAC address format: {SYNOLOGY_MAC}") + return False + + try: + mac_bytes = bytes.fromhex(mac_address) + except ValueError as e: + logger.error(f"Failed to parse MAC address: {str(e)}") + logger.error(f"MAC address must contain only hex characters: {SYNOLOGY_MAC}") + return False + + # Создание Magic Packet + magic_packet = b'\xff' * 6 + mac_bytes * 16 + + # Отправка пакета на конкретный адрес + try: + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) + sock.sendto(magic_packet, (SYNOLOGY_HOST, WOL_PORT)) + sock.close() + logger.info(f"Wake-on-LAN packet sent to {SYNOLOGY_MAC} at {SYNOLOGY_HOST}:{WOL_PORT}") + except Exception as e: + logger.error(f"Error sending directed WoL packet: {str(e)}") + return False + + # Для надежности отправляем также широковещательный пакет + try: + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) + + # Используем стандартный широковещательный адрес + broadcast_addr = "255.255.255.255" + sock.sendto(magic_packet, (broadcast_addr, WOL_PORT)) + sock.close() + logger.info(f"Wake-on-LAN broadcast packet sent to {broadcast_addr}:{WOL_PORT}") + except Exception as e: + logger.warning(f"Error sending broadcast WoL packet (non-critical): {str(e)}") + # Не считаем ошибкой, т.к. основной пакет уже отправлен + + return True + + except Exception as e: + logger.error(f"Unexpected error in wake_on_lan: {str(e)}") + return False + + def wait_for_boot(self, max_attempts: int = 30, delay: int = 10) -> bool: + """Ожидание загрузки Synology NAS""" + logger.info(f"Waiting for Synology NAS to boot (timeout: {max_attempts * delay}s)...") + + for attempt in range(max_attempts): + # Принудительно проверяем статус без использования кэша + if self.is_online(force_check=True): + logger.info(f"Synology NAS is online after {attempt + 1} attempts ({(attempt + 1) * delay}s)") + + # Проверяем, что не только сеть доступна, но и API загрузился + api_ready = False + logger.info("Waiting for API services to initialize...") + + for api_check in range(5): # Даем еще до 50 секунд для загрузки API + if self.sid or self.login(): + api_ready = True + logger.info(f"API services are ready after {api_check + 1} attempts") + break + logger.debug(f"API not ready yet, waiting... ({api_check + 1}/5)") + sleep(10) + + if not api_ready: + logger.warning("System is online but API services may not be fully initialized") + + # Дадим дополнительное время для полной загрузки всех сервисов + sleep(delay) + return True + + sleep(delay) + logger.info(f"Waiting for boot... Attempt {attempt + 1}/{max_attempts}") + + logger.error(f"Synology NAS didn't come online after {max_attempts * delay} seconds") + return False + + def power_on(self) -> bool: + """Включение Synology NAS""" + # Принудительная проверка статуса + if self.is_online(force_check=True): + logger.info("Synology NAS is already online") + return True + + logger.info("Powering on Synology NAS via Wake-on-LAN...") + + # Проверяем, настроен ли MAC-адрес + if not SYNOLOGY_MAC: + logger.error("Cannot power on: MAC address not configured in settings") + return False + + # Пробуем отправить несколько WoL пакетов для надежности + success = False + for attempt in range(3): + logger.debug(f"Sending WoL packet, attempt {attempt + 1}/3") + if self.wake_on_lan(): + success = True + break + sleep(1) + + if not success: + logger.error("Failed to send Wake-on-LAN packets") + return False + + # Ожидание загрузки + logger.info("WoL packets sent successfully, waiting for system to boot...") + boot_result = self.wait_for_boot(max_attempts=30, delay=10) + + if boot_result: + # Проверяем доступность API после загрузки + system_status = self.get_system_status() + if system_status.get("status") == "online": + logger.info("System booted successfully with API access") + return True + else: + logger.warning("System appears to be online but API may not be fully ready") + return True + else: + logger.error("System did not come online after WoL") + return False + + def power_off(self) -> bool: + """Выключение Synology NAS""" + if not self.is_online(force_check=True): + logger.info("Synology NAS is already offline") + return True + + logger.info("Powering off Synology NAS...") + + # Список API и методов для попытки выключения + apis_to_try = [ + {"api": "SYNO.Core.System", "method": "shutdown", "version": 1}, + {"api": "SYNO.DSM.System", "method": "shutdown", "version": 1}, + {"api": SYNOLOGY_POWER_API, "method": "shutdown", "version": SYNOLOGY_API_VERSION} + ] + + # Перебираем все возможные API и методы + for api_info in apis_to_try: + try: + logger.info(f"Trying shutdown with {api_info['api']} API using method {api_info['method']}") + api_result = self._make_api_request(api_info['api'], api_info['method'], version=api_info['version']) + if api_result is not None: + logger.info(f"Successfully initiated system shutdown using {api_info['api']} API") + + # Даем системе время начать процесс выключения + logger.info("Waiting for shutdown to initialize...") + sleep(5) + + return True + except Exception as e: + logger.error(f"Error during shutdown with {api_info['api']}: {str(e)}") + + # Если все попытки не удались, возвращаем False + logger.error("All shutdown attempts failed") + return False + + # Если все еще не сработало, используем оригинальный метод shutdown_system + if not result: + result = self.shutdown_system() + + if result: + # Дополнительная проверка, что система действительно выключилась + logger.info("Verifying system is offline...") + for attempt in range(12): # Проверяем в течение 2 минут (12 * 10 сек) + if not self.is_online(force_check=True): + logger.info(f"System confirmed offline after {attempt * 10} seconds") + return True + logger.debug(f"System still shutting down, checking again... ({attempt + 1}/12)") + sleep(10) + + logger.warning("System still appears to be online after shutdown command") + return False + else: + logger.error("Failed to initiate shutdown") + return False + + # Заглушки для расширенных методов + def get_shared_folders(self) -> List[Dict[str, Any]]: + """Получение списка общих папок""" + logger.info("Getting list of shared folders") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for shared folders request") + return [] + + try: + # Запрашиваем список общих папок через FileStation API + result = self._make_api_request("SYNO.FileStation.List", "list_share", version=2) + + if not result: + # Пробуем альтернативный API + logger.info("Trying alternative API for shared folders") + alt_result = self._make_api_request("SYNO.FileStation.List", "list_share", version=1) + if alt_result: + return alt_result.get("shares", []) + return [] + + return result.get("shares", []) + + except Exception as e: + logger.error(f"Error getting shared folders: {str(e)}") + return [] + + def get_system_load(self) -> Dict[str, Any]: + """Получение информации о загрузке системы""" + logger.info("Getting system load information") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for system load request") + return {} + + try: + # Запрашиваем информацию о загрузке системы + result = self._make_api_request("SYNO.Core.System.Utilization", "get", version=1) + + if not result: + # Пробуем альтернативный API + logger.info("Trying alternative API for system load") + alt_result = self._make_api_request("SYNO.Core.System", "info", version=3) + + if not alt_result: + return {} + + # Формируем из частичных данных + return { + "cpu_load": alt_result.get("cpu_usage", 0), + "memory": { + "total": alt_result.get("memory_size", 0), + "used": alt_result.get("memory_usage", 0), + "usage_percent": alt_result.get("memory_usage_percent", 0) + } + } + + # Формируем структурированный результат + return { + "cpu_load": result.get("cpu", {}).get("user", 0) + result.get("cpu", {}).get("system", 0), + "memory": result.get("memory", {}), + "network": result.get("network", {}) + } + + except Exception as e: + logger.error(f"Error getting system load: {str(e)}") + return {} + + def is_online_api(self) -> bool: + """Проверка онлайн-статуса Synology NAS с использованием API""" + if not self.is_online(): + return False + + # Проверяем доступность API через авторизацию + if not self.sid and not self.login(): + return False + + return True + + def get_storage_status(self) -> Dict[str, Any]: + """Получение подробной информации о хранилище""" + logger.info("Getting storage status information") + + # Проверяем доступность NAS и API + if not self.is_online_api(): + logger.error("Cannot get storage status: NAS is not online or API is not accessible") + return {"error": "authentication_failed"} + + try: + # Пробуем получить информацию о хранилище через SYNO.Storage.CGI.Storage API + result = self._make_api_request("SYNO.Storage.CGI.Storage", "load_info", version=1) + + if not result: + # Пробуем альтернативный API + logger.info("Trying alternative API for storage info") + alt_result = self._make_api_request("SYNO.Core.Storage", "info", version=1) + + if not alt_result: + # Пробуем еще один альтернативный API + logger.info("Trying SYNO.Core.System API for storage info") + sys_result = self._make_api_request("SYNO.Core.System", "info", version=3) + + if not sys_result: + return { + "volumes": [], + "disks": [], + "total_size": 0, + "total_used": 0, + "error": "no_data" + } + + # Извлекаем базовую информацию о хранилище из системной информации + return { + "volumes": [], + "disks": [], + "total_size": sys_result.get("hdd_total_size", 0) * 1024**2, # МБ в байты + "total_used": sys_result.get("hdd_total_size", 0) * sys_result.get("hdd_usage", 0) / 100 * 1024**2, + } + + # Обрабатываем данные из альтернативного API + volumes = alt_result.get("volumes", []) + disks = alt_result.get("disks", []) + + else: + # Обрабатываем данные из основного API + volumes = result.get("volumes", []) + disks = result.get("disks", []) + + # Рассчитываем общие размеры + total_size = 0 + total_used = 0 + + for volume in volumes: + volume_size = volume.get("size", {}).get("total", 0) + volume_used = volume.get("size", {}).get("used", 0) + + total_size += volume_size + total_used += volume_used + + return { + "volumes": volumes, + "disks": disks, + "total_size": total_size, + "total_used": total_used + } + + except Exception as e: + logger.error(f"Error in get_storage_status: {str(e)}") + return {"error": str(e)} + + def get_security_status(self) -> Dict[str, Any]: + """Получение информации о состоянии безопасности""" + logger.info("Getting security status information") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for security status request") + return {"success": False, "error": "authentication_failed"} + + try: + # Пробуем получить информацию о безопасности через API Security Scan + result = self._make_api_request("SYNO.Core.SecurityScan.Status", "get", version=1) + + if not result: + # Пробуем альтернативный API + logger.info("Trying alternative API for security status") + # Проверяем статус брандмауэра + firewall_result = self._make_api_request("SYNO.Core.Security.Firewall.Status", "get", version=1) + + # Проверяем статус автоматических обновлений + update_result = self._make_api_request("SYNO.Core.Upgrade.Server.Setting", "get", version=1) + + # Если ни один из API не отвечает + if not firewall_result and not update_result: + # Получаем общую информацию о системе для базовой проверки безопасности + sys_result = self._make_api_request("SYNO.DSM.Info", "getinfo", version=2) + + if not sys_result: + return { + "success": False, + "status": "unknown", + "last_check": None, + "is_secure": False, + "error": "no_security_api" + } + + # Собираем базовые сведения из системной информации + return { + "success": True, + "status": "basic", + "last_check": None, + "is_secure": True, # Предполагаем, что система в целом безопасна + "firewall_enabled": None, + "auto_update": None, + "version_latest": sys_result.get("version_string", "") + } + + # Собираем информацию из доступных результатов + firewall_enabled = firewall_result.get("enabled", False) if firewall_result else None + auto_update = update_result.get("auto_update", False) if update_result else None + + # Определяем, насколько система безопасна + is_secure = True # По умолчанию предполагаем, что система безопасна + if firewall_enabled is not None and not firewall_enabled: + is_secure = False + + return { + "success": True, + "status": "partial", + "last_check": None, + "is_secure": is_secure, + "firewall_enabled": firewall_enabled, + "auto_update": auto_update + } + + # Если основное API отвечает, возвращаем его данные + return { + "success": True, + "status": result.get("status", "unknown"), + "last_check": result.get("last_check", None), + "is_secure": result.get("is_secure", False), + "details": result.get("details", {}) + } + + except Exception as e: + logger.error(f"Error in get_security_status: {str(e)}") + return {"success": False, "error": str(e)} + + def get_processes(self, limit: int = 20) -> List[Dict[str, Any]]: + """Получение списка активных процессов""" + logger.info(f"Getting list of active processes (limit={limit})") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for processes request") + return [] + + try: + # Получаем список процессов через API + result = self._make_api_request("SYNO.Core.System.Process", "list", version=1, + params={"sort_by": "cpu", "order": "DESC", "limit": limit}) + + if not result: + logger.warning("Failed to get process list") + return [] + + return result.get("processes", []) + + except Exception as e: + logger.error(f"Error getting process list: {str(e)}") + return [] + + def get_network_status(self) -> Dict[str, Any]: + """Получение информации о сетевых подключениях""" + logger.info("Getting network status information") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for network status request") + return {} + + try: + # Получаем информацию о сетевых интерфейсах + interface_result = self._make_api_request("SYNO.Core.Network.Interface", "list", version=1) + + # Получаем статистику использования сети + utilization_result = self._make_api_request("SYNO.Core.System.Utilization", "get", version=1) + + interfaces = [] + if interface_result: + interfaces = interface_result.get("interfaces", []) + + network_stats = {} + if utilization_result and "network" in utilization_result: + network_stats = utilization_result.get("network", {}) + + # Объединяем данные + for interface in interfaces: + iface_id = interface.get("id", "") + if iface_id in network_stats: + interface["rx"] = network_stats[iface_id].get("rx", 0) + interface["tx"] = network_stats[iface_id].get("tx", 0) + + return { + "interfaces": interfaces, + "statistics": network_stats + } + + except Exception as e: + logger.error(f"Error getting network status: {str(e)}") + return {} + + def get_system_logs(self, limit: int = 20) -> List[Dict[str, Any]]: + """Получение журналов системы""" + logger.info(f"Getting system logs (limit={limit})") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for system logs request") + return [] + + try: + # Получаем журналы через API + result = self._make_api_request("SYNO.Core.SyslogClient.Log", "list", version=1, + params={"start": 0, "limit": limit, "sort": "time", "dir": "DESC"}) + + if not result: + # Пробуем альтернативный API + logger.info("Trying alternative API for system logs") + alt_result = self._make_api_request("SYNO.Core.Log", "list", version=1, + params={"start": 0, "limit": limit, "sort": "time", "dir": "DESC"}) + + if alt_result: + return alt_result.get("logs", []) + return [] + + return result.get("logs", []) + + except Exception as e: + logger.error(f"Error getting system logs: {str(e)}") + return [] + + def get_power_schedule(self) -> Dict[str, Any]: + """Получение расписания включения/выключения""" + logger.info("Getting power schedule") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for power schedule request") + return {} + + try: + # Получаем расписание через API + result = self._make_api_request("SYNO.Core.Hardware.PowerSchedule", "get", version=1) + + if not result: + return {} + + return result + + except Exception as e: + logger.error(f"Error getting power schedule: {str(e)}") + return {} + + def set_power_schedule(self, schedule_type: str, days: List[int], time: str, enabled: bool = True) -> bool: + """Настройка расписания включения/выключения + + Args: + schedule_type: Тип расписания ('boot' или 'shutdown') + days: Список дней недели (0-6, где 0 - понедельник) + time: Время в формате 'HH:MM' + enabled: Включить или выключить расписание + + Returns: + True если успешно, False в противном случае + """ + logger.info(f"Setting power schedule for {schedule_type} at {time} on days {days}") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for setting power schedule") + return False + + try: + # Получаем текущее расписание + current_schedule = self._make_api_request("SYNO.Core.Hardware.PowerSchedule", "get", version=1) + + if not current_schedule: + logger.error("Failed to get current power schedule") + return False + + # Подготавливаем новое расписание + params = { + "enabled": enabled, + "type": schedule_type, + "day": days, + "time": time + } + + # Устанавливаем новое расписание + result = self._make_api_request("SYNO.Core.Hardware.PowerSchedule", "set", version=1, params=params) + + if not result: + logger.error("Failed to set power schedule") + return False + + logger.info(f"Power schedule for {schedule_type} set successfully") + return True + + except Exception as e: + logger.error(f"Error setting power schedule: {str(e)}") + return False + + def get_temperature_status(self) -> Dict[str, Any]: + """Получение информации о температуре системы и дисков""" + logger.info("Getting temperature status") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for temperature status request") + return {} + + try: + # Получаем информацию о системе для общей температуры + system_info = self._make_api_request("SYNO.DSM.Info", "getinfo", version=2) + + # Получаем информацию о дисках для их температуры + storage_info = self._make_api_request("SYNO.Storage.CGI.Storage", "load_info", version=1) + + system_temp = None + disk_temps = [] + + if system_info: + system_temp = system_info.get("temperature") + + if storage_info: + disks = storage_info.get("disks", []) + for disk in disks: + name = disk.get("name", "unknown") + model = disk.get("model", "unknown") + temp = disk.get("temp", None) + if temp is not None: + disk_temps.append({ + "name": name, + "model": model, + "temperature": temp + }) + + return { + "system_temperature": system_temp, + "disk_temperatures": disk_temps, + "warning": system_info.get("temperature_warn", False) if system_info else False + } + + except Exception as e: + logger.error(f"Error getting temperature status: {str(e)}") + return {} + + def browse_files(self, folder_path: str = "", limit: int = 100) -> Dict[str, Any]: + """Просмотр файлов в указанной директории + + Args: + folder_path: Путь к папке (пустая строка для корневых общих папок) + limit: Максимальное количество элементов для возврата + + Returns: + Словарь с информацией о файлах и папках + """ + logger.info(f"Browsing files in {folder_path or 'root'}") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for file browsing") + return {"success": False, "error": "authentication_failed"} + + try: + if not folder_path: + # Если путь не указан, получаем список общих папок + result = self._make_api_request("SYNO.FileStation.List", "list_share", version=2) + + if not result: + logger.error("Failed to list shared folders") + return {"success": False, "error": "api_error"} + + return { + "success": True, + "items": result.get("shares", []), + "path": "", + "is_root": True + } + else: + # Получаем список файлов в указанной директории + params = { + "folder_path": folder_path, + "limit": limit, + "offset": 0, + "sort_by": "name", + "sort_direction": "ASC" + } + + result = self._make_api_request("SYNO.FileStation.List", "list", version=2, params=params) + + if not result: + logger.error(f"Failed to list files in {folder_path}") + return {"success": False, "error": "api_error"} + + return { + "success": True, + "items": result.get("files", []), + "path": folder_path, + "is_root": False, + "total": result.get("total", 0) + } + + except Exception as e: + logger.error(f"Error browsing files: {str(e)}") + return {"success": False, "error": str(e)} + + def manage_service(self, service_name: str, action: str = "status") -> Dict[str, Any]: + """Управление системным сервисом + + Args: + service_name: Имя сервиса + action: Действие (status/start/stop/restart) + + Returns: + Словарь с результатом операции + """ + logger.info(f"Managing service {service_name}, action: {action}") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for service management") + return {"success": False, "error": "authentication_failed"} + + try: + # Проверяем доступное API для управления сервисами + if action == "status": + result = self._make_api_request("SYNO.Core.Service", "get", version=1, + params={"service": service_name}) + else: + result = self._make_api_request("SYNO.Core.Service", action, version=1, + params={"service": service_name}) + + if not result: + logger.error(f"Failed to {action} service {service_name}") + return {"success": False, "error": "api_error"} + + return { + "success": True, + "service": service_name, + "action": action, + "result": result, + "status": result.get("status") if action == "status" else "completed" + } + + except Exception as e: + logger.error(f"Error managing service {service_name}: {str(e)}") + return {"success": False, "error": str(e)} + + def search_files(self, pattern: str, folder_path: str = "", limit: int = 100) -> Dict[str, Any]: + """Поиск файлов по шаблону + + Args: + pattern: Шаблон для поиска + folder_path: Путь к папке для поиска (пустая строка для всех общих папок) + limit: Максимальное количество результатов + + Returns: + Словарь с найденными файлами + """ + logger.info(f"Searching for {pattern} in {folder_path or 'all shared folders'}") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for file search") + return {"success": False, "error": "authentication_failed"} + + try: + if not folder_path: + # Получаем список всех общих папок для поиска + shares_result = self._make_api_request("SYNO.FileStation.List", "list_share", version=2) + + if not shares_result: + logger.error("Failed to list shared folders for search") + return {"success": False, "error": "api_error"} + + # Формируем список путей для поиска + folder_paths = [share.get("path") for share in shares_result.get("shares", [])] + else: + folder_paths = [folder_path] + + # Запускаем поиск + params = { + "folder_path": folder_paths, + "pattern": pattern, + "limit": limit, + "offset": 0 + } + + result = self._make_api_request("SYNO.FileStation.Search", "start", version=2, params=params) + + if not result: + logger.error(f"Failed to start search for {pattern}") + return {"success": False, "error": "api_error"} + + # Получаем taskid для проверки результатов + taskid = result.get("taskid") + if not taskid: + logger.error("No taskid received for search") + return {"success": False, "error": "no_task_id"} + + # Ожидаем завершения поиска + search_result = {"finished": False, "progress": 0} + for _ in range(10): # Максимум 10 попыток + search_status = self._make_api_request("SYNO.FileStation.Search", "status", version=2, + params={"taskid": taskid}) + + if not search_status: + break + + search_result["progress"] = search_status.get("progress", 0) + + if search_status.get("finished", False): + search_result["finished"] = True + break + + time.sleep(0.5) # Пауза между запросами + + # Получаем результаты поиска + if search_result["finished"]: + list_result = self._make_api_request("SYNO.FileStation.Search", "list", version=2, + params={"taskid": taskid, "limit": limit}) + + if list_result: + files = list_result.get("files", []) + return { + "success": True, + "pattern": pattern, + "results": files, + "total": list_result.get("total", len(files)) + } + + # Если не удалось получить результаты, останавливаем поиск + self._make_api_request("SYNO.FileStation.Search", "stop", version=2, + params={"taskid": taskid}) + + return { + "success": False, + "error": "search_timeout", + "progress": search_result["progress"] + } + + except Exception as e: + logger.error(f"Error searching files: {str(e)}") + return {"success": False, "error": str(e)} + + def get_backup_status(self) -> Dict[str, Any]: + """Получение информации о резервном копировании""" + logger.info("Getting backup status information") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for backup status request") + return {"success": False, "error": "authentication_failed"} + + try: + # Пробуем получить информацию о Hyper Backup + hyper_result = self._make_api_request("SYNO.Backup.Repository", "list", version=1) + + # Пробуем получить информацию о задачах Time Backup + time_result = self._make_api_request("SYNO.BackupService.Task", "list", version=1) + + # Проверяем статус резервного копирования USB + usb_result = self._make_api_request("SYNO.USBCopy", "status", version=1) + + backups = { + "hyper_backup": hyper_result.get("backups", []) if hyper_result else [], + "time_backup": time_result.get("tasks", []) if time_result else [], + "usb_copy": {"enabled": usb_result.get("enable", False) if usb_result else False} + } + + return { + "success": True, + "backups": backups, + "available_apis": { + "hyper_backup": hyper_result is not None, + "time_backup": time_result is not None, + "usb_copy": usb_result is not None + } + } + + except Exception as e: + logger.error(f"Error getting backup status: {str(e)}") + return {"success": False, "error": str(e)} + + def check_for_updates(self) -> Dict[str, Any]: + """Проверка наличия обновлений системы""" + logger.info("Checking for system updates") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for update check") + return {"success": False, "error": "authentication_failed"} + + try: + # Получаем текущую информацию о системе + system_info = self._make_api_request("SYNO.DSM.Info", "getinfo", version=2) + + if not system_info: + logger.error("Failed to get system info for update check") + return {"success": False, "error": "api_error"} + + # Проверяем наличие обновлений + update_result = self._make_api_request("SYNO.Core.Upgrade.Server", "check", version=1) + + # Получаем настройки автоматического обновления + settings_result = self._make_api_request("SYNO.Core.Upgrade.Server.Setting", "get", version=1) + + # Получаем информацию о доступных обновлениях + update_info = self._make_api_request("SYNO.Core.Upgrade.Server.Download", "list", version=1) + + current_version = system_info.get("version_string", "unknown") + auto_update_enabled = settings_result.get("auto_update", False) if settings_result else False + + updates = [] + if update_info and "updates" in update_info: + updates = update_info.get("updates", []) + + update_available = len(updates) > 0 + + return { + "success": True, + "current_version": current_version, + "update_available": update_available, + "auto_update_enabled": auto_update_enabled, + "updates": updates + } + + except Exception as e: + logger.error(f"Error checking for updates: {str(e)}") + return {"success": False, "error": str(e)} + + def get_quota_info(self) -> Dict[str, Any]: + """Получение информации о квотах пользователей""" + logger.info("Getting user quota information") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for quota info request") + return {"success": False, "error": "authentication_failed"} + + try: + # Получаем список пользователей + users_result = self._make_api_request("SYNO.Core.User", "list", version=1) + + if not users_result: + logger.error("Failed to get user list for quota info") + return {"success": False, "error": "api_error"} + + users = users_result.get("users", []) + user_quotas = [] + + # Получаем квоты для каждого пользователя + for user in users: + user_name = user.get("name") + if not user_name: + continue + + quota_result = self._make_api_request("SYNO.Core.Quota", "get", version=1, + params={"user_name": user_name}) + + if quota_result and "quotas" in quota_result: + user_quotas.append({ + "user": user_name, + "quotas": quota_result.get("quotas", []) + }) + + return { + "success": True, + "user_quotas": user_quotas + } + + except Exception as e: + logger.error(f"Error getting quota info: {str(e)}") + return {"success": False, "error": str(e)} diff --git a/.history/src/api/synology_20250830101211.py b/.history/src/api/synology_20250830101211.py new file mode 100644 index 0000000..2a41bba --- /dev/null +++ b/.history/src/api/synology_20250830101211.py @@ -0,0 +1,1895 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Модуль для взаимодействия с API Synology NAS +""" + +import requests +from requests.adapters import HTTPAdapter +import json +import logging +import time +import urllib3 +from urllib3.util import Retry +from typing import Dict, Any, Optional, List +import socket +import struct +from time import sleep + +from src.config.config import ( + SYNOLOGY_HOST, + SYNOLOGY_PORT, + SYNOLOGY_USERNAME, + SYNOLOGY_PASSWORD, + SYNOLOGY_SECURE, + SYNOLOGY_TIMEOUT, + SYNOLOGY_MAC, + WOL_PORT, + SYNOLOGY_API_VERSION, + SYNOLOGY_POWER_API, + SYNOLOGY_INFO_API +) +from src.api.api_discovery import discover_available_apis, find_compatible_api + +# Отключение предупреждений о небезопасных SSL-соединениях +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + +logger = logging.getLogger(__name__) + +class SynologyAPI: + """Класс для взаимодействия с API Synology NAS""" + + def __init__(self): + """Инициализация класса SynologyAPI""" + logger.info("Creating API with auto-retry and connection pool") + logger.debug(f"Connection details: Host={SYNOLOGY_HOST}, Port={SYNOLOGY_PORT}, Secure={SYNOLOGY_SECURE}") + + self.protocol = "https" if SYNOLOGY_SECURE else "http" + self.base_url = f"{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi" + self.sid = None + self.session = requests.Session() + + # Настройка SSL + if self.protocol == "https": + logger.debug("SSL enabled, disabling certificate verification for internal network") + self.session.verify = False # Отключаем проверку SSL для внутренней сети + + # Добавляем пользовательские заголовки для улучшения совместимости с API + custom_headers = { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36', + 'Accept': 'application/json, text/javascript, */*; q=0.01', + 'Accept-Language': 'ru-RU,ru;q=0.9,en-US;q=0.8,en;q=0.7', + 'X-Requested-With': 'XMLHttpRequest', + 'Connection': 'keep-alive', + 'Referer': f'{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/' + } + self.session.headers.update(custom_headers) + logger.debug("Added browser-like headers for API compatibility") + + # Добавляем повторные попытки для HTTP-запросов + retry_strategy = Retry( + total=5, # Увеличиваем количество попыток + status_forcelist=[429, 500, 502, 503, 504, 404], + allowed_methods=["GET", "POST"], + backoff_factor=1.5, # Увеличиваем задержку между попытками + respect_retry_after_header=True + ) + adapter = HTTPAdapter( + max_retries=retry_strategy, + pool_connections=3, + pool_maxsize=10 + ) + self.session.mount("http://", adapter) + self.session.mount("https://", adapter) + + # Таймауты будут указаны в запросах + self.default_timeout = (SYNOLOGY_TIMEOUT, SYNOLOGY_TIMEOUT*2) + logger.debug(f"Setting default request timeout: {self.default_timeout}") + + # Кэш для хранения результатов запросов + self._cache = {} + self._cache_ttl = {} + self._last_online_check = 0 + self._last_online_status = False + self._online_check_interval = 30 # Интервал для кэширования проверок онлайн-статуса + + # Время последней успешной аутентификации и срок действия сессии + self._last_auth_time = 0 + self._auth_expiry = 3600 # По умолчанию 1 час + + # Информация о доступных API + self._available_apis = {} + self._api_info_ttl = 0 + + # Инициализируем API version resolver для автоматического определения совместимых API + self.api_resolver = None # Будет создан при необходимости + + def login(self) -> bool: + """Авторизация в API Synology NAS""" + # Сбрасываем SID для новой сессии + self.sid = None + + logger.info("Attempting to authenticate with Synology NAS...") + logger.debug(f"Base URL: {self.base_url}") + + # Проверяем доступность NAS перед запросом с помощью прямой TCP-проверки + # Избегаем вызова is_online(), чтобы не создавать рекурсию + online_status = self._check_tcp_connection() + if not online_status: + logger.error("Cannot login: Synology NAS is not reachable") + return False + + # Пробуем различные версии API для аутентификации + # Начинаем с версии 3, которая показала лучшую совместимость в тестах + auth_versions_to_try = [3, 2, 1, 6] # Пробуем сначала более стабильные версии + + for auth_version in auth_versions_to_try: + try: + # Определяем путь к API аутентификации + auth_path = "entry.cgi" # Используем entry.cgi вместо auth.cgi по умолчанию + + # Проверка информации API для определения доступных версий API + api_info_url = f"{self.base_url}/entry.cgi" + api_info_params = { + "api": "SYNO.API.Info", + "version": "1", + "method": "query", + "query": "SYNO.API.Auth" + } + + logger.debug(f"Querying API info for auth version {auth_version}") + try: + api_info_response = self.session.get( + api_info_url, + params=api_info_params, + timeout=self.default_timeout, + verify=False + ) + + if api_info_response.status_code == 200: + api_info_data = api_info_response.json() + if api_info_data.get("success"): + auth_info = api_info_data.get("data", {}).get("SYNO.API.Auth", {}) + max_version = auth_info.get("maxVersion", 6) + min_version = auth_info.get("minVersion", 1) + auth_path = auth_info.get("path", "entry.cgi") + logger.debug(f"Auth API versions: min={min_version}, max={max_version}, path={auth_path}") + + # Проверяем поддержку текущей версии + if auth_version < min_version or auth_version > max_version: + logger.warning(f"Auth API version {auth_version} not supported (min={min_version}, max={max_version}), trying next version") + continue + else: + logger.warning("Failed to query API info, using default auth path") + else: + logger.warning(f"API info request failed with status {api_info_response.status_code}, using default auth path") + except Exception as e: + logger.warning(f"Error querying API info: {str(e)}, using default auth path") + + # Основной запрос авторизации + url = f"{self.base_url}/{auth_path}" + params = { + "api": "SYNO.API.Auth", + "version": str(auth_version), + "method": "login", + "account": SYNOLOGY_USERNAME, + "passwd": SYNOLOGY_PASSWORD, + "session": "SynologyPowerControlBot", + "format": "cookie" + } + + # Для версии 6+ используем немного другой формат + if auth_version >= 6: + params["enable_syno_token"] = "yes" + + logger.debug(f"Sending auth request to {url} with API version {auth_version}") + start_time = time.time() + response = self.session.get( + url, + params=params, + timeout=self.default_timeout, + verify=False + ) + elapsed_time = time.time() - start_time + logger.debug(f"Auth request completed in {elapsed_time:.2f}s with status code {response.status_code}") + + # Проверка статуса HTTP + if response.status_code != 200: + logger.error(f"HTTP error: {response.status_code}") + logger.debug(f"Response content: {response.text[:200]}") + continue # Пробуем следующую версию + + try: + data = response.json() + except json.JSONDecodeError: + logger.error("Failed to decode JSON response") + logger.debug(f"Response content: {response.text[:200]}") + continue # Пробуем следующую версию + + if data.get("success"): + self.sid = data.get("data", {}).get("sid") + self._last_auth_time = time.time() + logger.info(f"Successfully logged in to Synology NAS using API version {auth_version}") + logger.debug(f"Session ID obtained: {self.sid[:5] if self.sid else 'None'}...") + + # Получаем и сохраняем токен SYNO, если он есть + syno_token = data.get("data", {}).get("synotoken") + if syno_token: + self.session.headers.update({'X-SYNO-TOKEN': syno_token}) + logger.debug("Added X-SYNO-TOKEN header for improved API compatibility") + + # Также добавляем SID в cookies для улучшения совместимости + self.session.cookies.update({ + 'id': self.sid, + 'sid': self.sid + }) + logger.debug("Added SID to session cookies for improved compatibility") + + # Проверка валидности полученной сессии с помощью простого запроса + # Будем использовать SYNO.API.Info без проверки сложных методов + + # Даем системе немного времени для инициализации сессии + time.sleep(0.5) + + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to log in with version {auth_version}: Error code {error_code}") + + # Если ошибка связана с версией API, пробуем следующую версию + if error_code in [104, 105]: + logger.warning(f"Auth version {auth_version} not supported, trying next version") + continue + + # Дополнительная диагностика + if error_code == 400: + logger.error("Authentication error: Invalid credentials") + elif error_code == 401: + logger.error("Authentication error: Account disabled") + elif error_code == 402: + logger.error("Authentication error: Permission denied") + elif error_code == 403: + logger.error("Authentication error: 2-factor authentication required") + elif error_code == 404: + logger.error("Authentication error: Failed to authenticate with 2-factor authentication") + + # Если ошибка связана с неверными учетными данными, нет смысла пробовать другие версии API + if error_code in [400, 401, 402, 403, 404]: + return False + + except requests.exceptions.Timeout: + logger.error(f"Connection timeout during auth with version {auth_version}") + continue # Пробуем следующую версию + except requests.exceptions.ConnectionError as e: + logger.error(f"Connection error during auth with version {auth_version}: {str(e)}") + continue # Пробуем следующую версию + except requests.RequestException as e: + logger.error(f"Request error during auth with version {auth_version}: {str(e)}") + continue # Пробуем следующую версию + except Exception as e: + logger.error(f"Unexpected error during login with version {auth_version}: {str(e)}", exc_info=True) + continue # Пробуем следующую версию + + # Если все версии не сработали + logger.error("Failed to authenticate with any API version") + return False + + def _validate_session(self) -> bool: + """Проверяет валидность сессии после авторизации""" + if not self.sid: + return False + + # Попробуем сделать простой запрос для проверки сессии + test_apis = [ + {"api": "SYNO.Core.System", "method": "info", "version": 1}, + {"api": "SYNO.DSM.Info", "method": "getinfo", "version": 1} + ] + + for test_api in test_apis: + try: + url = f"{self.base_url}/entry.cgi" + params = { + "api": test_api["api"], + "version": str(test_api["version"]), + "method": test_api["method"], + "_sid": self.sid + } + + response = self.session.get(url, params=params, timeout=self.default_timeout, verify=False) + + if response.status_code == 200: + data = response.json() + if data.get("success"): + logger.debug(f"Session validation successful using {test_api['api']}") + return True + else: + error_code = data.get("error", {}).get("code", -1) + if error_code != 119: # Не сессия истекла + logger.debug(f"Session validation with {test_api['api']} returned error code {error_code}") + return True # Считаем сессию валидной, если ошибка не связана с истечением сессии + except Exception as e: + logger.warning(f"Error during session validation with {test_api['api']}: {str(e)}") + + logger.warning("Session validation failed with all test APIs") + return False + + def logout(self) -> bool: + """Выход из API Synology NAS""" + if not self.sid: + return True + + try: + url = f"{self.base_url}/auth.cgi" + params = { + "api": "SYNO.API.Auth", + "version": "1", + "method": "logout", + "session": "SynologyPowerControlBot", + "_sid": self.sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + self.sid = None + logger.info("Successfully logged out from Synology NAS") + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to log out from Synology NAS: Error code {error_code}") + return False + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + return False + + def _make_api_request(self, api_name: str, method: str, version: int = 1, + params: Optional[Dict[str, Any]] = None, retry_auth: bool = True, retry_count: int = 0) -> Optional[Dict[str, Any]]: + """Обобщенный метод для выполнения API запросов с обработкой ошибок""" + # Ограничение на количество повторных попыток + if retry_count >= 3: + logger.error(f"Too many retries for {api_name}.{method}, giving up") + return None + + # Проверка наличия авторизации + if not self.sid and not self.login(): + logger.error(f"Not authenticated for API request: {api_name}.{method}") + return None + + # Проверка информации API для определения пути и поддерживаемой версии + api_info_url = f"{self.base_url}/entry.cgi" + api_info_params = { + "api": "SYNO.API.Info", + "version": "1", + "method": "query", + "query": api_name + } + + api_path = "entry.cgi" + try: + logger.debug(f"Querying API info for {api_name}") + api_info_response = self.session.get( + api_info_url, + params=api_info_params, + timeout=self.default_timeout, + verify=False + ) + + if api_info_response.status_code == 200: + api_info_data = api_info_response.json() + if api_info_data.get("success"): + api_info = api_info_data.get("data", {}).get(api_name, {}) + if api_info: + max_version = api_info.get("maxVersion", version) + min_version = api_info.get("minVersion", version) + api_path = api_info.get("path", "entry.cgi") + + # Проверка, поддерживается ли запрошенная версия + if version < min_version: + logger.warning(f"API version {version} for {api_name} is below minimum {min_version}, using {min_version}") + version = min_version + elif version > max_version: + logger.warning(f"API version {version} for {api_name} exceeds maximum {max_version}, using {max_version}") + version = max_version + + logger.debug(f"Using API path: {api_path}, version: {version}") + else: + logger.warning(f"API {api_name} not found in API info, using defaults") + except Exception as e: + logger.warning(f"Error querying API info for {api_name}: {str(e)}, using defaults") + + # Подготовка базовых параметров запроса + base_params = { + "api": api_name, + "version": str(version), + "method": method, + "_sid": self.sid # Используем _sid вместо sid для лучшей совместимости + } + + # Добавление дополнительных параметров, если они заданы + if params: + base_params.update(params) + + url = f"{self.base_url}/{api_path}" + logger.debug(f"API request: {api_name}.{method} v{version} to {url}") + logger.debug(f"Full request params: {base_params}") + + try: + start_time = time.time() + response = self.session.get( + url, + params=base_params, + timeout=self.default_timeout, + verify=False + ) + elapsed_time = time.time() - start_time + logger.debug(f"API request completed in {elapsed_time:.2f}s with status {response.status_code}") + + # Проверка статуса HTTP + if response.status_code != 200: + logger.error(f"HTTP error: {response.status_code} for {api_name}.{method}") + + # Повторная попытка при ошибках соединения + if response.status_code in [500, 502, 503, 504]: + logger.info(f"Server error, retrying request for {api_name}.{method}") + sleep(2) + return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1) + + return None + + try: + data = response.json() + except json.JSONDecodeError: + logger.error(f"Failed to decode JSON response for {api_name}.{method}") + logger.debug(f"Response content: {response.text[:200]}") + + # Повторная попытка при ошибках декодирования + logger.info(f"JSON decode error, retrying request for {api_name}.{method}") + sleep(2) + return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1) + + if data.get("success"): + logger.info(f"API request successful for {api_name}.{method}") + return data.get("data", {}) + else: + error_code = data.get("error", {}).get("code", -1) + error_desc = self._get_error_description(error_code) + logger.error(f"API error for {api_name}.{method}: {error_code} - {error_desc}") + + # Ошибки доступа или прав часто встречаются, но они не критичные + # Например, ошибка 102 означает, что нет прав, но NAS доступен + if error_code in [102, 103, 104, 105]: + logger.warning(f"Permission-related error for {api_name}.{method}: {error_code}") + # Возвращаем пустой словарь вместо None, + # чтобы вызывающий код мог понять, что запрос выполнен + return {} + + # Если ошибка связана с авторизацией и нам разрешено повторить попытку + if error_code in [106, 107, 119] and retry_auth: + logger.info(f"Session error (code {error_code}), creating fresh session...") + self.sid = None # Сбрасываем SID + + # Для ошибки 119 (Session timeout) дадим системе немного времени + if error_code == 119: + logger.info("Session timeout detected, waiting before retry...") + sleep(3) + + if self.login(): + logger.info("Re-authenticated with fresh session, retrying API request...") + # Рекурсивный вызов, но со счетчиком повторов + return self._make_api_request(api_name, method, version, params, False, retry_count + 1) + + # Для некоторых ошибок можно автоматически повторить запрос + if error_code in [408, 429, 500, 502, 503, 504]: + logger.info(f"Temporary error {error_code}, retrying request for {api_name}.{method}") + sleep(2) + return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1) + + return None + + except requests.exceptions.Timeout: + logger.error(f"Request timeout for {api_name}.{method}") + + # Повторная попытка при таймауте + if retry_count < 2: + logger.info(f"Timeout, retrying request for {api_name}.{method}") + sleep(2) + return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1) + + return None + except requests.exceptions.ConnectionError as e: + logger.error(f"Connection error for {api_name}.{method}: {str(e)}") + + # Повторная попытка при ошибке соединения + if retry_count < 2: + logger.info(f"Connection error, retrying request for {api_name}.{method}") + sleep(2) + return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1) + + return None + except Exception as e: + logger.error(f"Unexpected error during {api_name}.{method}: {str(e)}") + return None + + def get_system_status(self) -> Dict[str, Any]: + """Получение статуса системы""" + # Проверяем доступность системы + if not self.is_online(): + logger.info("Device is offline, skipping API request") + return {"status": "offline"} + + # Проверяем, есть ли кэшированный результат + cache_key = "system_status" + current_time = time.time() + if cache_key in self._cache and current_time - self._cache_ttl.get(cache_key, 0) < 60: + logger.debug("Using cached system status") + return self._cache[cache_key] + + # Используем рекомендованный API для получения информации о системе + logger.info(f"Getting system info using {SYNOLOGY_INFO_API} v{SYNOLOGY_API_VERSION}") + + # Для SYNO.DSM.Info метод getinfo, для других обычно используется get или info + if SYNOLOGY_INFO_API == "SYNO.DSM.Info": + method = "getinfo" + else: + method = "get" + + result = self._make_api_request(SYNOLOGY_INFO_API, method, version=SYNOLOGY_API_VERSION) + + if result: + logger.info(f"Successfully retrieved system info using {SYNOLOGY_INFO_API}") + + # Формируем расширенный ответ с дополнительной информацией + system_info = { + "status": "online", + "hostname": result.get("hostname", "unknown"), + "model": result.get("model", "unknown"), + "version": result.get("version", "unknown"), + "uptime": result.get("uptime", 0), + "time": current_time, + "is_online": True, + "api_used": SYNOLOGY_INFO_API + } + + # Сохраняем в кэше + self._cache[cache_key] = system_info + self._cache_ttl[cache_key] = current_time + + return system_info + + # Если основной API не сработал, пробуем резервные варианты + logger.warning(f"Failed to retrieve system info with {SYNOLOGY_INFO_API}, trying fallback APIs") + + # Пробуем резервные API + apis_to_try = [ + {"name": "SYNO.DSM.Info", "method": "getinfo", "version": 2}, + {"name": "SYNO.Core.System", "method": "info", "version": 1}, + {"name": "SYNO.Core.System.Status", "method": "get", "version": 1}, + {"name": "SYNO.Core.System.Info", "method": "get", "version": 1}, + ] + + for api in apis_to_try: + if api["name"] == SYNOLOGY_INFO_API: + continue # Пропускаем уже проверенный API + + logger.info(f"Trying to get system info using {api['name']}.{api['method']} v{api['version']}") + result = self._make_api_request(api["name"], api["method"], version=api["version"]) + + if result: + logger.info(f"Successfully retrieved system info using {api['name']}") + + # Формируем расширенный ответ с дополнительной информацией + system_info = { + "status": "online", + "hostname": result.get("hostname", "unknown"), + "model": result.get("model", "unknown"), + "version": result.get("version", "unknown"), + "uptime": result.get("uptime", 0), + "time": current_time, + "is_online": True, + "api_used": api["name"] + } + + # Сохраняем в кэше + self._cache[cache_key] = system_info + self._cache_ttl[cache_key] = current_time + + return system_info + + # Если все запросы не удались, но система онлайн, возвращаем базовую информацию + logger.warning("Failed to retrieve system info with all API methods") + return { + "status": "error", + "error": "Failed to fetch system information", + "is_online": True, + "time": current_time + } + + def shutdown_system(self) -> bool: + """Выключение системы""" + # Проверяем, включено ли устройство перед попыткой его выключить + if not self.is_online(force_check=True): + logger.info("Synology NAS is already offline, no need to shut down") + return True + + logger.info("Attempting to shutdown Synology NAS...") + + # Попробуем сначала использовать предпочтительный API для управления питанием + logger.info(f"Trying shutdown with {SYNOLOGY_POWER_API} v{SYNOLOGY_API_VERSION}") + + # Для SYNO.Core.Hardware.PowerRecovery используется метод setPowerOnState + # Для других API обычно используется метод shutdown или reboot + if SYNOLOGY_POWER_API == "SYNO.Core.Hardware.PowerRecovery": + # Для этого API нужны специальные параметры + params = {"state": "powerbtn"} # powerbtn имитирует нажатие кнопки питания + result = self._make_api_request(SYNOLOGY_POWER_API, "setPowerOnState", version=SYNOLOGY_API_VERSION, params=params) + else: + # Пробуем стандартный метод + result = self._make_api_request(SYNOLOGY_POWER_API, "shutdown", version=SYNOLOGY_API_VERSION) + + if result is not None: + logger.info(f"Successfully initiated system shutdown using {SYNOLOGY_POWER_API}") + + # Даем системе время начать процесс выключения + logger.info("Waiting for shutdown to initialize...") + sleep(5) + + # Проверяем статус + if not self.is_online(force_check=True): + logger.info("System is now offline. Shutdown confirmed successful.") + return True + else: + logger.info("System still appears to be online, but shutdown may be in progress.") + return True + else: + logger.warning(f"Failed to shutdown using {SYNOLOGY_POWER_API}, trying alternative methods") + + # Если не сработал основной метод, пробуем резервные варианты + # Проверка всех доступных методов API для выключения + apis_to_try = [ + {"name": "SYNO.Core.System", "method": "shutdown", "version": 3}, + {"name": "SYNO.Core.System.Power", "method": "shutdown", "version": 1}, + {"name": "SYNO.DSM.Power", "method": "shutdown", "version": 1}, + {"name": "SYNO.System.Power", "method": "shutdown", "version": 1}, + {"name": "SYNO.DSM.System", "method": "shutdown", "version": 1} + ] + + # Проверяем доступные API + try: + api_info_url = f"{self.base_url}/entry.cgi" + api_info_params = { + "api": "SYNO.API.Info", + "version": "1", + "method": "query", + "query": "SYNO.Core.System.Power,SYNO.DSM.Power,SYNO.Core.System,SYNO.System.Power,SYNO.DSM.System" + } + + logger.debug("Checking available shutdown APIs") + api_info_response = self.session.get( + api_info_url, + params=api_info_params, + timeout=self.default_timeout, + verify=False + ) + + if api_info_response.status_code == 200: + api_info_data = api_info_response.json() + if api_info_data.get("success"): + available_apis = api_info_data.get("data", {}) + logger.debug(f"Available APIs: {list(available_apis.keys())}") + + # Фильтруем только доступные API + filtered_apis = [] + for api in apis_to_try: + if api["name"] in available_apis: + api_info = available_apis[api["name"]] + # Проверка версии API + if api_info.get("minVersion", 1) <= api["version"] <= api_info.get("maxVersion", 1): + filtered_apis.append(api) + logger.debug(f"Adding {api['name']} to available shutdown APIs") + else: + logger.debug(f"API {api['name']} version {api['version']} not supported. Supported: {api_info.get('minVersion')}-{api_info.get('maxVersion')}") + + if filtered_apis: + apis_to_try = filtered_apis + else: + logger.warning("No compatible APIs found, trying all methods as fallback") + else: + logger.warning("Failed to query API info, using default methods") + else: + logger.warning(f"API info request failed with status {api_info_response.status_code}, using default methods") + except Exception as e: + logger.warning(f"Error querying API info: {str(e)}, using default methods") + + # Пробуем все доступные методы по порядку + for api in apis_to_try: + logger.info(f"Trying shutdown with {api['name']}.{api['method']} v{api['version']}") + result = self._make_api_request(api["name"], api["method"], version=api["version"]) + + if result is not None: + logger.info(f"Successfully initiated system shutdown using {api['name']}") + + # Даем системе время начать процесс выключения + logger.info("Waiting for shutdown to initialize...") + sleep(5) + + # Проверяем статус + if not self.is_online(force_check=True): + logger.info("System is now offline. Shutdown confirmed successful.") + return True + else: + logger.info("System still appears to be online, but shutdown may be in progress.") + return True + else: + logger.warning(f"Failed to shutdown using {api['name']}.{api['method']} v{api['version']}") + + # Если ни один метод не сработал, но система стала недоступна + if not self.is_online(force_check=True): + logger.info("System appears to be shutting down despite API errors") + return True + + logger.error("Failed to shutdown system after trying multiple APIs") + return False + + def reboot_system(self) -> bool: + """Перезагрузка системы""" + # Проверяем, включена ли система + if not self.is_online(force_check=True): + logger.error("Cannot reboot: System is offline") + return False + + logger.info("Attempting to reboot Synology NAS...") + + # Список API и методов для попытки перезагрузки + apis_to_try = [ + {"api": "SYNO.Core.System", "method": "reboot", "version": 1}, + {"api": "SYNO.DSM.System", "method": "reboot", "version": 1}, + {"api": "SYNO.Core.System.Power", "method": "restart", "version": 1}, + {"api": "SYNO.DSM.Power", "method": "reboot", "version": 1}, + {"api": "SYNO.Core.System", "method": "reboot", "version": 3}, + {"api": "SYNO.System.Power", "method": "reboot", "version": 1}, + {"api": "SYNO.Core.Hardware.NeedReboot", "method": "reboot", "version": 1} + ] + + # Добавляем настроенный в конфигурации API, если он отличается от уже добавленных + already_added = [item["api"] for item in apis_to_try] + if SYNOLOGY_POWER_API not in already_added: + for method in ["restart", "reboot"]: + apis_to_try.append({ + "api": SYNOLOGY_POWER_API, + "method": method, + "version": SYNOLOGY_API_VERSION + }) + + # Перебираем все возможные API и методы + for api_info in apis_to_try: + try: + logger.info(f"Trying reboot with {api_info['api']} API using method {api_info['method']}") + result = self._make_api_request(api_info['api'], api_info['method'], version=api_info['version']) + if result is not None: + logger.info(f"Successfully initiated system reboot using {api_info['api']} API") + + # Даем системе время начать процесс перезагрузки + logger.info("Waiting for reboot to initialize...") + sleep(5) + + # Ждем, пока система станет недоступна (признак перезагрузки) + reboot_started = False + for i in range(12): # Проверяем в течение 60 секунд (12 * 5сек) + if not self.is_online(force_check=True): + logger.info(f"System went offline after {i*5} seconds, reboot in progress") + reboot_started = True + break + logger.debug(f"System still online, waiting... ({i+1}/12)") + sleep(5) + + if reboot_started: + # Успешный вызов API и система ушла оффлайн - это признак успешной перезагрузки + return True + else: + # Успешный вызов API, но система не ушла оффлайн + logger.warning("System did not go offline after reboot command, but command was accepted") + # Даже если система не ушла оффлайн, команда могла быть принята + return True + except Exception as e: + logger.error(f"Error during reboot with {api_info['api']}: {str(e)}") + + # Если все попытки не удались, возвращаем False + logger.error("All reboot attempts failed") + return False + for api in apis_to_try: + logger.info(f"Trying reboot with {api['name']}.{api['method']} v{api['version']}") + result = self._make_api_request(api["name"], api["method"], version=api["version"]) + + if result is not None: + logger.info(f"Successfully initiated system reboot using {api['name']}") + + # Даем системе время начать процесс перезагрузки + logger.info("Waiting for reboot to initialize...") + sleep(5) + + # Ждем, пока система станет недоступна (признак перезагрузки) + reboot_started = False + for i in range(12): # Проверяем в течение 60 секунд (12 * 5сек) + if not self.is_online(force_check=True): + logger.info(f"System went offline after {i*5} seconds, reboot in progress") + reboot_started = True + break + logger.debug(f"System still online, waiting... ({i+1}/12)") + sleep(5) + + if reboot_started: + # Ждем, пока система снова станет доступна + logger.info("Waiting for system to come back online...") + return self.wait_for_boot(max_attempts=30, delay=10) + else: + logger.warning("System did not go offline after reboot command, but command was accepted") + # Даже если система не ушла оффлайн, команда могла быть принята + return True + else: + logger.warning(f"Failed to reboot using {api['name']}.{api['method']} v{api['version']}") + + logger.error("Failed to reboot system after trying multiple APIs") + return False + + def _get_error_description(self, error_code: int) -> str: + """Получение описания ошибки по коду""" + error_descriptions = { + 100: "Unknown error", + 101: "Invalid parameter", + 102: "API does not exist", + 103: "Method does not exist", + 104: "Version does not support", + 105: "Permission denied", + 106: "Session timeout", + 107: "Session interrupted by duplicate login", + 400: "Invalid credentials", + 401: "Account disabled", + 402: "Permission denied", + 403: "2FA required", + 404: "Failed to authenticate with 2FA" + } + return error_descriptions.get(error_code, "Unknown error code") + + def _check_tcp_connection(self) -> bool: + """Проверка базового TCP-соединения с Synology NAS""" + try: + socket_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + socket_obj.settimeout(SYNOLOGY_TIMEOUT) + result = socket_obj.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT)) + socket_obj.close() + + return result == 0 + except socket.error as e: + logger.error(f"Socket error during connection check: {str(e)}") + return False + except Exception as e: + logger.error(f"Unexpected error during connection check: {str(e)}") + return False + + def is_online(self, force_check=False) -> bool: + """Проверка онлайн-статуса Synology NAS""" + # Используем кэшированное значение, если доступно и не устарело + current_time = time.time() + if not force_check and (current_time - self._last_online_check) < self._online_check_interval: + logger.debug(f"Using cached online status: {self._last_online_status}") + return self._last_online_status + + logger.info("Checking if NAS is online...") + + # Проверяем TCP-соединение + online_status = self._check_tcp_connection() + logger.info(f"Detected Synology NAS online status: {online_status}") + + # Если TCP-соединение успешно и у нас есть действующий SID, + # попробуем более детальную проверку через API + if online_status and self.sid: + logger.info("Trying to fetch more detailed online status through API...") + + # Пробуем разные API для проверки онлайн-статуса + api_checks = [ + {"api": "SYNO.DSM.Info", "version": "2", "method": "getinfo"}, + {"api": "SYNO.Core.System", "version": "1", "method": "info"}, + {"api": "SYNO.Core.System.Status", "version": "1", "method": "get"} + ] + + api_success = False + for api_check in api_checks: + try: + url = f"{self.base_url}/entry.cgi" + params = { + "api": api_check["api"], + "version": api_check["version"], + "method": api_check["method"], + "sid": self.sid + } + + logger.debug(f"Trying online status check with {api_check['api']}") + response = self.session.get( + url, + params=params, + timeout=self.default_timeout, + verify=False + ) + + if response.status_code == 200: + data = response.json() + if data.get("success"): + logger.info(f"API request successful for {api_check['api']}") + logger.info("Synology NAS is online with API access") + api_success = True + break + else: + error_code = data.get("error", {}).get("code", -1) + logger.warning(f"API response indicates an error: {error_code}, but NAS is reachable") + else: + logger.warning(f"API returned status code {response.status_code}") + except Exception as e: + logger.warning(f"API check failed for {api_check['api']}, but TCP connection succeeded: {str(e)}") + + if not api_success: + logger.warning("All API checks failed, but TCP connection is successful") + + # Обновляем кэшированное значение + self._last_online_check = current_time + self._last_online_status = online_status + + return online_status + + def wake_on_lan(self) -> bool: + """Отправка Wake-on-LAN пакета для включения Synology NAS""" + if not SYNOLOGY_MAC: + logger.error("MAC address not configured") + return False + + try: + # Преобразование MAC-адреса в байты + mac_address = SYNOLOGY_MAC.replace(':', '').replace('-', '') + if len(mac_address) != 12: + logger.error(f"Invalid MAC address format: {SYNOLOGY_MAC}") + return False + + try: + mac_bytes = bytes.fromhex(mac_address) + except ValueError as e: + logger.error(f"Failed to parse MAC address: {str(e)}") + logger.error(f"MAC address must contain only hex characters: {SYNOLOGY_MAC}") + return False + + # Создание Magic Packet + magic_packet = b'\xff' * 6 + mac_bytes * 16 + + # Отправка пакета на конкретный адрес + try: + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) + sock.sendto(magic_packet, (SYNOLOGY_HOST, WOL_PORT)) + sock.close() + logger.info(f"Wake-on-LAN packet sent to {SYNOLOGY_MAC} at {SYNOLOGY_HOST}:{WOL_PORT}") + except Exception as e: + logger.error(f"Error sending directed WoL packet: {str(e)}") + return False + + # Для надежности отправляем также широковещательный пакет + try: + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) + + # Используем стандартный широковещательный адрес + broadcast_addr = "255.255.255.255" + sock.sendto(magic_packet, (broadcast_addr, WOL_PORT)) + sock.close() + logger.info(f"Wake-on-LAN broadcast packet sent to {broadcast_addr}:{WOL_PORT}") + except Exception as e: + logger.warning(f"Error sending broadcast WoL packet (non-critical): {str(e)}") + # Не считаем ошибкой, т.к. основной пакет уже отправлен + + return True + + except Exception as e: + logger.error(f"Unexpected error in wake_on_lan: {str(e)}") + return False + + def wait_for_boot(self, max_attempts: int = 30, delay: int = 10) -> bool: + """Ожидание загрузки Synology NAS""" + logger.info(f"Waiting for Synology NAS to boot (timeout: {max_attempts * delay}s)...") + + for attempt in range(max_attempts): + # Принудительно проверяем статус без использования кэша + if self.is_online(force_check=True): + logger.info(f"Synology NAS is online after {attempt + 1} attempts ({(attempt + 1) * delay}s)") + + # Проверяем, что не только сеть доступна, но и API загрузился + api_ready = False + logger.info("Waiting for API services to initialize...") + + for api_check in range(5): # Даем еще до 50 секунд для загрузки API + if self.sid or self.login(): + api_ready = True + logger.info(f"API services are ready after {api_check + 1} attempts") + break + logger.debug(f"API not ready yet, waiting... ({api_check + 1}/5)") + sleep(10) + + if not api_ready: + logger.warning("System is online but API services may not be fully initialized") + + # Дадим дополнительное время для полной загрузки всех сервисов + sleep(delay) + return True + + sleep(delay) + logger.info(f"Waiting for boot... Attempt {attempt + 1}/{max_attempts}") + + logger.error(f"Synology NAS didn't come online after {max_attempts * delay} seconds") + return False + + def power_on(self) -> bool: + """Включение Synology NAS""" + # Принудительная проверка статуса + if self.is_online(force_check=True): + logger.info("Synology NAS is already online") + return True + + logger.info("Powering on Synology NAS via Wake-on-LAN...") + + # Проверяем, настроен ли MAC-адрес + if not SYNOLOGY_MAC: + logger.error("Cannot power on: MAC address not configured in settings") + return False + + # Пробуем отправить несколько WoL пакетов для надежности + success = False + for attempt in range(3): + logger.debug(f"Sending WoL packet, attempt {attempt + 1}/3") + if self.wake_on_lan(): + success = True + break + sleep(1) + + if not success: + logger.error("Failed to send Wake-on-LAN packets") + return False + + # Ожидание загрузки + logger.info("WoL packets sent successfully, waiting for system to boot...") + boot_result = self.wait_for_boot(max_attempts=30, delay=10) + + if boot_result: + # Проверяем доступность API после загрузки + system_status = self.get_system_status() + if system_status.get("status") == "online": + logger.info("System booted successfully with API access") + return True + else: + logger.warning("System appears to be online but API may not be fully ready") + return True + else: + logger.error("System did not come online after WoL") + return False + + def power_off(self) -> bool: + """Выключение Synology NAS""" + if not self.is_online(force_check=True): + logger.info("Synology NAS is already offline") + return True + + logger.info("Powering off Synology NAS...") + + # Список API и методов для попытки выключения + apis_to_try = [ + {"api": "SYNO.Core.System", "method": "shutdown", "version": 1}, + {"api": "SYNO.DSM.System", "method": "shutdown", "version": 1}, + {"api": SYNOLOGY_POWER_API, "method": "shutdown", "version": SYNOLOGY_API_VERSION} + ] + + # Перебираем все возможные API и методы + for api_info in apis_to_try: + try: + logger.info(f"Trying shutdown with {api_info['api']} API using method {api_info['method']}") + api_result = self._make_api_request(api_info['api'], api_info['method'], version=api_info['version']) + if api_result is not None: + logger.info(f"Successfully initiated system shutdown using {api_info['api']} API") + + # Даем системе время начать процесс выключения + logger.info("Waiting for shutdown to initialize...") + sleep(5) + + return True + except Exception as e: + logger.error(f"Error during shutdown with {api_info['api']}: {str(e)}") + + # Если все попытки не удались, возвращаем False + logger.error("All shutdown attempts failed") + return False + + # Если все еще не сработало, используем оригинальный метод shutdown_system + if not result: + result = self.shutdown_system() + + if result: + # Дополнительная проверка, что система действительно выключилась + logger.info("Verifying system is offline...") + for attempt in range(12): # Проверяем в течение 2 минут (12 * 10 сек) + if not self.is_online(force_check=True): + logger.info(f"System confirmed offline after {attempt * 10} seconds") + return True + logger.debug(f"System still shutting down, checking again... ({attempt + 1}/12)") + sleep(10) + + logger.warning("System still appears to be online after shutdown command") + return False + else: + logger.error("Failed to initiate shutdown") + return False + + # Заглушки для расширенных методов + def get_shared_folders(self) -> List[Dict[str, Any]]: + """Получение списка общих папок""" + logger.info("Getting list of shared folders") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for shared folders request") + return [] + + try: + # Запрашиваем список общих папок через FileStation API + result = self._make_api_request("SYNO.FileStation.List", "list_share", version=2) + + if not result: + # Пробуем альтернативный API + logger.info("Trying alternative API for shared folders") + alt_result = self._make_api_request("SYNO.FileStation.List", "list_share", version=1) + if alt_result: + return alt_result.get("shares", []) + return [] + + return result.get("shares", []) + + except Exception as e: + logger.error(f"Error getting shared folders: {str(e)}") + return [] + + def get_system_load(self) -> Dict[str, Any]: + """Получение информации о загрузке системы""" + logger.info("Getting system load information") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for system load request") + return {} + + try: + # Запрашиваем информацию о загрузке системы + result = self._make_api_request("SYNO.Core.System.Utilization", "get", version=1) + + if not result: + # Пробуем альтернативный API + logger.info("Trying alternative API for system load") + alt_result = self._make_api_request("SYNO.Core.System", "info", version=3) + + if not alt_result: + return {} + + # Формируем из частичных данных + return { + "cpu_load": alt_result.get("cpu_usage", 0), + "memory": { + "total": alt_result.get("memory_size", 0), + "used": alt_result.get("memory_usage", 0), + "usage_percent": alt_result.get("memory_usage_percent", 0) + } + } + + # Формируем структурированный результат + return { + "cpu_load": result.get("cpu", {}).get("user", 0) + result.get("cpu", {}).get("system", 0), + "memory": result.get("memory", {}), + "network": result.get("network", {}) + } + + except Exception as e: + logger.error(f"Error getting system load: {str(e)}") + return {} + + def is_online_api(self) -> bool: + """Проверка онлайн-статуса Synology NAS с использованием API""" + if not self.is_online(): + return False + + # Проверяем доступность API через авторизацию + if not self.sid and not self.login(): + return False + + return True + + def get_storage_status(self) -> Dict[str, Any]: + """Получение подробной информации о хранилище""" + logger.info("Getting storage status information") + + # Проверяем доступность NAS и API + if not self.is_online_api(): + logger.error("Cannot get storage status: NAS is not online or API is not accessible") + return {"error": "authentication_failed"} + + try: + # Пробуем получить информацию о хранилище через SYNO.Storage.CGI.Storage API + result = self._make_api_request("SYNO.Storage.CGI.Storage", "load_info", version=1) + + if not result: + # Пробуем альтернативный API + logger.info("Trying alternative API for storage info") + alt_result = self._make_api_request("SYNO.Core.Storage", "info", version=1) + + if not alt_result: + # Пробуем еще один альтернативный API + logger.info("Trying SYNO.Core.System API for storage info") + sys_result = self._make_api_request("SYNO.Core.System", "info", version=3) + + if not sys_result: + return { + "volumes": [], + "disks": [], + "total_size": 0, + "total_used": 0, + "error": "no_data" + } + + # Извлекаем базовую информацию о хранилище из системной информации + return { + "volumes": [], + "disks": [], + "total_size": sys_result.get("hdd_total_size", 0) * 1024**2, # МБ в байты + "total_used": sys_result.get("hdd_total_size", 0) * sys_result.get("hdd_usage", 0) / 100 * 1024**2, + } + + # Обрабатываем данные из альтернативного API + volumes = alt_result.get("volumes", []) + disks = alt_result.get("disks", []) + + else: + # Обрабатываем данные из основного API + volumes = result.get("volumes", []) + disks = result.get("disks", []) + + # Рассчитываем общие размеры + total_size = 0 + total_used = 0 + + for volume in volumes: + volume_size = volume.get("size", {}).get("total", 0) + volume_used = volume.get("size", {}).get("used", 0) + + total_size += volume_size + total_used += volume_used + + return { + "volumes": volumes, + "disks": disks, + "total_size": total_size, + "total_used": total_used + } + + except Exception as e: + logger.error(f"Error in get_storage_status: {str(e)}") + return {"error": str(e)} + + def get_security_status(self) -> Dict[str, Any]: + """Получение информации о состоянии безопасности""" + logger.info("Getting security status information") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for security status request") + return {"success": False, "error": "authentication_failed"} + + try: + # Пробуем получить информацию о безопасности через API Security Scan + result = self._make_api_request("SYNO.Core.SecurityScan.Status", "get", version=1) + + if not result: + # Пробуем альтернативный API + logger.info("Trying alternative API for security status") + # Проверяем статус брандмауэра + firewall_result = self._make_api_request("SYNO.Core.Security.Firewall.Status", "get", version=1) + + # Проверяем статус автоматических обновлений + update_result = self._make_api_request("SYNO.Core.Upgrade.Server.Setting", "get", version=1) + + # Если ни один из API не отвечает + if not firewall_result and not update_result: + # Получаем общую информацию о системе для базовой проверки безопасности + sys_result = self._make_api_request("SYNO.DSM.Info", "getinfo", version=2) + + if not sys_result: + return { + "success": False, + "status": "unknown", + "last_check": None, + "is_secure": False, + "error": "no_security_api" + } + + # Собираем базовые сведения из системной информации + return { + "success": True, + "status": "basic", + "last_check": None, + "is_secure": True, # Предполагаем, что система в целом безопасна + "firewall_enabled": None, + "auto_update": None, + "version_latest": sys_result.get("version_string", "") + } + + # Собираем информацию из доступных результатов + firewall_enabled = firewall_result.get("enabled", False) if firewall_result else None + auto_update = update_result.get("auto_update", False) if update_result else None + + # Определяем, насколько система безопасна + is_secure = True # По умолчанию предполагаем, что система безопасна + if firewall_enabled is not None and not firewall_enabled: + is_secure = False + + return { + "success": True, + "status": "partial", + "last_check": None, + "is_secure": is_secure, + "firewall_enabled": firewall_enabled, + "auto_update": auto_update + } + + # Если основное API отвечает, возвращаем его данные + return { + "success": True, + "status": result.get("status", "unknown"), + "last_check": result.get("last_check", None), + "is_secure": result.get("is_secure", False), + "details": result.get("details", {}) + } + + except Exception as e: + logger.error(f"Error in get_security_status: {str(e)}") + return {"success": False, "error": str(e)} + + def get_processes(self, limit: int = 20) -> List[Dict[str, Any]]: + """Получение списка активных процессов""" + logger.info(f"Getting list of active processes (limit={limit})") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for processes request") + return [] + + try: + # Получаем список процессов через API + result = self._make_api_request("SYNO.Core.System.Process", "list", version=1, + params={"sort_by": "cpu", "order": "DESC", "limit": limit}) + + if not result: + logger.warning("Failed to get process list") + return [] + + return result.get("processes", []) + + except Exception as e: + logger.error(f"Error getting process list: {str(e)}") + return [] + + def get_network_status(self) -> Dict[str, Any]: + """Получение информации о сетевых подключениях""" + logger.info("Getting network status information") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for network status request") + return {} + + try: + # Получаем информацию о сетевых интерфейсах + interface_result = self._make_api_request("SYNO.Core.Network.Interface", "list", version=1) + + # Получаем статистику использования сети + utilization_result = self._make_api_request("SYNO.Core.System.Utilization", "get", version=1) + + interfaces = [] + if interface_result: + interfaces = interface_result.get("interfaces", []) + + network_stats = {} + if utilization_result and "network" in utilization_result: + network_stats = utilization_result.get("network", {}) + + # Объединяем данные + for interface in interfaces: + iface_id = interface.get("id", "") + if iface_id in network_stats: + interface["rx"] = network_stats[iface_id].get("rx", 0) + interface["tx"] = network_stats[iface_id].get("tx", 0) + + return { + "interfaces": interfaces, + "statistics": network_stats + } + + except Exception as e: + logger.error(f"Error getting network status: {str(e)}") + return {} + + def get_system_logs(self, limit: int = 20) -> List[Dict[str, Any]]: + """Получение журналов системы""" + logger.info(f"Getting system logs (limit={limit})") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for system logs request") + return [] + + try: + # Получаем журналы через API + result = self._make_api_request("SYNO.Core.SyslogClient.Log", "list", version=1, + params={"start": 0, "limit": limit, "sort": "time", "dir": "DESC"}) + + if not result: + # Пробуем альтернативный API + logger.info("Trying alternative API for system logs") + alt_result = self._make_api_request("SYNO.Core.Log", "list", version=1, + params={"start": 0, "limit": limit, "sort": "time", "dir": "DESC"}) + + if alt_result: + return alt_result.get("logs", []) + return [] + + return result.get("logs", []) + + except Exception as e: + logger.error(f"Error getting system logs: {str(e)}") + return [] + + def get_power_schedule(self) -> Dict[str, Any]: + """Получение расписания включения/выключения""" + logger.info("Getting power schedule") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for power schedule request") + return {} + + try: + # Получаем расписание через API + result = self._make_api_request("SYNO.Core.Hardware.PowerSchedule", "get", version=1) + + if not result: + return {} + + return result + + except Exception as e: + logger.error(f"Error getting power schedule: {str(e)}") + return {} + + def set_power_schedule(self, schedule_type: str, days: List[int], time: str, enabled: bool = True) -> bool: + """Настройка расписания включения/выключения + + Args: + schedule_type: Тип расписания ('boot' или 'shutdown') + days: Список дней недели (0-6, где 0 - понедельник) + time: Время в формате 'HH:MM' + enabled: Включить или выключить расписание + + Returns: + True если успешно, False в противном случае + """ + logger.info(f"Setting power schedule for {schedule_type} at {time} on days {days}") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for setting power schedule") + return False + + try: + # Получаем текущее расписание + current_schedule = self._make_api_request("SYNO.Core.Hardware.PowerSchedule", "get", version=1) + + if not current_schedule: + logger.error("Failed to get current power schedule") + return False + + # Подготавливаем новое расписание + params = { + "enabled": enabled, + "type": schedule_type, + "day": days, + "time": time + } + + # Устанавливаем новое расписание + result = self._make_api_request("SYNO.Core.Hardware.PowerSchedule", "set", version=1, params=params) + + if not result: + logger.error("Failed to set power schedule") + return False + + logger.info(f"Power schedule for {schedule_type} set successfully") + return True + + except Exception as e: + logger.error(f"Error setting power schedule: {str(e)}") + return False + + def get_temperature_status(self) -> Dict[str, Any]: + """Получение информации о температуре системы и дисков""" + logger.info("Getting temperature status") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for temperature status request") + return {} + + try: + # Получаем информацию о системе для общей температуры + system_info = self._make_api_request("SYNO.DSM.Info", "getinfo", version=2) + + # Получаем информацию о дисках для их температуры + storage_info = self._make_api_request("SYNO.Storage.CGI.Storage", "load_info", version=1) + + system_temp = None + disk_temps = [] + + if system_info: + system_temp = system_info.get("temperature") + + if storage_info: + disks = storage_info.get("disks", []) + for disk in disks: + name = disk.get("name", "unknown") + model = disk.get("model", "unknown") + temp = disk.get("temp", None) + if temp is not None: + disk_temps.append({ + "name": name, + "model": model, + "temperature": temp + }) + + return { + "system_temperature": system_temp, + "disk_temperatures": disk_temps, + "warning": system_info.get("temperature_warn", False) if system_info else False + } + + except Exception as e: + logger.error(f"Error getting temperature status: {str(e)}") + return {} + + def browse_files(self, folder_path: str = "", limit: int = 100) -> Dict[str, Any]: + """Просмотр файлов в указанной директории + + Args: + folder_path: Путь к папке (пустая строка для корневых общих папок) + limit: Максимальное количество элементов для возврата + + Returns: + Словарь с информацией о файлах и папках + """ + logger.info(f"Browsing files in {folder_path or 'root'}") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for file browsing") + return {"success": False, "error": "authentication_failed"} + + try: + if not folder_path: + # Если путь не указан, получаем список общих папок + result = self._make_api_request("SYNO.FileStation.List", "list_share", version=2) + + if not result: + logger.error("Failed to list shared folders") + return {"success": False, "error": "api_error"} + + return { + "success": True, + "items": result.get("shares", []), + "path": "", + "is_root": True + } + else: + # Получаем список файлов в указанной директории + params = { + "folder_path": folder_path, + "limit": limit, + "offset": 0, + "sort_by": "name", + "sort_direction": "ASC" + } + + result = self._make_api_request("SYNO.FileStation.List", "list", version=2, params=params) + + if not result: + logger.error(f"Failed to list files in {folder_path}") + return {"success": False, "error": "api_error"} + + return { + "success": True, + "items": result.get("files", []), + "path": folder_path, + "is_root": False, + "total": result.get("total", 0) + } + + except Exception as e: + logger.error(f"Error browsing files: {str(e)}") + return {"success": False, "error": str(e)} + + def manage_service(self, service_name: str, action: str = "status") -> Dict[str, Any]: + """Управление системным сервисом + + Args: + service_name: Имя сервиса + action: Действие (status/start/stop/restart) + + Returns: + Словарь с результатом операции + """ + logger.info(f"Managing service {service_name}, action: {action}") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for service management") + return {"success": False, "error": "authentication_failed"} + + try: + # Проверяем доступное API для управления сервисами + if action == "status": + result = self._make_api_request("SYNO.Core.Service", "get", version=1, + params={"service": service_name}) + else: + result = self._make_api_request("SYNO.Core.Service", action, version=1, + params={"service": service_name}) + + if not result: + logger.error(f"Failed to {action} service {service_name}") + return {"success": False, "error": "api_error"} + + return { + "success": True, + "service": service_name, + "action": action, + "result": result, + "status": result.get("status") if action == "status" else "completed" + } + + except Exception as e: + logger.error(f"Error managing service {service_name}: {str(e)}") + return {"success": False, "error": str(e)} + + def search_files(self, pattern: str, folder_path: str = "", limit: int = 100) -> Dict[str, Any]: + """Поиск файлов по шаблону + + Args: + pattern: Шаблон для поиска + folder_path: Путь к папке для поиска (пустая строка для всех общих папок) + limit: Максимальное количество результатов + + Returns: + Словарь с найденными файлами + """ + logger.info(f"Searching for {pattern} in {folder_path or 'all shared folders'}") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for file search") + return {"success": False, "error": "authentication_failed"} + + try: + if not folder_path: + # Получаем список всех общих папок для поиска + shares_result = self._make_api_request("SYNO.FileStation.List", "list_share", version=2) + + if not shares_result: + logger.error("Failed to list shared folders for search") + return {"success": False, "error": "api_error"} + + # Формируем список путей для поиска + folder_paths = [share.get("path") for share in shares_result.get("shares", [])] + else: + folder_paths = [folder_path] + + # Запускаем поиск + params = { + "folder_path": folder_paths, + "pattern": pattern, + "limit": limit, + "offset": 0 + } + + result = self._make_api_request("SYNO.FileStation.Search", "start", version=2, params=params) + + if not result: + logger.error(f"Failed to start search for {pattern}") + return {"success": False, "error": "api_error"} + + # Получаем taskid для проверки результатов + taskid = result.get("taskid") + if not taskid: + logger.error("No taskid received for search") + return {"success": False, "error": "no_task_id"} + + # Ожидаем завершения поиска + search_result = {"finished": False, "progress": 0} + for _ in range(10): # Максимум 10 попыток + search_status = self._make_api_request("SYNO.FileStation.Search", "status", version=2, + params={"taskid": taskid}) + + if not search_status: + break + + search_result["progress"] = search_status.get("progress", 0) + + if search_status.get("finished", False): + search_result["finished"] = True + break + + time.sleep(0.5) # Пауза между запросами + + # Получаем результаты поиска + if search_result["finished"]: + list_result = self._make_api_request("SYNO.FileStation.Search", "list", version=2, + params={"taskid": taskid, "limit": limit}) + + if list_result: + files = list_result.get("files", []) + return { + "success": True, + "pattern": pattern, + "results": files, + "total": list_result.get("total", len(files)) + } + + # Если не удалось получить результаты, останавливаем поиск + self._make_api_request("SYNO.FileStation.Search", "stop", version=2, + params={"taskid": taskid}) + + return { + "success": False, + "error": "search_timeout", + "progress": search_result["progress"] + } + + except Exception as e: + logger.error(f"Error searching files: {str(e)}") + return {"success": False, "error": str(e)} + + def get_backup_status(self) -> Dict[str, Any]: + """Получение информации о резервном копировании""" + logger.info("Getting backup status information") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for backup status request") + return {"success": False, "error": "authentication_failed"} + + try: + # Пробуем получить информацию о Hyper Backup + hyper_result = self._make_api_request("SYNO.Backup.Repository", "list", version=1) + + # Пробуем получить информацию о задачах Time Backup + time_result = self._make_api_request("SYNO.BackupService.Task", "list", version=1) + + # Проверяем статус резервного копирования USB + usb_result = self._make_api_request("SYNO.USBCopy", "status", version=1) + + backups = { + "hyper_backup": hyper_result.get("backups", []) if hyper_result else [], + "time_backup": time_result.get("tasks", []) if time_result else [], + "usb_copy": {"enabled": usb_result.get("enable", False) if usb_result else False} + } + + return { + "success": True, + "backups": backups, + "available_apis": { + "hyper_backup": hyper_result is not None, + "time_backup": time_result is not None, + "usb_copy": usb_result is not None + } + } + + except Exception as e: + logger.error(f"Error getting backup status: {str(e)}") + return {"success": False, "error": str(e)} + + def check_for_updates(self) -> Dict[str, Any]: + """Проверка наличия обновлений системы""" + logger.info("Checking for system updates") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for update check") + return {"success": False, "error": "authentication_failed"} + + try: + # Получаем текущую информацию о системе + system_info = self._make_api_request("SYNO.DSM.Info", "getinfo", version=2) + + if not system_info: + logger.error("Failed to get system info for update check") + return {"success": False, "error": "api_error"} + + # Проверяем наличие обновлений + update_result = self._make_api_request("SYNO.Core.Upgrade.Server", "check", version=1) + + # Получаем настройки автоматического обновления + settings_result = self._make_api_request("SYNO.Core.Upgrade.Server.Setting", "get", version=1) + + # Получаем информацию о доступных обновлениях + update_info = self._make_api_request("SYNO.Core.Upgrade.Server.Download", "list", version=1) + + current_version = system_info.get("version_string", "unknown") + auto_update_enabled = settings_result.get("auto_update", False) if settings_result else False + + updates = [] + if update_info and "updates" in update_info: + updates = update_info.get("updates", []) + + update_available = len(updates) > 0 + + return { + "success": True, + "current_version": current_version, + "update_available": update_available, + "auto_update_enabled": auto_update_enabled, + "updates": updates + } + + except Exception as e: + logger.error(f"Error checking for updates: {str(e)}") + return {"success": False, "error": str(e)} + + def get_quota_info(self) -> Dict[str, Any]: + """Получение информации о квотах пользователей""" + logger.info("Getting user quota information") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for quota info request") + return {"success": False, "error": "authentication_failed"} + + try: + # Получаем список пользователей + users_result = self._make_api_request("SYNO.Core.User", "list", version=1) + + if not users_result: + logger.error("Failed to get user list for quota info") + return {"success": False, "error": "api_error"} + + users = users_result.get("users", []) + user_quotas = [] + + # Получаем квоты для каждого пользователя + for user in users: + user_name = user.get("name") + if not user_name: + continue + + quota_result = self._make_api_request("SYNO.Core.Quota", "get", version=1, + params={"user_name": user_name}) + + if quota_result and "quotas" in quota_result: + user_quotas.append({ + "user": user_name, + "quotas": quota_result.get("quotas", []) + }) + + return { + "success": True, + "user_quotas": user_quotas + } + + except Exception as e: + logger.error(f"Error getting quota info: {str(e)}") + return {"success": False, "error": str(e)} diff --git a/.history/src/api/synology_20250830101236.py b/.history/src/api/synology_20250830101236.py new file mode 100644 index 0000000..6d5a673 --- /dev/null +++ b/.history/src/api/synology_20250830101236.py @@ -0,0 +1,1861 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Модуль для взаимодействия с API Synology NAS +""" + +import requests +from requests.adapters import HTTPAdapter +import json +import logging +import time +import urllib3 +from urllib3.util import Retry +from typing import Dict, Any, Optional, List +import socket +import struct +from time import sleep + +from src.config.config import ( + SYNOLOGY_HOST, + SYNOLOGY_PORT, + SYNOLOGY_USERNAME, + SYNOLOGY_PASSWORD, + SYNOLOGY_SECURE, + SYNOLOGY_TIMEOUT, + SYNOLOGY_MAC, + WOL_PORT, + SYNOLOGY_API_VERSION, + SYNOLOGY_POWER_API, + SYNOLOGY_INFO_API +) +from src.api.api_discovery import discover_available_apis, find_compatible_api + +# Отключение предупреждений о небезопасных SSL-соединениях +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + +logger = logging.getLogger(__name__) + +class SynologyAPI: + """Класс для взаимодействия с API Synology NAS""" + + def __init__(self): + """Инициализация класса SynologyAPI""" + logger.info("Creating API with auto-retry and connection pool") + logger.debug(f"Connection details: Host={SYNOLOGY_HOST}, Port={SYNOLOGY_PORT}, Secure={SYNOLOGY_SECURE}") + + self.protocol = "https" if SYNOLOGY_SECURE else "http" + self.base_url = f"{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi" + self.sid = None + self.session = requests.Session() + + # Настройка SSL + if self.protocol == "https": + logger.debug("SSL enabled, disabling certificate verification for internal network") + self.session.verify = False # Отключаем проверку SSL для внутренней сети + + # Добавляем пользовательские заголовки для улучшения совместимости с API + custom_headers = { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36', + 'Accept': 'application/json, text/javascript, */*; q=0.01', + 'Accept-Language': 'ru-RU,ru;q=0.9,en-US;q=0.8,en;q=0.7', + 'X-Requested-With': 'XMLHttpRequest', + 'Connection': 'keep-alive', + 'Referer': f'{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/' + } + self.session.headers.update(custom_headers) + logger.debug("Added browser-like headers for API compatibility") + + # Добавляем повторные попытки для HTTP-запросов + retry_strategy = Retry( + total=5, # Увеличиваем количество попыток + status_forcelist=[429, 500, 502, 503, 504, 404], + allowed_methods=["GET", "POST"], + backoff_factor=1.5, # Увеличиваем задержку между попытками + respect_retry_after_header=True + ) + adapter = HTTPAdapter( + max_retries=retry_strategy, + pool_connections=3, + pool_maxsize=10 + ) + self.session.mount("http://", adapter) + self.session.mount("https://", adapter) + + # Таймауты будут указаны в запросах + self.default_timeout = (SYNOLOGY_TIMEOUT, SYNOLOGY_TIMEOUT*2) + logger.debug(f"Setting default request timeout: {self.default_timeout}") + + # Кэш для хранения результатов запросов + self._cache = {} + self._cache_ttl = {} + self._last_online_check = 0 + self._last_online_status = False + self._online_check_interval = 30 # Интервал для кэширования проверок онлайн-статуса + + # Время последней успешной аутентификации и срок действия сессии + self._last_auth_time = 0 + self._auth_expiry = 3600 # По умолчанию 1 час + + # Информация о доступных API + self._available_apis = {} + self._api_info_ttl = 0 + + # Инициализируем API version resolver для автоматического определения совместимых API + self.api_resolver = None # Будет создан при необходимости + + def login(self) -> bool: + """Авторизация в API Synology NAS""" + # Сбрасываем SID для новой сессии + self.sid = None + + logger.info("Attempting to authenticate with Synology NAS...") + logger.debug(f"Base URL: {self.base_url}") + + # Проверяем доступность NAS перед запросом с помощью прямой TCP-проверки + # Избегаем вызова is_online(), чтобы не создавать рекурсию + online_status = self._check_tcp_connection() + if not online_status: + logger.error("Cannot login: Synology NAS is not reachable") + return False + + # Пробуем различные версии API для аутентификации + # Начинаем с версии 3, которая показала лучшую совместимость в тестах + auth_versions_to_try = [3, 2, 1, 6] # Пробуем сначала более стабильные версии + + for auth_version in auth_versions_to_try: + try: + # Определяем путь к API аутентификации + auth_path = "entry.cgi" # Используем entry.cgi вместо auth.cgi по умолчанию + + # Проверка информации API для определения доступных версий API + api_info_url = f"{self.base_url}/entry.cgi" + api_info_params = { + "api": "SYNO.API.Info", + "version": "1", + "method": "query", + "query": "SYNO.API.Auth" + } + + logger.debug(f"Querying API info for auth version {auth_version}") + try: + api_info_response = self.session.get( + api_info_url, + params=api_info_params, + timeout=self.default_timeout, + verify=False + ) + + if api_info_response.status_code == 200: + api_info_data = api_info_response.json() + if api_info_data.get("success"): + auth_info = api_info_data.get("data", {}).get("SYNO.API.Auth", {}) + max_version = auth_info.get("maxVersion", 6) + min_version = auth_info.get("minVersion", 1) + auth_path = auth_info.get("path", "entry.cgi") + logger.debug(f"Auth API versions: min={min_version}, max={max_version}, path={auth_path}") + + # Проверяем поддержку текущей версии + if auth_version < min_version or auth_version > max_version: + logger.warning(f"Auth API version {auth_version} not supported (min={min_version}, max={max_version}), trying next version") + continue + else: + logger.warning("Failed to query API info, using default auth path") + else: + logger.warning(f"API info request failed with status {api_info_response.status_code}, using default auth path") + except Exception as e: + logger.warning(f"Error querying API info: {str(e)}, using default auth path") + + # Основной запрос авторизации + url = f"{self.base_url}/{auth_path}" + params = { + "api": "SYNO.API.Auth", + "version": str(auth_version), + "method": "login", + "account": SYNOLOGY_USERNAME, + "passwd": SYNOLOGY_PASSWORD, + "session": "SynologyPowerControlBot", + "format": "cookie" + } + + # Для версии 6+ используем немного другой формат + if auth_version >= 6: + params["enable_syno_token"] = "yes" + + logger.debug(f"Sending auth request to {url} with API version {auth_version}") + start_time = time.time() + response = self.session.get( + url, + params=params, + timeout=self.default_timeout, + verify=False + ) + elapsed_time = time.time() - start_time + logger.debug(f"Auth request completed in {elapsed_time:.2f}s with status code {response.status_code}") + + # Проверка статуса HTTP + if response.status_code != 200: + logger.error(f"HTTP error: {response.status_code}") + logger.debug(f"Response content: {response.text[:200]}") + continue # Пробуем следующую версию + + try: + data = response.json() + except json.JSONDecodeError: + logger.error("Failed to decode JSON response") + logger.debug(f"Response content: {response.text[:200]}") + continue # Пробуем следующую версию + + if data.get("success"): + self.sid = data.get("data", {}).get("sid") + self._last_auth_time = time.time() + logger.info(f"Successfully logged in to Synology NAS using API version {auth_version}") + logger.debug(f"Session ID obtained: {self.sid[:5] if self.sid else 'None'}...") + + # Получаем и сохраняем токен SYNO, если он есть + syno_token = data.get("data", {}).get("synotoken") + if syno_token: + self.session.headers.update({'X-SYNO-TOKEN': syno_token}) + logger.debug("Added X-SYNO-TOKEN header for improved API compatibility") + + # Также добавляем SID в cookies для улучшения совместимости + self.session.cookies.update({ + 'id': self.sid, + 'sid': self.sid + }) + logger.debug("Added SID to session cookies for improved compatibility") + + # Проверка валидности полученной сессии с помощью простого запроса + # Будем использовать SYNO.API.Info без проверки сложных методов + + # Даем системе немного времени для инициализации сессии + time.sleep(0.5) + + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to log in with version {auth_version}: Error code {error_code}") + + # Если ошибка связана с версией API, пробуем следующую версию + if error_code in [104, 105]: + logger.warning(f"Auth version {auth_version} not supported, trying next version") + continue + + # Дополнительная диагностика + if error_code == 400: + logger.error("Authentication error: Invalid credentials") + elif error_code == 401: + logger.error("Authentication error: Account disabled") + elif error_code == 402: + logger.error("Authentication error: Permission denied") + elif error_code == 403: + logger.error("Authentication error: 2-factor authentication required") + elif error_code == 404: + logger.error("Authentication error: Failed to authenticate with 2-factor authentication") + + # Если ошибка связана с неверными учетными данными, нет смысла пробовать другие версии API + if error_code in [400, 401, 402, 403, 404]: + return False + + except requests.exceptions.Timeout: + logger.error(f"Connection timeout during auth with version {auth_version}") + continue # Пробуем следующую версию + except requests.exceptions.ConnectionError as e: + logger.error(f"Connection error during auth with version {auth_version}: {str(e)}") + continue # Пробуем следующую версию + except requests.RequestException as e: + logger.error(f"Request error during auth with version {auth_version}: {str(e)}") + continue # Пробуем следующую версию + except Exception as e: + logger.error(f"Unexpected error during login with version {auth_version}: {str(e)}", exc_info=True) + continue # Пробуем следующую версию + + # Если все версии не сработали + logger.error("Failed to authenticate with any API version") + return False + + def _validate_session(self) -> bool: + """Проверяет валидность сессии после авторизации""" + if not self.sid: + return False + + # Попробуем сделать простой запрос для проверки сессии + test_apis = [ + {"api": "SYNO.Core.System", "method": "info", "version": 1}, + {"api": "SYNO.DSM.Info", "method": "getinfo", "version": 1} + ] + + for test_api in test_apis: + try: + url = f"{self.base_url}/entry.cgi" + params = { + "api": test_api["api"], + "version": str(test_api["version"]), + "method": test_api["method"], + "_sid": self.sid + } + + response = self.session.get(url, params=params, timeout=self.default_timeout, verify=False) + + if response.status_code == 200: + data = response.json() + if data.get("success"): + logger.debug(f"Session validation successful using {test_api['api']}") + return True + else: + error_code = data.get("error", {}).get("code", -1) + if error_code != 119: # Не сессия истекла + logger.debug(f"Session validation with {test_api['api']} returned error code {error_code}") + return True # Считаем сессию валидной, если ошибка не связана с истечением сессии + except Exception as e: + logger.warning(f"Error during session validation with {test_api['api']}: {str(e)}") + + logger.warning("Session validation failed with all test APIs") + return False + + def logout(self) -> bool: + """Выход из API Synology NAS""" + if not self.sid: + return True + + try: + url = f"{self.base_url}/auth.cgi" + params = { + "api": "SYNO.API.Auth", + "version": "1", + "method": "logout", + "session": "SynologyPowerControlBot", + "_sid": self.sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + self.sid = None + logger.info("Successfully logged out from Synology NAS") + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to log out from Synology NAS: Error code {error_code}") + return False + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + return False + + def _make_api_request(self, api_name: str, method: str, version: int = 1, + params: Optional[Dict[str, Any]] = None, retry_auth: bool = True, retry_count: int = 0) -> Optional[Dict[str, Any]]: + """Обобщенный метод для выполнения API запросов с обработкой ошибок""" + # Ограничение на количество повторных попыток + if retry_count >= 3: + logger.error(f"Too many retries for {api_name}.{method}, giving up") + return None + + # Проверка наличия авторизации + if not self.sid and not self.login(): + logger.error(f"Not authenticated for API request: {api_name}.{method}") + return None + + # Проверка информации API для определения пути и поддерживаемой версии + api_info_url = f"{self.base_url}/entry.cgi" + api_info_params = { + "api": "SYNO.API.Info", + "version": "1", + "method": "query", + "query": api_name + } + + api_path = "entry.cgi" + try: + logger.debug(f"Querying API info for {api_name}") + api_info_response = self.session.get( + api_info_url, + params=api_info_params, + timeout=self.default_timeout, + verify=False + ) + + if api_info_response.status_code == 200: + api_info_data = api_info_response.json() + if api_info_data.get("success"): + api_info = api_info_data.get("data", {}).get(api_name, {}) + if api_info: + max_version = api_info.get("maxVersion", version) + min_version = api_info.get("minVersion", version) + api_path = api_info.get("path", "entry.cgi") + + # Проверка, поддерживается ли запрошенная версия + if version < min_version: + logger.warning(f"API version {version} for {api_name} is below minimum {min_version}, using {min_version}") + version = min_version + elif version > max_version: + logger.warning(f"API version {version} for {api_name} exceeds maximum {max_version}, using {max_version}") + version = max_version + + logger.debug(f"Using API path: {api_path}, version: {version}") + else: + logger.warning(f"API {api_name} not found in API info, using defaults") + except Exception as e: + logger.warning(f"Error querying API info for {api_name}: {str(e)}, using defaults") + + # Подготовка базовых параметров запроса + base_params = { + "api": api_name, + "version": str(version), + "method": method, + "_sid": self.sid # Используем _sid вместо sid для лучшей совместимости + } + + # Добавление дополнительных параметров, если они заданы + if params: + base_params.update(params) + + url = f"{self.base_url}/{api_path}" + logger.debug(f"API request: {api_name}.{method} v{version} to {url}") + logger.debug(f"Full request params: {base_params}") + + try: + start_time = time.time() + response = self.session.get( + url, + params=base_params, + timeout=self.default_timeout, + verify=False + ) + elapsed_time = time.time() - start_time + logger.debug(f"API request completed in {elapsed_time:.2f}s with status {response.status_code}") + + # Проверка статуса HTTP + if response.status_code != 200: + logger.error(f"HTTP error: {response.status_code} for {api_name}.{method}") + + # Повторная попытка при ошибках соединения + if response.status_code in [500, 502, 503, 504]: + logger.info(f"Server error, retrying request for {api_name}.{method}") + sleep(2) + return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1) + + return None + + try: + data = response.json() + except json.JSONDecodeError: + logger.error(f"Failed to decode JSON response for {api_name}.{method}") + logger.debug(f"Response content: {response.text[:200]}") + + # Повторная попытка при ошибках декодирования + logger.info(f"JSON decode error, retrying request for {api_name}.{method}") + sleep(2) + return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1) + + if data.get("success"): + logger.info(f"API request successful for {api_name}.{method}") + return data.get("data", {}) + else: + error_code = data.get("error", {}).get("code", -1) + error_desc = self._get_error_description(error_code) + logger.error(f"API error for {api_name}.{method}: {error_code} - {error_desc}") + + # Ошибки доступа или прав часто встречаются, но они не критичные + # Например, ошибка 102 означает, что нет прав, но NAS доступен + if error_code in [102, 103, 104, 105]: + logger.warning(f"Permission-related error for {api_name}.{method}: {error_code}") + # Возвращаем пустой словарь вместо None, + # чтобы вызывающий код мог понять, что запрос выполнен + return {} + + # Если ошибка связана с авторизацией и нам разрешено повторить попытку + if error_code in [106, 107, 119] and retry_auth: + logger.info(f"Session error (code {error_code}), creating fresh session...") + self.sid = None # Сбрасываем SID + + # Для ошибки 119 (Session timeout) дадим системе немного времени + if error_code == 119: + logger.info("Session timeout detected, waiting before retry...") + sleep(3) + + if self.login(): + logger.info("Re-authenticated with fresh session, retrying API request...") + # Рекурсивный вызов, но со счетчиком повторов + return self._make_api_request(api_name, method, version, params, False, retry_count + 1) + + # Для некоторых ошибок можно автоматически повторить запрос + if error_code in [408, 429, 500, 502, 503, 504]: + logger.info(f"Temporary error {error_code}, retrying request for {api_name}.{method}") + sleep(2) + return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1) + + return None + + except requests.exceptions.Timeout: + logger.error(f"Request timeout for {api_name}.{method}") + + # Повторная попытка при таймауте + if retry_count < 2: + logger.info(f"Timeout, retrying request for {api_name}.{method}") + sleep(2) + return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1) + + return None + except requests.exceptions.ConnectionError as e: + logger.error(f"Connection error for {api_name}.{method}: {str(e)}") + + # Повторная попытка при ошибке соединения + if retry_count < 2: + logger.info(f"Connection error, retrying request for {api_name}.{method}") + sleep(2) + return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1) + + return None + except Exception as e: + logger.error(f"Unexpected error during {api_name}.{method}: {str(e)}") + return None + + def get_system_status(self) -> Dict[str, Any]: + """Получение статуса системы""" + # Проверяем доступность системы + if not self.is_online(): + logger.info("Device is offline, skipping API request") + return {"status": "offline"} + + # Проверяем, есть ли кэшированный результат + cache_key = "system_status" + current_time = time.time() + if cache_key in self._cache and current_time - self._cache_ttl.get(cache_key, 0) < 60: + logger.debug("Using cached system status") + return self._cache[cache_key] + + # Используем рекомендованный API для получения информации о системе + logger.info(f"Getting system info using {SYNOLOGY_INFO_API} v{SYNOLOGY_API_VERSION}") + + # Для SYNO.DSM.Info метод getinfo, для других обычно используется get или info + if SYNOLOGY_INFO_API == "SYNO.DSM.Info": + method = "getinfo" + else: + method = "get" + + result = self._make_api_request(SYNOLOGY_INFO_API, method, version=SYNOLOGY_API_VERSION) + + if result: + logger.info(f"Successfully retrieved system info using {SYNOLOGY_INFO_API}") + + # Формируем расширенный ответ с дополнительной информацией + system_info = { + "status": "online", + "hostname": result.get("hostname", "unknown"), + "model": result.get("model", "unknown"), + "version": result.get("version", "unknown"), + "uptime": result.get("uptime", 0), + "time": current_time, + "is_online": True, + "api_used": SYNOLOGY_INFO_API + } + + # Сохраняем в кэше + self._cache[cache_key] = system_info + self._cache_ttl[cache_key] = current_time + + return system_info + + # Если основной API не сработал, пробуем резервные варианты + logger.warning(f"Failed to retrieve system info with {SYNOLOGY_INFO_API}, trying fallback APIs") + + # Пробуем резервные API + apis_to_try = [ + {"name": "SYNO.DSM.Info", "method": "getinfo", "version": 2}, + {"name": "SYNO.Core.System", "method": "info", "version": 1}, + {"name": "SYNO.Core.System.Status", "method": "get", "version": 1}, + {"name": "SYNO.Core.System.Info", "method": "get", "version": 1}, + ] + + for api in apis_to_try: + if api["name"] == SYNOLOGY_INFO_API: + continue # Пропускаем уже проверенный API + + logger.info(f"Trying to get system info using {api['name']}.{api['method']} v{api['version']}") + result = self._make_api_request(api["name"], api["method"], version=api["version"]) + + if result: + logger.info(f"Successfully retrieved system info using {api['name']}") + + # Формируем расширенный ответ с дополнительной информацией + system_info = { + "status": "online", + "hostname": result.get("hostname", "unknown"), + "model": result.get("model", "unknown"), + "version": result.get("version", "unknown"), + "uptime": result.get("uptime", 0), + "time": current_time, + "is_online": True, + "api_used": api["name"] + } + + # Сохраняем в кэше + self._cache[cache_key] = system_info + self._cache_ttl[cache_key] = current_time + + return system_info + + # Если все запросы не удались, но система онлайн, возвращаем базовую информацию + logger.warning("Failed to retrieve system info with all API methods") + return { + "status": "error", + "error": "Failed to fetch system information", + "is_online": True, + "time": current_time + } + + def shutdown_system(self) -> bool: + """Выключение системы""" + # Проверяем, включено ли устройство перед попыткой его выключить + if not self.is_online(force_check=True): + logger.info("Synology NAS is already offline, no need to shut down") + return True + + logger.info("Attempting to shutdown Synology NAS...") + + # Попробуем сначала использовать предпочтительный API для управления питанием + logger.info(f"Trying shutdown with {SYNOLOGY_POWER_API} v{SYNOLOGY_API_VERSION}") + + # Для SYNO.Core.Hardware.PowerRecovery используется метод setPowerOnState + # Для других API обычно используется метод shutdown или reboot + if SYNOLOGY_POWER_API == "SYNO.Core.Hardware.PowerRecovery": + # Для этого API нужны специальные параметры + params = {"state": "powerbtn"} # powerbtn имитирует нажатие кнопки питания + result = self._make_api_request(SYNOLOGY_POWER_API, "setPowerOnState", version=SYNOLOGY_API_VERSION, params=params) + else: + # Пробуем стандартный метод + result = self._make_api_request(SYNOLOGY_POWER_API, "shutdown", version=SYNOLOGY_API_VERSION) + + if result is not None: + logger.info(f"Successfully initiated system shutdown using {SYNOLOGY_POWER_API}") + + # Даем системе время начать процесс выключения + logger.info("Waiting for shutdown to initialize...") + sleep(5) + + # Проверяем статус + if not self.is_online(force_check=True): + logger.info("System is now offline. Shutdown confirmed successful.") + return True + else: + logger.info("System still appears to be online, but shutdown may be in progress.") + return True + else: + logger.warning(f"Failed to shutdown using {SYNOLOGY_POWER_API}, trying alternative methods") + + # Если не сработал основной метод, пробуем резервные варианты + # Проверка всех доступных методов API для выключения + apis_to_try = [ + {"name": "SYNO.Core.System", "method": "shutdown", "version": 3}, + {"name": "SYNO.Core.System.Power", "method": "shutdown", "version": 1}, + {"name": "SYNO.DSM.Power", "method": "shutdown", "version": 1}, + {"name": "SYNO.System.Power", "method": "shutdown", "version": 1}, + {"name": "SYNO.DSM.System", "method": "shutdown", "version": 1} + ] + + # Проверяем доступные API + try: + api_info_url = f"{self.base_url}/entry.cgi" + api_info_params = { + "api": "SYNO.API.Info", + "version": "1", + "method": "query", + "query": "SYNO.Core.System.Power,SYNO.DSM.Power,SYNO.Core.System,SYNO.System.Power,SYNO.DSM.System" + } + + logger.debug("Checking available shutdown APIs") + api_info_response = self.session.get( + api_info_url, + params=api_info_params, + timeout=self.default_timeout, + verify=False + ) + + if api_info_response.status_code == 200: + api_info_data = api_info_response.json() + if api_info_data.get("success"): + available_apis = api_info_data.get("data", {}) + logger.debug(f"Available APIs: {list(available_apis.keys())}") + + # Фильтруем только доступные API + filtered_apis = [] + for api in apis_to_try: + if api["name"] in available_apis: + api_info = available_apis[api["name"]] + # Проверка версии API + if api_info.get("minVersion", 1) <= api["version"] <= api_info.get("maxVersion", 1): + filtered_apis.append(api) + logger.debug(f"Adding {api['name']} to available shutdown APIs") + else: + logger.debug(f"API {api['name']} version {api['version']} not supported. Supported: {api_info.get('minVersion')}-{api_info.get('maxVersion')}") + + if filtered_apis: + apis_to_try = filtered_apis + else: + logger.warning("No compatible APIs found, trying all methods as fallback") + else: + logger.warning("Failed to query API info, using default methods") + else: + logger.warning(f"API info request failed with status {api_info_response.status_code}, using default methods") + except Exception as e: + logger.warning(f"Error querying API info: {str(e)}, using default methods") + + # Пробуем все доступные методы по порядку + for api in apis_to_try: + logger.info(f"Trying shutdown with {api['name']}.{api['method']} v{api['version']}") + result = self._make_api_request(api["name"], api["method"], version=api["version"]) + + if result is not None: + logger.info(f"Successfully initiated system shutdown using {api['name']}") + + # Даем системе время начать процесс выключения + logger.info("Waiting for shutdown to initialize...") + sleep(5) + + # Проверяем статус + if not self.is_online(force_check=True): + logger.info("System is now offline. Shutdown confirmed successful.") + return True + else: + logger.info("System still appears to be online, but shutdown may be in progress.") + return True + else: + logger.warning(f"Failed to shutdown using {api['name']}.{api['method']} v{api['version']}") + + # Если ни один метод не сработал, но система стала недоступна + if not self.is_online(force_check=True): + logger.info("System appears to be shutting down despite API errors") + return True + + logger.error("Failed to shutdown system after trying multiple APIs") + return False + + def reboot_system(self) -> bool: + """Перезагрузка системы""" + # Проверяем, включена ли система + if not self.is_online(force_check=True): + logger.error("Cannot reboot: System is offline") + return False + + logger.info("Attempting to reboot Synology NAS...") + + # Список API и методов для попытки перезагрузки + apis_to_try = [ + {"api": "SYNO.Core.System", "method": "reboot", "version": 1}, + {"api": "SYNO.DSM.System", "method": "reboot", "version": 1}, + {"api": "SYNO.Core.System.Power", "method": "restart", "version": 1}, + {"api": "SYNO.DSM.Power", "method": "reboot", "version": 1}, + {"api": "SYNO.Core.System", "method": "reboot", "version": 3}, + {"api": "SYNO.System.Power", "method": "reboot", "version": 1}, + {"api": "SYNO.Core.Hardware.NeedReboot", "method": "reboot", "version": 1} + ] + + # Добавляем настроенный в конфигурации API, если он отличается от уже добавленных + already_added = [item["api"] for item in apis_to_try] + if SYNOLOGY_POWER_API not in already_added: + for method in ["restart", "reboot"]: + apis_to_try.append({ + "api": SYNOLOGY_POWER_API, + "method": method, + "version": SYNOLOGY_API_VERSION + }) + + # Перебираем все возможные API и методы + for api_info in apis_to_try: + try: + logger.info(f"Trying reboot with {api_info['api']} API using method {api_info['method']}") + result = self._make_api_request(api_info['api'], api_info['method'], version=api_info['version']) + if result is not None: + logger.info(f"Successfully initiated system reboot using {api_info['api']} API") + + # Даем системе время начать процесс перезагрузки + logger.info("Waiting for reboot to initialize...") + sleep(5) + + # Ждем, пока система станет недоступна (признак перезагрузки) + reboot_started = False + for i in range(12): # Проверяем в течение 60 секунд (12 * 5сек) + if not self.is_online(force_check=True): + logger.info(f"System went offline after {i*5} seconds, reboot in progress") + reboot_started = True + break + logger.debug(f"System still online, waiting... ({i+1}/12)") + sleep(5) + + if reboot_started: + # Успешный вызов API и система ушла оффлайн - это признак успешной перезагрузки + return True + else: + # Успешный вызов API, но система не ушла оффлайн + logger.warning("System did not go offline after reboot command, but command was accepted") + # Даже если система не ушла оффлайн, команда могла быть принята + return True + except Exception as e: + logger.error(f"Error during reboot with {api_info['api']}: {str(e)}") + + # Если все попытки не удались, возвращаем False + logger.error("All reboot attempts failed") + return False + + def _get_error_description(self, error_code: int) -> str: + """Получение описания ошибки по коду""" + error_descriptions = { + 100: "Unknown error", + 101: "Invalid parameter", + 102: "API does not exist", + 103: "Method does not exist", + 104: "Version does not support", + 105: "Permission denied", + 106: "Session timeout", + 107: "Session interrupted by duplicate login", + 400: "Invalid credentials", + 401: "Account disabled", + 402: "Permission denied", + 403: "2FA required", + 404: "Failed to authenticate with 2FA" + } + return error_descriptions.get(error_code, "Unknown error code") + + def _check_tcp_connection(self) -> bool: + """Проверка базового TCP-соединения с Synology NAS""" + try: + socket_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + socket_obj.settimeout(SYNOLOGY_TIMEOUT) + result = socket_obj.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT)) + socket_obj.close() + + return result == 0 + except socket.error as e: + logger.error(f"Socket error during connection check: {str(e)}") + return False + except Exception as e: + logger.error(f"Unexpected error during connection check: {str(e)}") + return False + + def is_online(self, force_check=False) -> bool: + """Проверка онлайн-статуса Synology NAS""" + # Используем кэшированное значение, если доступно и не устарело + current_time = time.time() + if not force_check and (current_time - self._last_online_check) < self._online_check_interval: + logger.debug(f"Using cached online status: {self._last_online_status}") + return self._last_online_status + + logger.info("Checking if NAS is online...") + + # Проверяем TCP-соединение + online_status = self._check_tcp_connection() + logger.info(f"Detected Synology NAS online status: {online_status}") + + # Если TCP-соединение успешно и у нас есть действующий SID, + # попробуем более детальную проверку через API + if online_status and self.sid: + logger.info("Trying to fetch more detailed online status through API...") + + # Пробуем разные API для проверки онлайн-статуса + api_checks = [ + {"api": "SYNO.DSM.Info", "version": "2", "method": "getinfo"}, + {"api": "SYNO.Core.System", "version": "1", "method": "info"}, + {"api": "SYNO.Core.System.Status", "version": "1", "method": "get"} + ] + + api_success = False + for api_check in api_checks: + try: + url = f"{self.base_url}/entry.cgi" + params = { + "api": api_check["api"], + "version": api_check["version"], + "method": api_check["method"], + "sid": self.sid + } + + logger.debug(f"Trying online status check with {api_check['api']}") + response = self.session.get( + url, + params=params, + timeout=self.default_timeout, + verify=False + ) + + if response.status_code == 200: + data = response.json() + if data.get("success"): + logger.info(f"API request successful for {api_check['api']}") + logger.info("Synology NAS is online with API access") + api_success = True + break + else: + error_code = data.get("error", {}).get("code", -1) + logger.warning(f"API response indicates an error: {error_code}, but NAS is reachable") + else: + logger.warning(f"API returned status code {response.status_code}") + except Exception as e: + logger.warning(f"API check failed for {api_check['api']}, but TCP connection succeeded: {str(e)}") + + if not api_success: + logger.warning("All API checks failed, but TCP connection is successful") + + # Обновляем кэшированное значение + self._last_online_check = current_time + self._last_online_status = online_status + + return online_status + + def wake_on_lan(self) -> bool: + """Отправка Wake-on-LAN пакета для включения Synology NAS""" + if not SYNOLOGY_MAC: + logger.error("MAC address not configured") + return False + + try: + # Преобразование MAC-адреса в байты + mac_address = SYNOLOGY_MAC.replace(':', '').replace('-', '') + if len(mac_address) != 12: + logger.error(f"Invalid MAC address format: {SYNOLOGY_MAC}") + return False + + try: + mac_bytes = bytes.fromhex(mac_address) + except ValueError as e: + logger.error(f"Failed to parse MAC address: {str(e)}") + logger.error(f"MAC address must contain only hex characters: {SYNOLOGY_MAC}") + return False + + # Создание Magic Packet + magic_packet = b'\xff' * 6 + mac_bytes * 16 + + # Отправка пакета на конкретный адрес + try: + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) + sock.sendto(magic_packet, (SYNOLOGY_HOST, WOL_PORT)) + sock.close() + logger.info(f"Wake-on-LAN packet sent to {SYNOLOGY_MAC} at {SYNOLOGY_HOST}:{WOL_PORT}") + except Exception as e: + logger.error(f"Error sending directed WoL packet: {str(e)}") + return False + + # Для надежности отправляем также широковещательный пакет + try: + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) + + # Используем стандартный широковещательный адрес + broadcast_addr = "255.255.255.255" + sock.sendto(magic_packet, (broadcast_addr, WOL_PORT)) + sock.close() + logger.info(f"Wake-on-LAN broadcast packet sent to {broadcast_addr}:{WOL_PORT}") + except Exception as e: + logger.warning(f"Error sending broadcast WoL packet (non-critical): {str(e)}") + # Не считаем ошибкой, т.к. основной пакет уже отправлен + + return True + + except Exception as e: + logger.error(f"Unexpected error in wake_on_lan: {str(e)}") + return False + + def wait_for_boot(self, max_attempts: int = 30, delay: int = 10) -> bool: + """Ожидание загрузки Synology NAS""" + logger.info(f"Waiting for Synology NAS to boot (timeout: {max_attempts * delay}s)...") + + for attempt in range(max_attempts): + # Принудительно проверяем статус без использования кэша + if self.is_online(force_check=True): + logger.info(f"Synology NAS is online after {attempt + 1} attempts ({(attempt + 1) * delay}s)") + + # Проверяем, что не только сеть доступна, но и API загрузился + api_ready = False + logger.info("Waiting for API services to initialize...") + + for api_check in range(5): # Даем еще до 50 секунд для загрузки API + if self.sid or self.login(): + api_ready = True + logger.info(f"API services are ready after {api_check + 1} attempts") + break + logger.debug(f"API not ready yet, waiting... ({api_check + 1}/5)") + sleep(10) + + if not api_ready: + logger.warning("System is online but API services may not be fully initialized") + + # Дадим дополнительное время для полной загрузки всех сервисов + sleep(delay) + return True + + sleep(delay) + logger.info(f"Waiting for boot... Attempt {attempt + 1}/{max_attempts}") + + logger.error(f"Synology NAS didn't come online after {max_attempts * delay} seconds") + return False + + def power_on(self) -> bool: + """Включение Synology NAS""" + # Принудительная проверка статуса + if self.is_online(force_check=True): + logger.info("Synology NAS is already online") + return True + + logger.info("Powering on Synology NAS via Wake-on-LAN...") + + # Проверяем, настроен ли MAC-адрес + if not SYNOLOGY_MAC: + logger.error("Cannot power on: MAC address not configured in settings") + return False + + # Пробуем отправить несколько WoL пакетов для надежности + success = False + for attempt in range(3): + logger.debug(f"Sending WoL packet, attempt {attempt + 1}/3") + if self.wake_on_lan(): + success = True + break + sleep(1) + + if not success: + logger.error("Failed to send Wake-on-LAN packets") + return False + + # Ожидание загрузки + logger.info("WoL packets sent successfully, waiting for system to boot...") + boot_result = self.wait_for_boot(max_attempts=30, delay=10) + + if boot_result: + # Проверяем доступность API после загрузки + system_status = self.get_system_status() + if system_status.get("status") == "online": + logger.info("System booted successfully with API access") + return True + else: + logger.warning("System appears to be online but API may not be fully ready") + return True + else: + logger.error("System did not come online after WoL") + return False + + def power_off(self) -> bool: + """Выключение Synology NAS""" + if not self.is_online(force_check=True): + logger.info("Synology NAS is already offline") + return True + + logger.info("Powering off Synology NAS...") + + # Список API и методов для попытки выключения + apis_to_try = [ + {"api": "SYNO.Core.System", "method": "shutdown", "version": 1}, + {"api": "SYNO.DSM.System", "method": "shutdown", "version": 1}, + {"api": SYNOLOGY_POWER_API, "method": "shutdown", "version": SYNOLOGY_API_VERSION} + ] + + # Перебираем все возможные API и методы + for api_info in apis_to_try: + try: + logger.info(f"Trying shutdown with {api_info['api']} API using method {api_info['method']}") + api_result = self._make_api_request(api_info['api'], api_info['method'], version=api_info['version']) + if api_result is not None: + logger.info(f"Successfully initiated system shutdown using {api_info['api']} API") + + # Даем системе время начать процесс выключения + logger.info("Waiting for shutdown to initialize...") + sleep(5) + + return True + except Exception as e: + logger.error(f"Error during shutdown with {api_info['api']}: {str(e)}") + + # Если все попытки не удались, возвращаем False + logger.error("All shutdown attempts failed") + return False + + # Если все еще не сработало, используем оригинальный метод shutdown_system + if not result: + result = self.shutdown_system() + + if result: + # Дополнительная проверка, что система действительно выключилась + logger.info("Verifying system is offline...") + for attempt in range(12): # Проверяем в течение 2 минут (12 * 10 сек) + if not self.is_online(force_check=True): + logger.info(f"System confirmed offline after {attempt * 10} seconds") + return True + logger.debug(f"System still shutting down, checking again... ({attempt + 1}/12)") + sleep(10) + + logger.warning("System still appears to be online after shutdown command") + return False + else: + logger.error("Failed to initiate shutdown") + return False + + # Заглушки для расширенных методов + def get_shared_folders(self) -> List[Dict[str, Any]]: + """Получение списка общих папок""" + logger.info("Getting list of shared folders") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for shared folders request") + return [] + + try: + # Запрашиваем список общих папок через FileStation API + result = self._make_api_request("SYNO.FileStation.List", "list_share", version=2) + + if not result: + # Пробуем альтернативный API + logger.info("Trying alternative API for shared folders") + alt_result = self._make_api_request("SYNO.FileStation.List", "list_share", version=1) + if alt_result: + return alt_result.get("shares", []) + return [] + + return result.get("shares", []) + + except Exception as e: + logger.error(f"Error getting shared folders: {str(e)}") + return [] + + def get_system_load(self) -> Dict[str, Any]: + """Получение информации о загрузке системы""" + logger.info("Getting system load information") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for system load request") + return {} + + try: + # Запрашиваем информацию о загрузке системы + result = self._make_api_request("SYNO.Core.System.Utilization", "get", version=1) + + if not result: + # Пробуем альтернативный API + logger.info("Trying alternative API for system load") + alt_result = self._make_api_request("SYNO.Core.System", "info", version=3) + + if not alt_result: + return {} + + # Формируем из частичных данных + return { + "cpu_load": alt_result.get("cpu_usage", 0), + "memory": { + "total": alt_result.get("memory_size", 0), + "used": alt_result.get("memory_usage", 0), + "usage_percent": alt_result.get("memory_usage_percent", 0) + } + } + + # Формируем структурированный результат + return { + "cpu_load": result.get("cpu", {}).get("user", 0) + result.get("cpu", {}).get("system", 0), + "memory": result.get("memory", {}), + "network": result.get("network", {}) + } + + except Exception as e: + logger.error(f"Error getting system load: {str(e)}") + return {} + + def is_online_api(self) -> bool: + """Проверка онлайн-статуса Synology NAS с использованием API""" + if not self.is_online(): + return False + + # Проверяем доступность API через авторизацию + if not self.sid and not self.login(): + return False + + return True + + def get_storage_status(self) -> Dict[str, Any]: + """Получение подробной информации о хранилище""" + logger.info("Getting storage status information") + + # Проверяем доступность NAS и API + if not self.is_online_api(): + logger.error("Cannot get storage status: NAS is not online or API is not accessible") + return {"error": "authentication_failed"} + + try: + # Пробуем получить информацию о хранилище через SYNO.Storage.CGI.Storage API + result = self._make_api_request("SYNO.Storage.CGI.Storage", "load_info", version=1) + + if not result: + # Пробуем альтернативный API + logger.info("Trying alternative API for storage info") + alt_result = self._make_api_request("SYNO.Core.Storage", "info", version=1) + + if not alt_result: + # Пробуем еще один альтернативный API + logger.info("Trying SYNO.Core.System API for storage info") + sys_result = self._make_api_request("SYNO.Core.System", "info", version=3) + + if not sys_result: + return { + "volumes": [], + "disks": [], + "total_size": 0, + "total_used": 0, + "error": "no_data" + } + + # Извлекаем базовую информацию о хранилище из системной информации + return { + "volumes": [], + "disks": [], + "total_size": sys_result.get("hdd_total_size", 0) * 1024**2, # МБ в байты + "total_used": sys_result.get("hdd_total_size", 0) * sys_result.get("hdd_usage", 0) / 100 * 1024**2, + } + + # Обрабатываем данные из альтернативного API + volumes = alt_result.get("volumes", []) + disks = alt_result.get("disks", []) + + else: + # Обрабатываем данные из основного API + volumes = result.get("volumes", []) + disks = result.get("disks", []) + + # Рассчитываем общие размеры + total_size = 0 + total_used = 0 + + for volume in volumes: + volume_size = volume.get("size", {}).get("total", 0) + volume_used = volume.get("size", {}).get("used", 0) + + total_size += volume_size + total_used += volume_used + + return { + "volumes": volumes, + "disks": disks, + "total_size": total_size, + "total_used": total_used + } + + except Exception as e: + logger.error(f"Error in get_storage_status: {str(e)}") + return {"error": str(e)} + + def get_security_status(self) -> Dict[str, Any]: + """Получение информации о состоянии безопасности""" + logger.info("Getting security status information") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for security status request") + return {"success": False, "error": "authentication_failed"} + + try: + # Пробуем получить информацию о безопасности через API Security Scan + result = self._make_api_request("SYNO.Core.SecurityScan.Status", "get", version=1) + + if not result: + # Пробуем альтернативный API + logger.info("Trying alternative API for security status") + # Проверяем статус брандмауэра + firewall_result = self._make_api_request("SYNO.Core.Security.Firewall.Status", "get", version=1) + + # Проверяем статус автоматических обновлений + update_result = self._make_api_request("SYNO.Core.Upgrade.Server.Setting", "get", version=1) + + # Если ни один из API не отвечает + if not firewall_result and not update_result: + # Получаем общую информацию о системе для базовой проверки безопасности + sys_result = self._make_api_request("SYNO.DSM.Info", "getinfo", version=2) + + if not sys_result: + return { + "success": False, + "status": "unknown", + "last_check": None, + "is_secure": False, + "error": "no_security_api" + } + + # Собираем базовые сведения из системной информации + return { + "success": True, + "status": "basic", + "last_check": None, + "is_secure": True, # Предполагаем, что система в целом безопасна + "firewall_enabled": None, + "auto_update": None, + "version_latest": sys_result.get("version_string", "") + } + + # Собираем информацию из доступных результатов + firewall_enabled = firewall_result.get("enabled", False) if firewall_result else None + auto_update = update_result.get("auto_update", False) if update_result else None + + # Определяем, насколько система безопасна + is_secure = True # По умолчанию предполагаем, что система безопасна + if firewall_enabled is not None and not firewall_enabled: + is_secure = False + + return { + "success": True, + "status": "partial", + "last_check": None, + "is_secure": is_secure, + "firewall_enabled": firewall_enabled, + "auto_update": auto_update + } + + # Если основное API отвечает, возвращаем его данные + return { + "success": True, + "status": result.get("status", "unknown"), + "last_check": result.get("last_check", None), + "is_secure": result.get("is_secure", False), + "details": result.get("details", {}) + } + + except Exception as e: + logger.error(f"Error in get_security_status: {str(e)}") + return {"success": False, "error": str(e)} + + def get_processes(self, limit: int = 20) -> List[Dict[str, Any]]: + """Получение списка активных процессов""" + logger.info(f"Getting list of active processes (limit={limit})") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for processes request") + return [] + + try: + # Получаем список процессов через API + result = self._make_api_request("SYNO.Core.System.Process", "list", version=1, + params={"sort_by": "cpu", "order": "DESC", "limit": limit}) + + if not result: + logger.warning("Failed to get process list") + return [] + + return result.get("processes", []) + + except Exception as e: + logger.error(f"Error getting process list: {str(e)}") + return [] + + def get_network_status(self) -> Dict[str, Any]: + """Получение информации о сетевых подключениях""" + logger.info("Getting network status information") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for network status request") + return {} + + try: + # Получаем информацию о сетевых интерфейсах + interface_result = self._make_api_request("SYNO.Core.Network.Interface", "list", version=1) + + # Получаем статистику использования сети + utilization_result = self._make_api_request("SYNO.Core.System.Utilization", "get", version=1) + + interfaces = [] + if interface_result: + interfaces = interface_result.get("interfaces", []) + + network_stats = {} + if utilization_result and "network" in utilization_result: + network_stats = utilization_result.get("network", {}) + + # Объединяем данные + for interface in interfaces: + iface_id = interface.get("id", "") + if iface_id in network_stats: + interface["rx"] = network_stats[iface_id].get("rx", 0) + interface["tx"] = network_stats[iface_id].get("tx", 0) + + return { + "interfaces": interfaces, + "statistics": network_stats + } + + except Exception as e: + logger.error(f"Error getting network status: {str(e)}") + return {} + + def get_system_logs(self, limit: int = 20) -> List[Dict[str, Any]]: + """Получение журналов системы""" + logger.info(f"Getting system logs (limit={limit})") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for system logs request") + return [] + + try: + # Получаем журналы через API + result = self._make_api_request("SYNO.Core.SyslogClient.Log", "list", version=1, + params={"start": 0, "limit": limit, "sort": "time", "dir": "DESC"}) + + if not result: + # Пробуем альтернативный API + logger.info("Trying alternative API for system logs") + alt_result = self._make_api_request("SYNO.Core.Log", "list", version=1, + params={"start": 0, "limit": limit, "sort": "time", "dir": "DESC"}) + + if alt_result: + return alt_result.get("logs", []) + return [] + + return result.get("logs", []) + + except Exception as e: + logger.error(f"Error getting system logs: {str(e)}") + return [] + + def get_power_schedule(self) -> Dict[str, Any]: + """Получение расписания включения/выключения""" + logger.info("Getting power schedule") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for power schedule request") + return {} + + try: + # Получаем расписание через API + result = self._make_api_request("SYNO.Core.Hardware.PowerSchedule", "get", version=1) + + if not result: + return {} + + return result + + except Exception as e: + logger.error(f"Error getting power schedule: {str(e)}") + return {} + + def set_power_schedule(self, schedule_type: str, days: List[int], time: str, enabled: bool = True) -> bool: + """Настройка расписания включения/выключения + + Args: + schedule_type: Тип расписания ('boot' или 'shutdown') + days: Список дней недели (0-6, где 0 - понедельник) + time: Время в формате 'HH:MM' + enabled: Включить или выключить расписание + + Returns: + True если успешно, False в противном случае + """ + logger.info(f"Setting power schedule for {schedule_type} at {time} on days {days}") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for setting power schedule") + return False + + try: + # Получаем текущее расписание + current_schedule = self._make_api_request("SYNO.Core.Hardware.PowerSchedule", "get", version=1) + + if not current_schedule: + logger.error("Failed to get current power schedule") + return False + + # Подготавливаем новое расписание + params = { + "enabled": enabled, + "type": schedule_type, + "day": days, + "time": time + } + + # Устанавливаем новое расписание + result = self._make_api_request("SYNO.Core.Hardware.PowerSchedule", "set", version=1, params=params) + + if not result: + logger.error("Failed to set power schedule") + return False + + logger.info(f"Power schedule for {schedule_type} set successfully") + return True + + except Exception as e: + logger.error(f"Error setting power schedule: {str(e)}") + return False + + def get_temperature_status(self) -> Dict[str, Any]: + """Получение информации о температуре системы и дисков""" + logger.info("Getting temperature status") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for temperature status request") + return {} + + try: + # Получаем информацию о системе для общей температуры + system_info = self._make_api_request("SYNO.DSM.Info", "getinfo", version=2) + + # Получаем информацию о дисках для их температуры + storage_info = self._make_api_request("SYNO.Storage.CGI.Storage", "load_info", version=1) + + system_temp = None + disk_temps = [] + + if system_info: + system_temp = system_info.get("temperature") + + if storage_info: + disks = storage_info.get("disks", []) + for disk in disks: + name = disk.get("name", "unknown") + model = disk.get("model", "unknown") + temp = disk.get("temp", None) + if temp is not None: + disk_temps.append({ + "name": name, + "model": model, + "temperature": temp + }) + + return { + "system_temperature": system_temp, + "disk_temperatures": disk_temps, + "warning": system_info.get("temperature_warn", False) if system_info else False + } + + except Exception as e: + logger.error(f"Error getting temperature status: {str(e)}") + return {} + + def browse_files(self, folder_path: str = "", limit: int = 100) -> Dict[str, Any]: + """Просмотр файлов в указанной директории + + Args: + folder_path: Путь к папке (пустая строка для корневых общих папок) + limit: Максимальное количество элементов для возврата + + Returns: + Словарь с информацией о файлах и папках + """ + logger.info(f"Browsing files in {folder_path or 'root'}") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for file browsing") + return {"success": False, "error": "authentication_failed"} + + try: + if not folder_path: + # Если путь не указан, получаем список общих папок + result = self._make_api_request("SYNO.FileStation.List", "list_share", version=2) + + if not result: + logger.error("Failed to list shared folders") + return {"success": False, "error": "api_error"} + + return { + "success": True, + "items": result.get("shares", []), + "path": "", + "is_root": True + } + else: + # Получаем список файлов в указанной директории + params = { + "folder_path": folder_path, + "limit": limit, + "offset": 0, + "sort_by": "name", + "sort_direction": "ASC" + } + + result = self._make_api_request("SYNO.FileStation.List", "list", version=2, params=params) + + if not result: + logger.error(f"Failed to list files in {folder_path}") + return {"success": False, "error": "api_error"} + + return { + "success": True, + "items": result.get("files", []), + "path": folder_path, + "is_root": False, + "total": result.get("total", 0) + } + + except Exception as e: + logger.error(f"Error browsing files: {str(e)}") + return {"success": False, "error": str(e)} + + def manage_service(self, service_name: str, action: str = "status") -> Dict[str, Any]: + """Управление системным сервисом + + Args: + service_name: Имя сервиса + action: Действие (status/start/stop/restart) + + Returns: + Словарь с результатом операции + """ + logger.info(f"Managing service {service_name}, action: {action}") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for service management") + return {"success": False, "error": "authentication_failed"} + + try: + # Проверяем доступное API для управления сервисами + if action == "status": + result = self._make_api_request("SYNO.Core.Service", "get", version=1, + params={"service": service_name}) + else: + result = self._make_api_request("SYNO.Core.Service", action, version=1, + params={"service": service_name}) + + if not result: + logger.error(f"Failed to {action} service {service_name}") + return {"success": False, "error": "api_error"} + + return { + "success": True, + "service": service_name, + "action": action, + "result": result, + "status": result.get("status") if action == "status" else "completed" + } + + except Exception as e: + logger.error(f"Error managing service {service_name}: {str(e)}") + return {"success": False, "error": str(e)} + + def search_files(self, pattern: str, folder_path: str = "", limit: int = 100) -> Dict[str, Any]: + """Поиск файлов по шаблону + + Args: + pattern: Шаблон для поиска + folder_path: Путь к папке для поиска (пустая строка для всех общих папок) + limit: Максимальное количество результатов + + Returns: + Словарь с найденными файлами + """ + logger.info(f"Searching for {pattern} in {folder_path or 'all shared folders'}") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for file search") + return {"success": False, "error": "authentication_failed"} + + try: + if not folder_path: + # Получаем список всех общих папок для поиска + shares_result = self._make_api_request("SYNO.FileStation.List", "list_share", version=2) + + if not shares_result: + logger.error("Failed to list shared folders for search") + return {"success": False, "error": "api_error"} + + # Формируем список путей для поиска + folder_paths = [share.get("path") for share in shares_result.get("shares", [])] + else: + folder_paths = [folder_path] + + # Запускаем поиск + params = { + "folder_path": folder_paths, + "pattern": pattern, + "limit": limit, + "offset": 0 + } + + result = self._make_api_request("SYNO.FileStation.Search", "start", version=2, params=params) + + if not result: + logger.error(f"Failed to start search for {pattern}") + return {"success": False, "error": "api_error"} + + # Получаем taskid для проверки результатов + taskid = result.get("taskid") + if not taskid: + logger.error("No taskid received for search") + return {"success": False, "error": "no_task_id"} + + # Ожидаем завершения поиска + search_result = {"finished": False, "progress": 0} + for _ in range(10): # Максимум 10 попыток + search_status = self._make_api_request("SYNO.FileStation.Search", "status", version=2, + params={"taskid": taskid}) + + if not search_status: + break + + search_result["progress"] = search_status.get("progress", 0) + + if search_status.get("finished", False): + search_result["finished"] = True + break + + time.sleep(0.5) # Пауза между запросами + + # Получаем результаты поиска + if search_result["finished"]: + list_result = self._make_api_request("SYNO.FileStation.Search", "list", version=2, + params={"taskid": taskid, "limit": limit}) + + if list_result: + files = list_result.get("files", []) + return { + "success": True, + "pattern": pattern, + "results": files, + "total": list_result.get("total", len(files)) + } + + # Если не удалось получить результаты, останавливаем поиск + self._make_api_request("SYNO.FileStation.Search", "stop", version=2, + params={"taskid": taskid}) + + return { + "success": False, + "error": "search_timeout", + "progress": search_result["progress"] + } + + except Exception as e: + logger.error(f"Error searching files: {str(e)}") + return {"success": False, "error": str(e)} + + def get_backup_status(self) -> Dict[str, Any]: + """Получение информации о резервном копировании""" + logger.info("Getting backup status information") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for backup status request") + return {"success": False, "error": "authentication_failed"} + + try: + # Пробуем получить информацию о Hyper Backup + hyper_result = self._make_api_request("SYNO.Backup.Repository", "list", version=1) + + # Пробуем получить информацию о задачах Time Backup + time_result = self._make_api_request("SYNO.BackupService.Task", "list", version=1) + + # Проверяем статус резервного копирования USB + usb_result = self._make_api_request("SYNO.USBCopy", "status", version=1) + + backups = { + "hyper_backup": hyper_result.get("backups", []) if hyper_result else [], + "time_backup": time_result.get("tasks", []) if time_result else [], + "usb_copy": {"enabled": usb_result.get("enable", False) if usb_result else False} + } + + return { + "success": True, + "backups": backups, + "available_apis": { + "hyper_backup": hyper_result is not None, + "time_backup": time_result is not None, + "usb_copy": usb_result is not None + } + } + + except Exception as e: + logger.error(f"Error getting backup status: {str(e)}") + return {"success": False, "error": str(e)} + + def check_for_updates(self) -> Dict[str, Any]: + """Проверка наличия обновлений системы""" + logger.info("Checking for system updates") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for update check") + return {"success": False, "error": "authentication_failed"} + + try: + # Получаем текущую информацию о системе + system_info = self._make_api_request("SYNO.DSM.Info", "getinfo", version=2) + + if not system_info: + logger.error("Failed to get system info for update check") + return {"success": False, "error": "api_error"} + + # Проверяем наличие обновлений + update_result = self._make_api_request("SYNO.Core.Upgrade.Server", "check", version=1) + + # Получаем настройки автоматического обновления + settings_result = self._make_api_request("SYNO.Core.Upgrade.Server.Setting", "get", version=1) + + # Получаем информацию о доступных обновлениях + update_info = self._make_api_request("SYNO.Core.Upgrade.Server.Download", "list", version=1) + + current_version = system_info.get("version_string", "unknown") + auto_update_enabled = settings_result.get("auto_update", False) if settings_result else False + + updates = [] + if update_info and "updates" in update_info: + updates = update_info.get("updates", []) + + update_available = len(updates) > 0 + + return { + "success": True, + "current_version": current_version, + "update_available": update_available, + "auto_update_enabled": auto_update_enabled, + "updates": updates + } + + except Exception as e: + logger.error(f"Error checking for updates: {str(e)}") + return {"success": False, "error": str(e)} + + def get_quota_info(self) -> Dict[str, Any]: + """Получение информации о квотах пользователей""" + logger.info("Getting user quota information") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for quota info request") + return {"success": False, "error": "authentication_failed"} + + try: + # Получаем список пользователей + users_result = self._make_api_request("SYNO.Core.User", "list", version=1) + + if not users_result: + logger.error("Failed to get user list for quota info") + return {"success": False, "error": "api_error"} + + users = users_result.get("users", []) + user_quotas = [] + + # Получаем квоты для каждого пользователя + for user in users: + user_name = user.get("name") + if not user_name: + continue + + quota_result = self._make_api_request("SYNO.Core.Quota", "get", version=1, + params={"user_name": user_name}) + + if quota_result and "quotas" in quota_result: + user_quotas.append({ + "user": user_name, + "quotas": quota_result.get("quotas", []) + }) + + return { + "success": True, + "user_quotas": user_quotas + } + + except Exception as e: + logger.error(f"Error getting quota info: {str(e)}") + return {"success": False, "error": str(e)} diff --git a/.history/src/api/synology_20250830101843.py b/.history/src/api/synology_20250830101843.py new file mode 100644 index 0000000..6d5a673 --- /dev/null +++ b/.history/src/api/synology_20250830101843.py @@ -0,0 +1,1861 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Модуль для взаимодействия с API Synology NAS +""" + +import requests +from requests.adapters import HTTPAdapter +import json +import logging +import time +import urllib3 +from urllib3.util import Retry +from typing import Dict, Any, Optional, List +import socket +import struct +from time import sleep + +from src.config.config import ( + SYNOLOGY_HOST, + SYNOLOGY_PORT, + SYNOLOGY_USERNAME, + SYNOLOGY_PASSWORD, + SYNOLOGY_SECURE, + SYNOLOGY_TIMEOUT, + SYNOLOGY_MAC, + WOL_PORT, + SYNOLOGY_API_VERSION, + SYNOLOGY_POWER_API, + SYNOLOGY_INFO_API +) +from src.api.api_discovery import discover_available_apis, find_compatible_api + +# Отключение предупреждений о небезопасных SSL-соединениях +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + +logger = logging.getLogger(__name__) + +class SynologyAPI: + """Класс для взаимодействия с API Synology NAS""" + + def __init__(self): + """Инициализация класса SynologyAPI""" + logger.info("Creating API with auto-retry and connection pool") + logger.debug(f"Connection details: Host={SYNOLOGY_HOST}, Port={SYNOLOGY_PORT}, Secure={SYNOLOGY_SECURE}") + + self.protocol = "https" if SYNOLOGY_SECURE else "http" + self.base_url = f"{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi" + self.sid = None + self.session = requests.Session() + + # Настройка SSL + if self.protocol == "https": + logger.debug("SSL enabled, disabling certificate verification for internal network") + self.session.verify = False # Отключаем проверку SSL для внутренней сети + + # Добавляем пользовательские заголовки для улучшения совместимости с API + custom_headers = { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36', + 'Accept': 'application/json, text/javascript, */*; q=0.01', + 'Accept-Language': 'ru-RU,ru;q=0.9,en-US;q=0.8,en;q=0.7', + 'X-Requested-With': 'XMLHttpRequest', + 'Connection': 'keep-alive', + 'Referer': f'{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/' + } + self.session.headers.update(custom_headers) + logger.debug("Added browser-like headers for API compatibility") + + # Добавляем повторные попытки для HTTP-запросов + retry_strategy = Retry( + total=5, # Увеличиваем количество попыток + status_forcelist=[429, 500, 502, 503, 504, 404], + allowed_methods=["GET", "POST"], + backoff_factor=1.5, # Увеличиваем задержку между попытками + respect_retry_after_header=True + ) + adapter = HTTPAdapter( + max_retries=retry_strategy, + pool_connections=3, + pool_maxsize=10 + ) + self.session.mount("http://", adapter) + self.session.mount("https://", adapter) + + # Таймауты будут указаны в запросах + self.default_timeout = (SYNOLOGY_TIMEOUT, SYNOLOGY_TIMEOUT*2) + logger.debug(f"Setting default request timeout: {self.default_timeout}") + + # Кэш для хранения результатов запросов + self._cache = {} + self._cache_ttl = {} + self._last_online_check = 0 + self._last_online_status = False + self._online_check_interval = 30 # Интервал для кэширования проверок онлайн-статуса + + # Время последней успешной аутентификации и срок действия сессии + self._last_auth_time = 0 + self._auth_expiry = 3600 # По умолчанию 1 час + + # Информация о доступных API + self._available_apis = {} + self._api_info_ttl = 0 + + # Инициализируем API version resolver для автоматического определения совместимых API + self.api_resolver = None # Будет создан при необходимости + + def login(self) -> bool: + """Авторизация в API Synology NAS""" + # Сбрасываем SID для новой сессии + self.sid = None + + logger.info("Attempting to authenticate with Synology NAS...") + logger.debug(f"Base URL: {self.base_url}") + + # Проверяем доступность NAS перед запросом с помощью прямой TCP-проверки + # Избегаем вызова is_online(), чтобы не создавать рекурсию + online_status = self._check_tcp_connection() + if not online_status: + logger.error("Cannot login: Synology NAS is not reachable") + return False + + # Пробуем различные версии API для аутентификации + # Начинаем с версии 3, которая показала лучшую совместимость в тестах + auth_versions_to_try = [3, 2, 1, 6] # Пробуем сначала более стабильные версии + + for auth_version in auth_versions_to_try: + try: + # Определяем путь к API аутентификации + auth_path = "entry.cgi" # Используем entry.cgi вместо auth.cgi по умолчанию + + # Проверка информации API для определения доступных версий API + api_info_url = f"{self.base_url}/entry.cgi" + api_info_params = { + "api": "SYNO.API.Info", + "version": "1", + "method": "query", + "query": "SYNO.API.Auth" + } + + logger.debug(f"Querying API info for auth version {auth_version}") + try: + api_info_response = self.session.get( + api_info_url, + params=api_info_params, + timeout=self.default_timeout, + verify=False + ) + + if api_info_response.status_code == 200: + api_info_data = api_info_response.json() + if api_info_data.get("success"): + auth_info = api_info_data.get("data", {}).get("SYNO.API.Auth", {}) + max_version = auth_info.get("maxVersion", 6) + min_version = auth_info.get("minVersion", 1) + auth_path = auth_info.get("path", "entry.cgi") + logger.debug(f"Auth API versions: min={min_version}, max={max_version}, path={auth_path}") + + # Проверяем поддержку текущей версии + if auth_version < min_version or auth_version > max_version: + logger.warning(f"Auth API version {auth_version} not supported (min={min_version}, max={max_version}), trying next version") + continue + else: + logger.warning("Failed to query API info, using default auth path") + else: + logger.warning(f"API info request failed with status {api_info_response.status_code}, using default auth path") + except Exception as e: + logger.warning(f"Error querying API info: {str(e)}, using default auth path") + + # Основной запрос авторизации + url = f"{self.base_url}/{auth_path}" + params = { + "api": "SYNO.API.Auth", + "version": str(auth_version), + "method": "login", + "account": SYNOLOGY_USERNAME, + "passwd": SYNOLOGY_PASSWORD, + "session": "SynologyPowerControlBot", + "format": "cookie" + } + + # Для версии 6+ используем немного другой формат + if auth_version >= 6: + params["enable_syno_token"] = "yes" + + logger.debug(f"Sending auth request to {url} with API version {auth_version}") + start_time = time.time() + response = self.session.get( + url, + params=params, + timeout=self.default_timeout, + verify=False + ) + elapsed_time = time.time() - start_time + logger.debug(f"Auth request completed in {elapsed_time:.2f}s with status code {response.status_code}") + + # Проверка статуса HTTP + if response.status_code != 200: + logger.error(f"HTTP error: {response.status_code}") + logger.debug(f"Response content: {response.text[:200]}") + continue # Пробуем следующую версию + + try: + data = response.json() + except json.JSONDecodeError: + logger.error("Failed to decode JSON response") + logger.debug(f"Response content: {response.text[:200]}") + continue # Пробуем следующую версию + + if data.get("success"): + self.sid = data.get("data", {}).get("sid") + self._last_auth_time = time.time() + logger.info(f"Successfully logged in to Synology NAS using API version {auth_version}") + logger.debug(f"Session ID obtained: {self.sid[:5] if self.sid else 'None'}...") + + # Получаем и сохраняем токен SYNO, если он есть + syno_token = data.get("data", {}).get("synotoken") + if syno_token: + self.session.headers.update({'X-SYNO-TOKEN': syno_token}) + logger.debug("Added X-SYNO-TOKEN header for improved API compatibility") + + # Также добавляем SID в cookies для улучшения совместимости + self.session.cookies.update({ + 'id': self.sid, + 'sid': self.sid + }) + logger.debug("Added SID to session cookies for improved compatibility") + + # Проверка валидности полученной сессии с помощью простого запроса + # Будем использовать SYNO.API.Info без проверки сложных методов + + # Даем системе немного времени для инициализации сессии + time.sleep(0.5) + + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to log in with version {auth_version}: Error code {error_code}") + + # Если ошибка связана с версией API, пробуем следующую версию + if error_code in [104, 105]: + logger.warning(f"Auth version {auth_version} not supported, trying next version") + continue + + # Дополнительная диагностика + if error_code == 400: + logger.error("Authentication error: Invalid credentials") + elif error_code == 401: + logger.error("Authentication error: Account disabled") + elif error_code == 402: + logger.error("Authentication error: Permission denied") + elif error_code == 403: + logger.error("Authentication error: 2-factor authentication required") + elif error_code == 404: + logger.error("Authentication error: Failed to authenticate with 2-factor authentication") + + # Если ошибка связана с неверными учетными данными, нет смысла пробовать другие версии API + if error_code in [400, 401, 402, 403, 404]: + return False + + except requests.exceptions.Timeout: + logger.error(f"Connection timeout during auth with version {auth_version}") + continue # Пробуем следующую версию + except requests.exceptions.ConnectionError as e: + logger.error(f"Connection error during auth with version {auth_version}: {str(e)}") + continue # Пробуем следующую версию + except requests.RequestException as e: + logger.error(f"Request error during auth with version {auth_version}: {str(e)}") + continue # Пробуем следующую версию + except Exception as e: + logger.error(f"Unexpected error during login with version {auth_version}: {str(e)}", exc_info=True) + continue # Пробуем следующую версию + + # Если все версии не сработали + logger.error("Failed to authenticate with any API version") + return False + + def _validate_session(self) -> bool: + """Проверяет валидность сессии после авторизации""" + if not self.sid: + return False + + # Попробуем сделать простой запрос для проверки сессии + test_apis = [ + {"api": "SYNO.Core.System", "method": "info", "version": 1}, + {"api": "SYNO.DSM.Info", "method": "getinfo", "version": 1} + ] + + for test_api in test_apis: + try: + url = f"{self.base_url}/entry.cgi" + params = { + "api": test_api["api"], + "version": str(test_api["version"]), + "method": test_api["method"], + "_sid": self.sid + } + + response = self.session.get(url, params=params, timeout=self.default_timeout, verify=False) + + if response.status_code == 200: + data = response.json() + if data.get("success"): + logger.debug(f"Session validation successful using {test_api['api']}") + return True + else: + error_code = data.get("error", {}).get("code", -1) + if error_code != 119: # Не сессия истекла + logger.debug(f"Session validation with {test_api['api']} returned error code {error_code}") + return True # Считаем сессию валидной, если ошибка не связана с истечением сессии + except Exception as e: + logger.warning(f"Error during session validation with {test_api['api']}: {str(e)}") + + logger.warning("Session validation failed with all test APIs") + return False + + def logout(self) -> bool: + """Выход из API Synology NAS""" + if not self.sid: + return True + + try: + url = f"{self.base_url}/auth.cgi" + params = { + "api": "SYNO.API.Auth", + "version": "1", + "method": "logout", + "session": "SynologyPowerControlBot", + "_sid": self.sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + self.sid = None + logger.info("Successfully logged out from Synology NAS") + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to log out from Synology NAS: Error code {error_code}") + return False + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + return False + + def _make_api_request(self, api_name: str, method: str, version: int = 1, + params: Optional[Dict[str, Any]] = None, retry_auth: bool = True, retry_count: int = 0) -> Optional[Dict[str, Any]]: + """Обобщенный метод для выполнения API запросов с обработкой ошибок""" + # Ограничение на количество повторных попыток + if retry_count >= 3: + logger.error(f"Too many retries for {api_name}.{method}, giving up") + return None + + # Проверка наличия авторизации + if not self.sid and not self.login(): + logger.error(f"Not authenticated for API request: {api_name}.{method}") + return None + + # Проверка информации API для определения пути и поддерживаемой версии + api_info_url = f"{self.base_url}/entry.cgi" + api_info_params = { + "api": "SYNO.API.Info", + "version": "1", + "method": "query", + "query": api_name + } + + api_path = "entry.cgi" + try: + logger.debug(f"Querying API info for {api_name}") + api_info_response = self.session.get( + api_info_url, + params=api_info_params, + timeout=self.default_timeout, + verify=False + ) + + if api_info_response.status_code == 200: + api_info_data = api_info_response.json() + if api_info_data.get("success"): + api_info = api_info_data.get("data", {}).get(api_name, {}) + if api_info: + max_version = api_info.get("maxVersion", version) + min_version = api_info.get("minVersion", version) + api_path = api_info.get("path", "entry.cgi") + + # Проверка, поддерживается ли запрошенная версия + if version < min_version: + logger.warning(f"API version {version} for {api_name} is below minimum {min_version}, using {min_version}") + version = min_version + elif version > max_version: + logger.warning(f"API version {version} for {api_name} exceeds maximum {max_version}, using {max_version}") + version = max_version + + logger.debug(f"Using API path: {api_path}, version: {version}") + else: + logger.warning(f"API {api_name} not found in API info, using defaults") + except Exception as e: + logger.warning(f"Error querying API info for {api_name}: {str(e)}, using defaults") + + # Подготовка базовых параметров запроса + base_params = { + "api": api_name, + "version": str(version), + "method": method, + "_sid": self.sid # Используем _sid вместо sid для лучшей совместимости + } + + # Добавление дополнительных параметров, если они заданы + if params: + base_params.update(params) + + url = f"{self.base_url}/{api_path}" + logger.debug(f"API request: {api_name}.{method} v{version} to {url}") + logger.debug(f"Full request params: {base_params}") + + try: + start_time = time.time() + response = self.session.get( + url, + params=base_params, + timeout=self.default_timeout, + verify=False + ) + elapsed_time = time.time() - start_time + logger.debug(f"API request completed in {elapsed_time:.2f}s with status {response.status_code}") + + # Проверка статуса HTTP + if response.status_code != 200: + logger.error(f"HTTP error: {response.status_code} for {api_name}.{method}") + + # Повторная попытка при ошибках соединения + if response.status_code in [500, 502, 503, 504]: + logger.info(f"Server error, retrying request for {api_name}.{method}") + sleep(2) + return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1) + + return None + + try: + data = response.json() + except json.JSONDecodeError: + logger.error(f"Failed to decode JSON response for {api_name}.{method}") + logger.debug(f"Response content: {response.text[:200]}") + + # Повторная попытка при ошибках декодирования + logger.info(f"JSON decode error, retrying request for {api_name}.{method}") + sleep(2) + return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1) + + if data.get("success"): + logger.info(f"API request successful for {api_name}.{method}") + return data.get("data", {}) + else: + error_code = data.get("error", {}).get("code", -1) + error_desc = self._get_error_description(error_code) + logger.error(f"API error for {api_name}.{method}: {error_code} - {error_desc}") + + # Ошибки доступа или прав часто встречаются, но они не критичные + # Например, ошибка 102 означает, что нет прав, но NAS доступен + if error_code in [102, 103, 104, 105]: + logger.warning(f"Permission-related error for {api_name}.{method}: {error_code}") + # Возвращаем пустой словарь вместо None, + # чтобы вызывающий код мог понять, что запрос выполнен + return {} + + # Если ошибка связана с авторизацией и нам разрешено повторить попытку + if error_code in [106, 107, 119] and retry_auth: + logger.info(f"Session error (code {error_code}), creating fresh session...") + self.sid = None # Сбрасываем SID + + # Для ошибки 119 (Session timeout) дадим системе немного времени + if error_code == 119: + logger.info("Session timeout detected, waiting before retry...") + sleep(3) + + if self.login(): + logger.info("Re-authenticated with fresh session, retrying API request...") + # Рекурсивный вызов, но со счетчиком повторов + return self._make_api_request(api_name, method, version, params, False, retry_count + 1) + + # Для некоторых ошибок можно автоматически повторить запрос + if error_code in [408, 429, 500, 502, 503, 504]: + logger.info(f"Temporary error {error_code}, retrying request for {api_name}.{method}") + sleep(2) + return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1) + + return None + + except requests.exceptions.Timeout: + logger.error(f"Request timeout for {api_name}.{method}") + + # Повторная попытка при таймауте + if retry_count < 2: + logger.info(f"Timeout, retrying request for {api_name}.{method}") + sleep(2) + return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1) + + return None + except requests.exceptions.ConnectionError as e: + logger.error(f"Connection error for {api_name}.{method}: {str(e)}") + + # Повторная попытка при ошибке соединения + if retry_count < 2: + logger.info(f"Connection error, retrying request for {api_name}.{method}") + sleep(2) + return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1) + + return None + except Exception as e: + logger.error(f"Unexpected error during {api_name}.{method}: {str(e)}") + return None + + def get_system_status(self) -> Dict[str, Any]: + """Получение статуса системы""" + # Проверяем доступность системы + if not self.is_online(): + logger.info("Device is offline, skipping API request") + return {"status": "offline"} + + # Проверяем, есть ли кэшированный результат + cache_key = "system_status" + current_time = time.time() + if cache_key in self._cache and current_time - self._cache_ttl.get(cache_key, 0) < 60: + logger.debug("Using cached system status") + return self._cache[cache_key] + + # Используем рекомендованный API для получения информации о системе + logger.info(f"Getting system info using {SYNOLOGY_INFO_API} v{SYNOLOGY_API_VERSION}") + + # Для SYNO.DSM.Info метод getinfo, для других обычно используется get или info + if SYNOLOGY_INFO_API == "SYNO.DSM.Info": + method = "getinfo" + else: + method = "get" + + result = self._make_api_request(SYNOLOGY_INFO_API, method, version=SYNOLOGY_API_VERSION) + + if result: + logger.info(f"Successfully retrieved system info using {SYNOLOGY_INFO_API}") + + # Формируем расширенный ответ с дополнительной информацией + system_info = { + "status": "online", + "hostname": result.get("hostname", "unknown"), + "model": result.get("model", "unknown"), + "version": result.get("version", "unknown"), + "uptime": result.get("uptime", 0), + "time": current_time, + "is_online": True, + "api_used": SYNOLOGY_INFO_API + } + + # Сохраняем в кэше + self._cache[cache_key] = system_info + self._cache_ttl[cache_key] = current_time + + return system_info + + # Если основной API не сработал, пробуем резервные варианты + logger.warning(f"Failed to retrieve system info with {SYNOLOGY_INFO_API}, trying fallback APIs") + + # Пробуем резервные API + apis_to_try = [ + {"name": "SYNO.DSM.Info", "method": "getinfo", "version": 2}, + {"name": "SYNO.Core.System", "method": "info", "version": 1}, + {"name": "SYNO.Core.System.Status", "method": "get", "version": 1}, + {"name": "SYNO.Core.System.Info", "method": "get", "version": 1}, + ] + + for api in apis_to_try: + if api["name"] == SYNOLOGY_INFO_API: + continue # Пропускаем уже проверенный API + + logger.info(f"Trying to get system info using {api['name']}.{api['method']} v{api['version']}") + result = self._make_api_request(api["name"], api["method"], version=api["version"]) + + if result: + logger.info(f"Successfully retrieved system info using {api['name']}") + + # Формируем расширенный ответ с дополнительной информацией + system_info = { + "status": "online", + "hostname": result.get("hostname", "unknown"), + "model": result.get("model", "unknown"), + "version": result.get("version", "unknown"), + "uptime": result.get("uptime", 0), + "time": current_time, + "is_online": True, + "api_used": api["name"] + } + + # Сохраняем в кэше + self._cache[cache_key] = system_info + self._cache_ttl[cache_key] = current_time + + return system_info + + # Если все запросы не удались, но система онлайн, возвращаем базовую информацию + logger.warning("Failed to retrieve system info with all API methods") + return { + "status": "error", + "error": "Failed to fetch system information", + "is_online": True, + "time": current_time + } + + def shutdown_system(self) -> bool: + """Выключение системы""" + # Проверяем, включено ли устройство перед попыткой его выключить + if not self.is_online(force_check=True): + logger.info("Synology NAS is already offline, no need to shut down") + return True + + logger.info("Attempting to shutdown Synology NAS...") + + # Попробуем сначала использовать предпочтительный API для управления питанием + logger.info(f"Trying shutdown with {SYNOLOGY_POWER_API} v{SYNOLOGY_API_VERSION}") + + # Для SYNO.Core.Hardware.PowerRecovery используется метод setPowerOnState + # Для других API обычно используется метод shutdown или reboot + if SYNOLOGY_POWER_API == "SYNO.Core.Hardware.PowerRecovery": + # Для этого API нужны специальные параметры + params = {"state": "powerbtn"} # powerbtn имитирует нажатие кнопки питания + result = self._make_api_request(SYNOLOGY_POWER_API, "setPowerOnState", version=SYNOLOGY_API_VERSION, params=params) + else: + # Пробуем стандартный метод + result = self._make_api_request(SYNOLOGY_POWER_API, "shutdown", version=SYNOLOGY_API_VERSION) + + if result is not None: + logger.info(f"Successfully initiated system shutdown using {SYNOLOGY_POWER_API}") + + # Даем системе время начать процесс выключения + logger.info("Waiting for shutdown to initialize...") + sleep(5) + + # Проверяем статус + if not self.is_online(force_check=True): + logger.info("System is now offline. Shutdown confirmed successful.") + return True + else: + logger.info("System still appears to be online, but shutdown may be in progress.") + return True + else: + logger.warning(f"Failed to shutdown using {SYNOLOGY_POWER_API}, trying alternative methods") + + # Если не сработал основной метод, пробуем резервные варианты + # Проверка всех доступных методов API для выключения + apis_to_try = [ + {"name": "SYNO.Core.System", "method": "shutdown", "version": 3}, + {"name": "SYNO.Core.System.Power", "method": "shutdown", "version": 1}, + {"name": "SYNO.DSM.Power", "method": "shutdown", "version": 1}, + {"name": "SYNO.System.Power", "method": "shutdown", "version": 1}, + {"name": "SYNO.DSM.System", "method": "shutdown", "version": 1} + ] + + # Проверяем доступные API + try: + api_info_url = f"{self.base_url}/entry.cgi" + api_info_params = { + "api": "SYNO.API.Info", + "version": "1", + "method": "query", + "query": "SYNO.Core.System.Power,SYNO.DSM.Power,SYNO.Core.System,SYNO.System.Power,SYNO.DSM.System" + } + + logger.debug("Checking available shutdown APIs") + api_info_response = self.session.get( + api_info_url, + params=api_info_params, + timeout=self.default_timeout, + verify=False + ) + + if api_info_response.status_code == 200: + api_info_data = api_info_response.json() + if api_info_data.get("success"): + available_apis = api_info_data.get("data", {}) + logger.debug(f"Available APIs: {list(available_apis.keys())}") + + # Фильтруем только доступные API + filtered_apis = [] + for api in apis_to_try: + if api["name"] in available_apis: + api_info = available_apis[api["name"]] + # Проверка версии API + if api_info.get("minVersion", 1) <= api["version"] <= api_info.get("maxVersion", 1): + filtered_apis.append(api) + logger.debug(f"Adding {api['name']} to available shutdown APIs") + else: + logger.debug(f"API {api['name']} version {api['version']} not supported. Supported: {api_info.get('minVersion')}-{api_info.get('maxVersion')}") + + if filtered_apis: + apis_to_try = filtered_apis + else: + logger.warning("No compatible APIs found, trying all methods as fallback") + else: + logger.warning("Failed to query API info, using default methods") + else: + logger.warning(f"API info request failed with status {api_info_response.status_code}, using default methods") + except Exception as e: + logger.warning(f"Error querying API info: {str(e)}, using default methods") + + # Пробуем все доступные методы по порядку + for api in apis_to_try: + logger.info(f"Trying shutdown with {api['name']}.{api['method']} v{api['version']}") + result = self._make_api_request(api["name"], api["method"], version=api["version"]) + + if result is not None: + logger.info(f"Successfully initiated system shutdown using {api['name']}") + + # Даем системе время начать процесс выключения + logger.info("Waiting for shutdown to initialize...") + sleep(5) + + # Проверяем статус + if not self.is_online(force_check=True): + logger.info("System is now offline. Shutdown confirmed successful.") + return True + else: + logger.info("System still appears to be online, but shutdown may be in progress.") + return True + else: + logger.warning(f"Failed to shutdown using {api['name']}.{api['method']} v{api['version']}") + + # Если ни один метод не сработал, но система стала недоступна + if not self.is_online(force_check=True): + logger.info("System appears to be shutting down despite API errors") + return True + + logger.error("Failed to shutdown system after trying multiple APIs") + return False + + def reboot_system(self) -> bool: + """Перезагрузка системы""" + # Проверяем, включена ли система + if not self.is_online(force_check=True): + logger.error("Cannot reboot: System is offline") + return False + + logger.info("Attempting to reboot Synology NAS...") + + # Список API и методов для попытки перезагрузки + apis_to_try = [ + {"api": "SYNO.Core.System", "method": "reboot", "version": 1}, + {"api": "SYNO.DSM.System", "method": "reboot", "version": 1}, + {"api": "SYNO.Core.System.Power", "method": "restart", "version": 1}, + {"api": "SYNO.DSM.Power", "method": "reboot", "version": 1}, + {"api": "SYNO.Core.System", "method": "reboot", "version": 3}, + {"api": "SYNO.System.Power", "method": "reboot", "version": 1}, + {"api": "SYNO.Core.Hardware.NeedReboot", "method": "reboot", "version": 1} + ] + + # Добавляем настроенный в конфигурации API, если он отличается от уже добавленных + already_added = [item["api"] for item in apis_to_try] + if SYNOLOGY_POWER_API not in already_added: + for method in ["restart", "reboot"]: + apis_to_try.append({ + "api": SYNOLOGY_POWER_API, + "method": method, + "version": SYNOLOGY_API_VERSION + }) + + # Перебираем все возможные API и методы + for api_info in apis_to_try: + try: + logger.info(f"Trying reboot with {api_info['api']} API using method {api_info['method']}") + result = self._make_api_request(api_info['api'], api_info['method'], version=api_info['version']) + if result is not None: + logger.info(f"Successfully initiated system reboot using {api_info['api']} API") + + # Даем системе время начать процесс перезагрузки + logger.info("Waiting for reboot to initialize...") + sleep(5) + + # Ждем, пока система станет недоступна (признак перезагрузки) + reboot_started = False + for i in range(12): # Проверяем в течение 60 секунд (12 * 5сек) + if not self.is_online(force_check=True): + logger.info(f"System went offline after {i*5} seconds, reboot in progress") + reboot_started = True + break + logger.debug(f"System still online, waiting... ({i+1}/12)") + sleep(5) + + if reboot_started: + # Успешный вызов API и система ушла оффлайн - это признак успешной перезагрузки + return True + else: + # Успешный вызов API, но система не ушла оффлайн + logger.warning("System did not go offline after reboot command, but command was accepted") + # Даже если система не ушла оффлайн, команда могла быть принята + return True + except Exception as e: + logger.error(f"Error during reboot with {api_info['api']}: {str(e)}") + + # Если все попытки не удались, возвращаем False + logger.error("All reboot attempts failed") + return False + + def _get_error_description(self, error_code: int) -> str: + """Получение описания ошибки по коду""" + error_descriptions = { + 100: "Unknown error", + 101: "Invalid parameter", + 102: "API does not exist", + 103: "Method does not exist", + 104: "Version does not support", + 105: "Permission denied", + 106: "Session timeout", + 107: "Session interrupted by duplicate login", + 400: "Invalid credentials", + 401: "Account disabled", + 402: "Permission denied", + 403: "2FA required", + 404: "Failed to authenticate with 2FA" + } + return error_descriptions.get(error_code, "Unknown error code") + + def _check_tcp_connection(self) -> bool: + """Проверка базового TCP-соединения с Synology NAS""" + try: + socket_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + socket_obj.settimeout(SYNOLOGY_TIMEOUT) + result = socket_obj.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT)) + socket_obj.close() + + return result == 0 + except socket.error as e: + logger.error(f"Socket error during connection check: {str(e)}") + return False + except Exception as e: + logger.error(f"Unexpected error during connection check: {str(e)}") + return False + + def is_online(self, force_check=False) -> bool: + """Проверка онлайн-статуса Synology NAS""" + # Используем кэшированное значение, если доступно и не устарело + current_time = time.time() + if not force_check and (current_time - self._last_online_check) < self._online_check_interval: + logger.debug(f"Using cached online status: {self._last_online_status}") + return self._last_online_status + + logger.info("Checking if NAS is online...") + + # Проверяем TCP-соединение + online_status = self._check_tcp_connection() + logger.info(f"Detected Synology NAS online status: {online_status}") + + # Если TCP-соединение успешно и у нас есть действующий SID, + # попробуем более детальную проверку через API + if online_status and self.sid: + logger.info("Trying to fetch more detailed online status through API...") + + # Пробуем разные API для проверки онлайн-статуса + api_checks = [ + {"api": "SYNO.DSM.Info", "version": "2", "method": "getinfo"}, + {"api": "SYNO.Core.System", "version": "1", "method": "info"}, + {"api": "SYNO.Core.System.Status", "version": "1", "method": "get"} + ] + + api_success = False + for api_check in api_checks: + try: + url = f"{self.base_url}/entry.cgi" + params = { + "api": api_check["api"], + "version": api_check["version"], + "method": api_check["method"], + "sid": self.sid + } + + logger.debug(f"Trying online status check with {api_check['api']}") + response = self.session.get( + url, + params=params, + timeout=self.default_timeout, + verify=False + ) + + if response.status_code == 200: + data = response.json() + if data.get("success"): + logger.info(f"API request successful for {api_check['api']}") + logger.info("Synology NAS is online with API access") + api_success = True + break + else: + error_code = data.get("error", {}).get("code", -1) + logger.warning(f"API response indicates an error: {error_code}, but NAS is reachable") + else: + logger.warning(f"API returned status code {response.status_code}") + except Exception as e: + logger.warning(f"API check failed for {api_check['api']}, but TCP connection succeeded: {str(e)}") + + if not api_success: + logger.warning("All API checks failed, but TCP connection is successful") + + # Обновляем кэшированное значение + self._last_online_check = current_time + self._last_online_status = online_status + + return online_status + + def wake_on_lan(self) -> bool: + """Отправка Wake-on-LAN пакета для включения Synology NAS""" + if not SYNOLOGY_MAC: + logger.error("MAC address not configured") + return False + + try: + # Преобразование MAC-адреса в байты + mac_address = SYNOLOGY_MAC.replace(':', '').replace('-', '') + if len(mac_address) != 12: + logger.error(f"Invalid MAC address format: {SYNOLOGY_MAC}") + return False + + try: + mac_bytes = bytes.fromhex(mac_address) + except ValueError as e: + logger.error(f"Failed to parse MAC address: {str(e)}") + logger.error(f"MAC address must contain only hex characters: {SYNOLOGY_MAC}") + return False + + # Создание Magic Packet + magic_packet = b'\xff' * 6 + mac_bytes * 16 + + # Отправка пакета на конкретный адрес + try: + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) + sock.sendto(magic_packet, (SYNOLOGY_HOST, WOL_PORT)) + sock.close() + logger.info(f"Wake-on-LAN packet sent to {SYNOLOGY_MAC} at {SYNOLOGY_HOST}:{WOL_PORT}") + except Exception as e: + logger.error(f"Error sending directed WoL packet: {str(e)}") + return False + + # Для надежности отправляем также широковещательный пакет + try: + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) + + # Используем стандартный широковещательный адрес + broadcast_addr = "255.255.255.255" + sock.sendto(magic_packet, (broadcast_addr, WOL_PORT)) + sock.close() + logger.info(f"Wake-on-LAN broadcast packet sent to {broadcast_addr}:{WOL_PORT}") + except Exception as e: + logger.warning(f"Error sending broadcast WoL packet (non-critical): {str(e)}") + # Не считаем ошибкой, т.к. основной пакет уже отправлен + + return True + + except Exception as e: + logger.error(f"Unexpected error in wake_on_lan: {str(e)}") + return False + + def wait_for_boot(self, max_attempts: int = 30, delay: int = 10) -> bool: + """Ожидание загрузки Synology NAS""" + logger.info(f"Waiting for Synology NAS to boot (timeout: {max_attempts * delay}s)...") + + for attempt in range(max_attempts): + # Принудительно проверяем статус без использования кэша + if self.is_online(force_check=True): + logger.info(f"Synology NAS is online after {attempt + 1} attempts ({(attempt + 1) * delay}s)") + + # Проверяем, что не только сеть доступна, но и API загрузился + api_ready = False + logger.info("Waiting for API services to initialize...") + + for api_check in range(5): # Даем еще до 50 секунд для загрузки API + if self.sid or self.login(): + api_ready = True + logger.info(f"API services are ready after {api_check + 1} attempts") + break + logger.debug(f"API not ready yet, waiting... ({api_check + 1}/5)") + sleep(10) + + if not api_ready: + logger.warning("System is online but API services may not be fully initialized") + + # Дадим дополнительное время для полной загрузки всех сервисов + sleep(delay) + return True + + sleep(delay) + logger.info(f"Waiting for boot... Attempt {attempt + 1}/{max_attempts}") + + logger.error(f"Synology NAS didn't come online after {max_attempts * delay} seconds") + return False + + def power_on(self) -> bool: + """Включение Synology NAS""" + # Принудительная проверка статуса + if self.is_online(force_check=True): + logger.info("Synology NAS is already online") + return True + + logger.info("Powering on Synology NAS via Wake-on-LAN...") + + # Проверяем, настроен ли MAC-адрес + if not SYNOLOGY_MAC: + logger.error("Cannot power on: MAC address not configured in settings") + return False + + # Пробуем отправить несколько WoL пакетов для надежности + success = False + for attempt in range(3): + logger.debug(f"Sending WoL packet, attempt {attempt + 1}/3") + if self.wake_on_lan(): + success = True + break + sleep(1) + + if not success: + logger.error("Failed to send Wake-on-LAN packets") + return False + + # Ожидание загрузки + logger.info("WoL packets sent successfully, waiting for system to boot...") + boot_result = self.wait_for_boot(max_attempts=30, delay=10) + + if boot_result: + # Проверяем доступность API после загрузки + system_status = self.get_system_status() + if system_status.get("status") == "online": + logger.info("System booted successfully with API access") + return True + else: + logger.warning("System appears to be online but API may not be fully ready") + return True + else: + logger.error("System did not come online after WoL") + return False + + def power_off(self) -> bool: + """Выключение Synology NAS""" + if not self.is_online(force_check=True): + logger.info("Synology NAS is already offline") + return True + + logger.info("Powering off Synology NAS...") + + # Список API и методов для попытки выключения + apis_to_try = [ + {"api": "SYNO.Core.System", "method": "shutdown", "version": 1}, + {"api": "SYNO.DSM.System", "method": "shutdown", "version": 1}, + {"api": SYNOLOGY_POWER_API, "method": "shutdown", "version": SYNOLOGY_API_VERSION} + ] + + # Перебираем все возможные API и методы + for api_info in apis_to_try: + try: + logger.info(f"Trying shutdown with {api_info['api']} API using method {api_info['method']}") + api_result = self._make_api_request(api_info['api'], api_info['method'], version=api_info['version']) + if api_result is not None: + logger.info(f"Successfully initiated system shutdown using {api_info['api']} API") + + # Даем системе время начать процесс выключения + logger.info("Waiting for shutdown to initialize...") + sleep(5) + + return True + except Exception as e: + logger.error(f"Error during shutdown with {api_info['api']}: {str(e)}") + + # Если все попытки не удались, возвращаем False + logger.error("All shutdown attempts failed") + return False + + # Если все еще не сработало, используем оригинальный метод shutdown_system + if not result: + result = self.shutdown_system() + + if result: + # Дополнительная проверка, что система действительно выключилась + logger.info("Verifying system is offline...") + for attempt in range(12): # Проверяем в течение 2 минут (12 * 10 сек) + if not self.is_online(force_check=True): + logger.info(f"System confirmed offline after {attempt * 10} seconds") + return True + logger.debug(f"System still shutting down, checking again... ({attempt + 1}/12)") + sleep(10) + + logger.warning("System still appears to be online after shutdown command") + return False + else: + logger.error("Failed to initiate shutdown") + return False + + # Заглушки для расширенных методов + def get_shared_folders(self) -> List[Dict[str, Any]]: + """Получение списка общих папок""" + logger.info("Getting list of shared folders") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for shared folders request") + return [] + + try: + # Запрашиваем список общих папок через FileStation API + result = self._make_api_request("SYNO.FileStation.List", "list_share", version=2) + + if not result: + # Пробуем альтернативный API + logger.info("Trying alternative API for shared folders") + alt_result = self._make_api_request("SYNO.FileStation.List", "list_share", version=1) + if alt_result: + return alt_result.get("shares", []) + return [] + + return result.get("shares", []) + + except Exception as e: + logger.error(f"Error getting shared folders: {str(e)}") + return [] + + def get_system_load(self) -> Dict[str, Any]: + """Получение информации о загрузке системы""" + logger.info("Getting system load information") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for system load request") + return {} + + try: + # Запрашиваем информацию о загрузке системы + result = self._make_api_request("SYNO.Core.System.Utilization", "get", version=1) + + if not result: + # Пробуем альтернативный API + logger.info("Trying alternative API for system load") + alt_result = self._make_api_request("SYNO.Core.System", "info", version=3) + + if not alt_result: + return {} + + # Формируем из частичных данных + return { + "cpu_load": alt_result.get("cpu_usage", 0), + "memory": { + "total": alt_result.get("memory_size", 0), + "used": alt_result.get("memory_usage", 0), + "usage_percent": alt_result.get("memory_usage_percent", 0) + } + } + + # Формируем структурированный результат + return { + "cpu_load": result.get("cpu", {}).get("user", 0) + result.get("cpu", {}).get("system", 0), + "memory": result.get("memory", {}), + "network": result.get("network", {}) + } + + except Exception as e: + logger.error(f"Error getting system load: {str(e)}") + return {} + + def is_online_api(self) -> bool: + """Проверка онлайн-статуса Synology NAS с использованием API""" + if not self.is_online(): + return False + + # Проверяем доступность API через авторизацию + if not self.sid and not self.login(): + return False + + return True + + def get_storage_status(self) -> Dict[str, Any]: + """Получение подробной информации о хранилище""" + logger.info("Getting storage status information") + + # Проверяем доступность NAS и API + if not self.is_online_api(): + logger.error("Cannot get storage status: NAS is not online or API is not accessible") + return {"error": "authentication_failed"} + + try: + # Пробуем получить информацию о хранилище через SYNO.Storage.CGI.Storage API + result = self._make_api_request("SYNO.Storage.CGI.Storage", "load_info", version=1) + + if not result: + # Пробуем альтернативный API + logger.info("Trying alternative API for storage info") + alt_result = self._make_api_request("SYNO.Core.Storage", "info", version=1) + + if not alt_result: + # Пробуем еще один альтернативный API + logger.info("Trying SYNO.Core.System API for storage info") + sys_result = self._make_api_request("SYNO.Core.System", "info", version=3) + + if not sys_result: + return { + "volumes": [], + "disks": [], + "total_size": 0, + "total_used": 0, + "error": "no_data" + } + + # Извлекаем базовую информацию о хранилище из системной информации + return { + "volumes": [], + "disks": [], + "total_size": sys_result.get("hdd_total_size", 0) * 1024**2, # МБ в байты + "total_used": sys_result.get("hdd_total_size", 0) * sys_result.get("hdd_usage", 0) / 100 * 1024**2, + } + + # Обрабатываем данные из альтернативного API + volumes = alt_result.get("volumes", []) + disks = alt_result.get("disks", []) + + else: + # Обрабатываем данные из основного API + volumes = result.get("volumes", []) + disks = result.get("disks", []) + + # Рассчитываем общие размеры + total_size = 0 + total_used = 0 + + for volume in volumes: + volume_size = volume.get("size", {}).get("total", 0) + volume_used = volume.get("size", {}).get("used", 0) + + total_size += volume_size + total_used += volume_used + + return { + "volumes": volumes, + "disks": disks, + "total_size": total_size, + "total_used": total_used + } + + except Exception as e: + logger.error(f"Error in get_storage_status: {str(e)}") + return {"error": str(e)} + + def get_security_status(self) -> Dict[str, Any]: + """Получение информации о состоянии безопасности""" + logger.info("Getting security status information") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for security status request") + return {"success": False, "error": "authentication_failed"} + + try: + # Пробуем получить информацию о безопасности через API Security Scan + result = self._make_api_request("SYNO.Core.SecurityScan.Status", "get", version=1) + + if not result: + # Пробуем альтернативный API + logger.info("Trying alternative API for security status") + # Проверяем статус брандмауэра + firewall_result = self._make_api_request("SYNO.Core.Security.Firewall.Status", "get", version=1) + + # Проверяем статус автоматических обновлений + update_result = self._make_api_request("SYNO.Core.Upgrade.Server.Setting", "get", version=1) + + # Если ни один из API не отвечает + if not firewall_result and not update_result: + # Получаем общую информацию о системе для базовой проверки безопасности + sys_result = self._make_api_request("SYNO.DSM.Info", "getinfo", version=2) + + if not sys_result: + return { + "success": False, + "status": "unknown", + "last_check": None, + "is_secure": False, + "error": "no_security_api" + } + + # Собираем базовые сведения из системной информации + return { + "success": True, + "status": "basic", + "last_check": None, + "is_secure": True, # Предполагаем, что система в целом безопасна + "firewall_enabled": None, + "auto_update": None, + "version_latest": sys_result.get("version_string", "") + } + + # Собираем информацию из доступных результатов + firewall_enabled = firewall_result.get("enabled", False) if firewall_result else None + auto_update = update_result.get("auto_update", False) if update_result else None + + # Определяем, насколько система безопасна + is_secure = True # По умолчанию предполагаем, что система безопасна + if firewall_enabled is not None and not firewall_enabled: + is_secure = False + + return { + "success": True, + "status": "partial", + "last_check": None, + "is_secure": is_secure, + "firewall_enabled": firewall_enabled, + "auto_update": auto_update + } + + # Если основное API отвечает, возвращаем его данные + return { + "success": True, + "status": result.get("status", "unknown"), + "last_check": result.get("last_check", None), + "is_secure": result.get("is_secure", False), + "details": result.get("details", {}) + } + + except Exception as e: + logger.error(f"Error in get_security_status: {str(e)}") + return {"success": False, "error": str(e)} + + def get_processes(self, limit: int = 20) -> List[Dict[str, Any]]: + """Получение списка активных процессов""" + logger.info(f"Getting list of active processes (limit={limit})") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for processes request") + return [] + + try: + # Получаем список процессов через API + result = self._make_api_request("SYNO.Core.System.Process", "list", version=1, + params={"sort_by": "cpu", "order": "DESC", "limit": limit}) + + if not result: + logger.warning("Failed to get process list") + return [] + + return result.get("processes", []) + + except Exception as e: + logger.error(f"Error getting process list: {str(e)}") + return [] + + def get_network_status(self) -> Dict[str, Any]: + """Получение информации о сетевых подключениях""" + logger.info("Getting network status information") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for network status request") + return {} + + try: + # Получаем информацию о сетевых интерфейсах + interface_result = self._make_api_request("SYNO.Core.Network.Interface", "list", version=1) + + # Получаем статистику использования сети + utilization_result = self._make_api_request("SYNO.Core.System.Utilization", "get", version=1) + + interfaces = [] + if interface_result: + interfaces = interface_result.get("interfaces", []) + + network_stats = {} + if utilization_result and "network" in utilization_result: + network_stats = utilization_result.get("network", {}) + + # Объединяем данные + for interface in interfaces: + iface_id = interface.get("id", "") + if iface_id in network_stats: + interface["rx"] = network_stats[iface_id].get("rx", 0) + interface["tx"] = network_stats[iface_id].get("tx", 0) + + return { + "interfaces": interfaces, + "statistics": network_stats + } + + except Exception as e: + logger.error(f"Error getting network status: {str(e)}") + return {} + + def get_system_logs(self, limit: int = 20) -> List[Dict[str, Any]]: + """Получение журналов системы""" + logger.info(f"Getting system logs (limit={limit})") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for system logs request") + return [] + + try: + # Получаем журналы через API + result = self._make_api_request("SYNO.Core.SyslogClient.Log", "list", version=1, + params={"start": 0, "limit": limit, "sort": "time", "dir": "DESC"}) + + if not result: + # Пробуем альтернативный API + logger.info("Trying alternative API for system logs") + alt_result = self._make_api_request("SYNO.Core.Log", "list", version=1, + params={"start": 0, "limit": limit, "sort": "time", "dir": "DESC"}) + + if alt_result: + return alt_result.get("logs", []) + return [] + + return result.get("logs", []) + + except Exception as e: + logger.error(f"Error getting system logs: {str(e)}") + return [] + + def get_power_schedule(self) -> Dict[str, Any]: + """Получение расписания включения/выключения""" + logger.info("Getting power schedule") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for power schedule request") + return {} + + try: + # Получаем расписание через API + result = self._make_api_request("SYNO.Core.Hardware.PowerSchedule", "get", version=1) + + if not result: + return {} + + return result + + except Exception as e: + logger.error(f"Error getting power schedule: {str(e)}") + return {} + + def set_power_schedule(self, schedule_type: str, days: List[int], time: str, enabled: bool = True) -> bool: + """Настройка расписания включения/выключения + + Args: + schedule_type: Тип расписания ('boot' или 'shutdown') + days: Список дней недели (0-6, где 0 - понедельник) + time: Время в формате 'HH:MM' + enabled: Включить или выключить расписание + + Returns: + True если успешно, False в противном случае + """ + logger.info(f"Setting power schedule for {schedule_type} at {time} on days {days}") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for setting power schedule") + return False + + try: + # Получаем текущее расписание + current_schedule = self._make_api_request("SYNO.Core.Hardware.PowerSchedule", "get", version=1) + + if not current_schedule: + logger.error("Failed to get current power schedule") + return False + + # Подготавливаем новое расписание + params = { + "enabled": enabled, + "type": schedule_type, + "day": days, + "time": time + } + + # Устанавливаем новое расписание + result = self._make_api_request("SYNO.Core.Hardware.PowerSchedule", "set", version=1, params=params) + + if not result: + logger.error("Failed to set power schedule") + return False + + logger.info(f"Power schedule for {schedule_type} set successfully") + return True + + except Exception as e: + logger.error(f"Error setting power schedule: {str(e)}") + return False + + def get_temperature_status(self) -> Dict[str, Any]: + """Получение информации о температуре системы и дисков""" + logger.info("Getting temperature status") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for temperature status request") + return {} + + try: + # Получаем информацию о системе для общей температуры + system_info = self._make_api_request("SYNO.DSM.Info", "getinfo", version=2) + + # Получаем информацию о дисках для их температуры + storage_info = self._make_api_request("SYNO.Storage.CGI.Storage", "load_info", version=1) + + system_temp = None + disk_temps = [] + + if system_info: + system_temp = system_info.get("temperature") + + if storage_info: + disks = storage_info.get("disks", []) + for disk in disks: + name = disk.get("name", "unknown") + model = disk.get("model", "unknown") + temp = disk.get("temp", None) + if temp is not None: + disk_temps.append({ + "name": name, + "model": model, + "temperature": temp + }) + + return { + "system_temperature": system_temp, + "disk_temperatures": disk_temps, + "warning": system_info.get("temperature_warn", False) if system_info else False + } + + except Exception as e: + logger.error(f"Error getting temperature status: {str(e)}") + return {} + + def browse_files(self, folder_path: str = "", limit: int = 100) -> Dict[str, Any]: + """Просмотр файлов в указанной директории + + Args: + folder_path: Путь к папке (пустая строка для корневых общих папок) + limit: Максимальное количество элементов для возврата + + Returns: + Словарь с информацией о файлах и папках + """ + logger.info(f"Browsing files in {folder_path or 'root'}") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for file browsing") + return {"success": False, "error": "authentication_failed"} + + try: + if not folder_path: + # Если путь не указан, получаем список общих папок + result = self._make_api_request("SYNO.FileStation.List", "list_share", version=2) + + if not result: + logger.error("Failed to list shared folders") + return {"success": False, "error": "api_error"} + + return { + "success": True, + "items": result.get("shares", []), + "path": "", + "is_root": True + } + else: + # Получаем список файлов в указанной директории + params = { + "folder_path": folder_path, + "limit": limit, + "offset": 0, + "sort_by": "name", + "sort_direction": "ASC" + } + + result = self._make_api_request("SYNO.FileStation.List", "list", version=2, params=params) + + if not result: + logger.error(f"Failed to list files in {folder_path}") + return {"success": False, "error": "api_error"} + + return { + "success": True, + "items": result.get("files", []), + "path": folder_path, + "is_root": False, + "total": result.get("total", 0) + } + + except Exception as e: + logger.error(f"Error browsing files: {str(e)}") + return {"success": False, "error": str(e)} + + def manage_service(self, service_name: str, action: str = "status") -> Dict[str, Any]: + """Управление системным сервисом + + Args: + service_name: Имя сервиса + action: Действие (status/start/stop/restart) + + Returns: + Словарь с результатом операции + """ + logger.info(f"Managing service {service_name}, action: {action}") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for service management") + return {"success": False, "error": "authentication_failed"} + + try: + # Проверяем доступное API для управления сервисами + if action == "status": + result = self._make_api_request("SYNO.Core.Service", "get", version=1, + params={"service": service_name}) + else: + result = self._make_api_request("SYNO.Core.Service", action, version=1, + params={"service": service_name}) + + if not result: + logger.error(f"Failed to {action} service {service_name}") + return {"success": False, "error": "api_error"} + + return { + "success": True, + "service": service_name, + "action": action, + "result": result, + "status": result.get("status") if action == "status" else "completed" + } + + except Exception as e: + logger.error(f"Error managing service {service_name}: {str(e)}") + return {"success": False, "error": str(e)} + + def search_files(self, pattern: str, folder_path: str = "", limit: int = 100) -> Dict[str, Any]: + """Поиск файлов по шаблону + + Args: + pattern: Шаблон для поиска + folder_path: Путь к папке для поиска (пустая строка для всех общих папок) + limit: Максимальное количество результатов + + Returns: + Словарь с найденными файлами + """ + logger.info(f"Searching for {pattern} in {folder_path or 'all shared folders'}") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for file search") + return {"success": False, "error": "authentication_failed"} + + try: + if not folder_path: + # Получаем список всех общих папок для поиска + shares_result = self._make_api_request("SYNO.FileStation.List", "list_share", version=2) + + if not shares_result: + logger.error("Failed to list shared folders for search") + return {"success": False, "error": "api_error"} + + # Формируем список путей для поиска + folder_paths = [share.get("path") for share in shares_result.get("shares", [])] + else: + folder_paths = [folder_path] + + # Запускаем поиск + params = { + "folder_path": folder_paths, + "pattern": pattern, + "limit": limit, + "offset": 0 + } + + result = self._make_api_request("SYNO.FileStation.Search", "start", version=2, params=params) + + if not result: + logger.error(f"Failed to start search for {pattern}") + return {"success": False, "error": "api_error"} + + # Получаем taskid для проверки результатов + taskid = result.get("taskid") + if not taskid: + logger.error("No taskid received for search") + return {"success": False, "error": "no_task_id"} + + # Ожидаем завершения поиска + search_result = {"finished": False, "progress": 0} + for _ in range(10): # Максимум 10 попыток + search_status = self._make_api_request("SYNO.FileStation.Search", "status", version=2, + params={"taskid": taskid}) + + if not search_status: + break + + search_result["progress"] = search_status.get("progress", 0) + + if search_status.get("finished", False): + search_result["finished"] = True + break + + time.sleep(0.5) # Пауза между запросами + + # Получаем результаты поиска + if search_result["finished"]: + list_result = self._make_api_request("SYNO.FileStation.Search", "list", version=2, + params={"taskid": taskid, "limit": limit}) + + if list_result: + files = list_result.get("files", []) + return { + "success": True, + "pattern": pattern, + "results": files, + "total": list_result.get("total", len(files)) + } + + # Если не удалось получить результаты, останавливаем поиск + self._make_api_request("SYNO.FileStation.Search", "stop", version=2, + params={"taskid": taskid}) + + return { + "success": False, + "error": "search_timeout", + "progress": search_result["progress"] + } + + except Exception as e: + logger.error(f"Error searching files: {str(e)}") + return {"success": False, "error": str(e)} + + def get_backup_status(self) -> Dict[str, Any]: + """Получение информации о резервном копировании""" + logger.info("Getting backup status information") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for backup status request") + return {"success": False, "error": "authentication_failed"} + + try: + # Пробуем получить информацию о Hyper Backup + hyper_result = self._make_api_request("SYNO.Backup.Repository", "list", version=1) + + # Пробуем получить информацию о задачах Time Backup + time_result = self._make_api_request("SYNO.BackupService.Task", "list", version=1) + + # Проверяем статус резервного копирования USB + usb_result = self._make_api_request("SYNO.USBCopy", "status", version=1) + + backups = { + "hyper_backup": hyper_result.get("backups", []) if hyper_result else [], + "time_backup": time_result.get("tasks", []) if time_result else [], + "usb_copy": {"enabled": usb_result.get("enable", False) if usb_result else False} + } + + return { + "success": True, + "backups": backups, + "available_apis": { + "hyper_backup": hyper_result is not None, + "time_backup": time_result is not None, + "usb_copy": usb_result is not None + } + } + + except Exception as e: + logger.error(f"Error getting backup status: {str(e)}") + return {"success": False, "error": str(e)} + + def check_for_updates(self) -> Dict[str, Any]: + """Проверка наличия обновлений системы""" + logger.info("Checking for system updates") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for update check") + return {"success": False, "error": "authentication_failed"} + + try: + # Получаем текущую информацию о системе + system_info = self._make_api_request("SYNO.DSM.Info", "getinfo", version=2) + + if not system_info: + logger.error("Failed to get system info for update check") + return {"success": False, "error": "api_error"} + + # Проверяем наличие обновлений + update_result = self._make_api_request("SYNO.Core.Upgrade.Server", "check", version=1) + + # Получаем настройки автоматического обновления + settings_result = self._make_api_request("SYNO.Core.Upgrade.Server.Setting", "get", version=1) + + # Получаем информацию о доступных обновлениях + update_info = self._make_api_request("SYNO.Core.Upgrade.Server.Download", "list", version=1) + + current_version = system_info.get("version_string", "unknown") + auto_update_enabled = settings_result.get("auto_update", False) if settings_result else False + + updates = [] + if update_info and "updates" in update_info: + updates = update_info.get("updates", []) + + update_available = len(updates) > 0 + + return { + "success": True, + "current_version": current_version, + "update_available": update_available, + "auto_update_enabled": auto_update_enabled, + "updates": updates + } + + except Exception as e: + logger.error(f"Error checking for updates: {str(e)}") + return {"success": False, "error": str(e)} + + def get_quota_info(self) -> Dict[str, Any]: + """Получение информации о квотах пользователей""" + logger.info("Getting user quota information") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for quota info request") + return {"success": False, "error": "authentication_failed"} + + try: + # Получаем список пользователей + users_result = self._make_api_request("SYNO.Core.User", "list", version=1) + + if not users_result: + logger.error("Failed to get user list for quota info") + return {"success": False, "error": "api_error"} + + users = users_result.get("users", []) + user_quotas = [] + + # Получаем квоты для каждого пользователя + for user in users: + user_name = user.get("name") + if not user_name: + continue + + quota_result = self._make_api_request("SYNO.Core.Quota", "get", version=1, + params={"user_name": user_name}) + + if quota_result and "quotas" in quota_result: + user_quotas.append({ + "user": user_name, + "quotas": quota_result.get("quotas", []) + }) + + return { + "success": True, + "user_quotas": user_quotas + } + + except Exception as e: + logger.error(f"Error getting quota info: {str(e)}") + return {"success": False, "error": str(e)} diff --git a/.history/src/bot_20250830063649.py b/.history/src/bot_20250830063649.py new file mode 100644 index 0000000..51929ad --- /dev/null +++ b/.history/src/bot_20250830063649.py @@ -0,0 +1,57 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Главный модуль запуска телеграм-бота для управления Synology NAS +""" + +import os +import logging +from telegram.ext import ( + Application, + CommandHandler, + CallbackQueryHandler, + MessageHandler, + filters +) + +from src.config.config import TELEGRAM_TOKEN +from src.handlers.command_handlers import ( + start_command, + help_command, + status_command, + power_command, + power_callback +) +from src.utils.logger import setup_logging + +def main() -> None: + """Основная функция запуска бота""" + # Настройка логирования + setup_logging() + logger = logging.getLogger(__name__) + + # Проверка наличия токена + if not TELEGRAM_TOKEN: + logger.error("Telegram token is not set! Please configure TELEGRAM_TOKEN in .env file.") + return + + # Создание и настройка приложения бота + logger.info("Starting Synology Power Control Bot") + application = Application.builder().token(TELEGRAM_TOKEN).build() + + # Регистрация обработчиков команд + application.add_handler(CommandHandler("start", start_command)) + application.add_handler(CommandHandler("help", help_command)) + application.add_handler(CommandHandler("status", status_command)) + application.add_handler(CommandHandler("power", power_command)) + + # Регистрация обработчика callback-запросов + application.add_handler(CallbackQueryHandler(power_callback)) + + # Запуск бота + logger.info("Bot started. Press Ctrl+C to stop.") + application.run_polling(allowed_updates=Application.ALL_UPDATES) + +if __name__ == "__main__": + main() diff --git a/.history/src/bot_20250830063839.py b/.history/src/bot_20250830063839.py new file mode 100644 index 0000000..51929ad --- /dev/null +++ b/.history/src/bot_20250830063839.py @@ -0,0 +1,57 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Главный модуль запуска телеграм-бота для управления Synology NAS +""" + +import os +import logging +from telegram.ext import ( + Application, + CommandHandler, + CallbackQueryHandler, + MessageHandler, + filters +) + +from src.config.config import TELEGRAM_TOKEN +from src.handlers.command_handlers import ( + start_command, + help_command, + status_command, + power_command, + power_callback +) +from src.utils.logger import setup_logging + +def main() -> None: + """Основная функция запуска бота""" + # Настройка логирования + setup_logging() + logger = logging.getLogger(__name__) + + # Проверка наличия токена + if not TELEGRAM_TOKEN: + logger.error("Telegram token is not set! Please configure TELEGRAM_TOKEN in .env file.") + return + + # Создание и настройка приложения бота + logger.info("Starting Synology Power Control Bot") + application = Application.builder().token(TELEGRAM_TOKEN).build() + + # Регистрация обработчиков команд + application.add_handler(CommandHandler("start", start_command)) + application.add_handler(CommandHandler("help", help_command)) + application.add_handler(CommandHandler("status", status_command)) + application.add_handler(CommandHandler("power", power_command)) + + # Регистрация обработчика callback-запросов + application.add_handler(CallbackQueryHandler(power_callback)) + + # Запуск бота + logger.info("Bot started. Press Ctrl+C to stop.") + application.run_polling(allowed_updates=Application.ALL_UPDATES) + +if __name__ == "__main__": + main() diff --git a/.history/src/bot_20250830065301.py b/.history/src/bot_20250830065301.py new file mode 100644 index 0000000..1a3e7a8 --- /dev/null +++ b/.history/src/bot_20250830065301.py @@ -0,0 +1,64 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Главный модуль запуска телеграм-бота для управления Synology NAS +""" + +import os +import logging +from telegram.ext import ( + Application, + CommandHandler, + CallbackQueryHandler, + MessageHandler, + filters +) + +from src.config.config import TELEGRAM_TOKEN +from src.handlers.command_handlers import ( + start_command, + help_command, + status_command, + power_command, + power_callback +) +from src.handlers.extended_handlers import ( + storage_command, + shares_command, + system_command, + load_command, + security_command +) +from src.utils.logger import setup_logging + +def main() -> None: + """Основная функция запуска бота""" + # Настройка логирования + setup_logging() + logger = logging.getLogger(__name__) + + # Проверка наличия токена + if not TELEGRAM_TOKEN: + logger.error("Telegram token is not set! Please configure TELEGRAM_TOKEN in .env file.") + return + + # Создание и настройка приложения бота + logger.info("Starting Synology Power Control Bot") + application = Application.builder().token(TELEGRAM_TOKEN).build() + + # Регистрация обработчиков команд + application.add_handler(CommandHandler("start", start_command)) + application.add_handler(CommandHandler("help", help_command)) + application.add_handler(CommandHandler("status", status_command)) + application.add_handler(CommandHandler("power", power_command)) + + # Регистрация обработчика callback-запросов + application.add_handler(CallbackQueryHandler(power_callback)) + + # Запуск бота + logger.info("Bot started. Press Ctrl+C to stop.") + application.run_polling(allowed_updates=Application.ALL_UPDATES) + +if __name__ == "__main__": + main() diff --git a/.history/src/bot_20250830065311.py b/.history/src/bot_20250830065311.py new file mode 100644 index 0000000..2967c3c --- /dev/null +++ b/.history/src/bot_20250830065311.py @@ -0,0 +1,71 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Главный модуль запуска телеграм-бота для управления Synology NAS +""" + +import os +import logging +from telegram.ext import ( + Application, + CommandHandler, + CallbackQueryHandler, + MessageHandler, + filters +) + +from src.config.config import TELEGRAM_TOKEN +from src.handlers.command_handlers import ( + start_command, + help_command, + status_command, + power_command, + power_callback +) +from src.handlers.extended_handlers import ( + storage_command, + shares_command, + system_command, + load_command, + security_command +) +from src.utils.logger import setup_logging + +def main() -> None: + """Основная функция запуска бота""" + # Настройка логирования + setup_logging() + logger = logging.getLogger(__name__) + + # Проверка наличия токена + if not TELEGRAM_TOKEN: + logger.error("Telegram token is not set! Please configure TELEGRAM_TOKEN in .env file.") + return + + # Создание и настройка приложения бота + logger.info("Starting Synology Power Control Bot") + application = Application.builder().token(TELEGRAM_TOKEN).build() + + # Регистрация обработчиков команд + application.add_handler(CommandHandler("start", start_command)) + application.add_handler(CommandHandler("help", help_command)) + application.add_handler(CommandHandler("status", status_command)) + application.add_handler(CommandHandler("power", power_command)) + + # Регистрация расширенных обработчиков команд + application.add_handler(CommandHandler("storage", storage_command)) + application.add_handler(CommandHandler("shares", shares_command)) + application.add_handler(CommandHandler("system", system_command)) + application.add_handler(CommandHandler("load", load_command)) + application.add_handler(CommandHandler("security", security_command)) + + # Регистрация обработчика callback-запросов + application.add_handler(CallbackQueryHandler(power_callback)) + + # Запуск бота + logger.info("Bot started. Press Ctrl+C to stop.") + application.run_polling(allowed_updates=Application.ALL_UPDATES) + +if __name__ == "__main__": + main() diff --git a/.history/src/bot_20250830065454.py b/.history/src/bot_20250830065454.py new file mode 100644 index 0000000..2967c3c --- /dev/null +++ b/.history/src/bot_20250830065454.py @@ -0,0 +1,71 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Главный модуль запуска телеграм-бота для управления Synology NAS +""" + +import os +import logging +from telegram.ext import ( + Application, + CommandHandler, + CallbackQueryHandler, + MessageHandler, + filters +) + +from src.config.config import TELEGRAM_TOKEN +from src.handlers.command_handlers import ( + start_command, + help_command, + status_command, + power_command, + power_callback +) +from src.handlers.extended_handlers import ( + storage_command, + shares_command, + system_command, + load_command, + security_command +) +from src.utils.logger import setup_logging + +def main() -> None: + """Основная функция запуска бота""" + # Настройка логирования + setup_logging() + logger = logging.getLogger(__name__) + + # Проверка наличия токена + if not TELEGRAM_TOKEN: + logger.error("Telegram token is not set! Please configure TELEGRAM_TOKEN in .env file.") + return + + # Создание и настройка приложения бота + logger.info("Starting Synology Power Control Bot") + application = Application.builder().token(TELEGRAM_TOKEN).build() + + # Регистрация обработчиков команд + application.add_handler(CommandHandler("start", start_command)) + application.add_handler(CommandHandler("help", help_command)) + application.add_handler(CommandHandler("status", status_command)) + application.add_handler(CommandHandler("power", power_command)) + + # Регистрация расширенных обработчиков команд + application.add_handler(CommandHandler("storage", storage_command)) + application.add_handler(CommandHandler("shares", shares_command)) + application.add_handler(CommandHandler("system", system_command)) + application.add_handler(CommandHandler("load", load_command)) + application.add_handler(CommandHandler("security", security_command)) + + # Регистрация обработчика callback-запросов + application.add_handler(CallbackQueryHandler(power_callback)) + + # Запуск бота + logger.info("Bot started. Press Ctrl+C to stop.") + application.run_polling(allowed_updates=Application.ALL_UPDATES) + +if __name__ == "__main__": + main() diff --git a/.history/src/bot_20250830072835.py b/.history/src/bot_20250830072835.py new file mode 100644 index 0000000..b336351 --- /dev/null +++ b/.history/src/bot_20250830072835.py @@ -0,0 +1,71 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Главный модуль запуска телеграм-бота для управления Synology NAS +""" + +import os +import logging +from telegram.ext import ( + Application, + CommandHandler, + CallbackQueryHandler, + MessageHandler, + filters +) + +from src.config.config import TELEGRAM_TOKEN +from src.handlers.command_handlers import ( + start_command, + help_command, + status_command, + power_command, + power_callback +) +from src.handlers.extended_handlers import ( + storage_command, + shares_command, + system_command, + load_command, + security_command +) +from src.utils.logger import setup_logging + +def main() -> None: + """Основная функция запуска бота""" + # Настройка логирования + setup_logging() + logger = logging.getLogger(__name__) + + # Проверка наличия токена + if not TELEGRAM_TOKEN: + logger.error("Telegram token is not set! Please configure TELEGRAM_TOKEN in .env file.") + return + + # Создание и настройка приложения бота + logger.info("Starting Synology Power Control Bot") + application = Application.builder().token(TELEGRAM_TOKEN).build() + + # Регистрация обработчиков команд + application.add_handler(CommandHandler("start", start_command)) + application.add_handler(CommandHandler("help", help_command)) + application.add_handler(CommandHandler("status", status_command)) + application.add_handler(CommandHandler("power", power_command)) + + # Регистрация расширенных обработчиков команд + application.add_handler(CommandHandler("storage", storage_command)) + application.add_handler(CommandHandler("shares", shares_command)) + application.add_handler(CommandHandler("system", system_command)) + application.add_handler(CommandHandler("load", load_command)) + application.add_handler(CommandHandler("security", security_command)) + + # Регистрация обработчика callback-запросов + application.add_handler(CallbackQueryHandler(power_callback)) + + # Запуск бота + logger.info("Bot started. Press Ctrl+C to stop.") + application.run_polling() + +if __name__ == "__main__": + main() diff --git a/.history/src/bot_20250830072844.py b/.history/src/bot_20250830072844.py new file mode 100644 index 0000000..b336351 --- /dev/null +++ b/.history/src/bot_20250830072844.py @@ -0,0 +1,71 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Главный модуль запуска телеграм-бота для управления Synology NAS +""" + +import os +import logging +from telegram.ext import ( + Application, + CommandHandler, + CallbackQueryHandler, + MessageHandler, + filters +) + +from src.config.config import TELEGRAM_TOKEN +from src.handlers.command_handlers import ( + start_command, + help_command, + status_command, + power_command, + power_callback +) +from src.handlers.extended_handlers import ( + storage_command, + shares_command, + system_command, + load_command, + security_command +) +from src.utils.logger import setup_logging + +def main() -> None: + """Основная функция запуска бота""" + # Настройка логирования + setup_logging() + logger = logging.getLogger(__name__) + + # Проверка наличия токена + if not TELEGRAM_TOKEN: + logger.error("Telegram token is not set! Please configure TELEGRAM_TOKEN in .env file.") + return + + # Создание и настройка приложения бота + logger.info("Starting Synology Power Control Bot") + application = Application.builder().token(TELEGRAM_TOKEN).build() + + # Регистрация обработчиков команд + application.add_handler(CommandHandler("start", start_command)) + application.add_handler(CommandHandler("help", help_command)) + application.add_handler(CommandHandler("status", status_command)) + application.add_handler(CommandHandler("power", power_command)) + + # Регистрация расширенных обработчиков команд + application.add_handler(CommandHandler("storage", storage_command)) + application.add_handler(CommandHandler("shares", shares_command)) + application.add_handler(CommandHandler("system", system_command)) + application.add_handler(CommandHandler("load", load_command)) + application.add_handler(CommandHandler("security", security_command)) + + # Регистрация обработчика callback-запросов + application.add_handler(CallbackQueryHandler(power_callback)) + + # Запуск бота + logger.info("Bot started. Press Ctrl+C to stop.") + application.run_polling() + +if __name__ == "__main__": + main() diff --git a/.history/src/bot_20250830075657.py b/.history/src/bot_20250830075657.py new file mode 100644 index 0000000..cbf8422 --- /dev/null +++ b/.history/src/bot_20250830075657.py @@ -0,0 +1,74 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Главный модуль запуска телеграм-бота для управления Synology NAS +""" + +import os +import signal +import sys +import asyncio +import logging +from telegram.ext import ( + Application, + CommandHandler, + CallbackQueryHandler, + MessageHandler, + filters +) + +from src.config.config import TELEGRAM_TOKEN +from src.handlers.command_handlers import ( + start_command, + help_command, + status_command, + power_command, + power_callback +) +from src.handlers.extended_handlers import ( + storage_command, + shares_command, + system_command, + load_command, + security_command +) +from src.utils.logger import setup_logging + +def main() -> None: + """Основная функция запуска бота""" + # Настройка логирования + setup_logging() + logger = logging.getLogger(__name__) + + # Проверка наличия токена + if not TELEGRAM_TOKEN: + logger.error("Telegram token is not set! Please configure TELEGRAM_TOKEN in .env file.") + return + + # Создание и настройка приложения бота + logger.info("Starting Synology Power Control Bot") + application = Application.builder().token(TELEGRAM_TOKEN).build() + + # Регистрация обработчиков команд + application.add_handler(CommandHandler("start", start_command)) + application.add_handler(CommandHandler("help", help_command)) + application.add_handler(CommandHandler("status", status_command)) + application.add_handler(CommandHandler("power", power_command)) + + # Регистрация расширенных обработчиков команд + application.add_handler(CommandHandler("storage", storage_command)) + application.add_handler(CommandHandler("shares", shares_command)) + application.add_handler(CommandHandler("system", system_command)) + application.add_handler(CommandHandler("load", load_command)) + application.add_handler(CommandHandler("security", security_command)) + + # Регистрация обработчика callback-запросов + application.add_handler(CallbackQueryHandler(power_callback)) + + # Запуск бота + logger.info("Bot started. Press Ctrl+C to stop.") + application.run_polling() + +if __name__ == "__main__": + main() diff --git a/.history/src/bot_20250830075723.py b/.history/src/bot_20250830075723.py new file mode 100644 index 0000000..f272445 --- /dev/null +++ b/.history/src/bot_20250830075723.py @@ -0,0 +1,102 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Главный модуль запуска телеграм-бота для управления Synology NAS +""" + +import os +import signal +import sys +import asyncio +import logging +from telegram.ext import ( + Application, + CommandHandler, + CallbackQueryHandler, + MessageHandler, + filters +) + +from src.config.config import TELEGRAM_TOKEN +from src.handlers.command_handlers import ( + start_command, + help_command, + status_command, + power_command, + power_callback +) +from src.handlers.extended_handlers import ( + storage_command, + shares_command, + system_command, + load_command, + security_command +) +from src.utils.logger import setup_logging + +async def shutdown(application: Application) -> None: + """Корректное завершение работы бота""" + logger = logging.getLogger(__name__) + logger.info("Stopping Synology Power Control Bot...") + + # Останавливаем прием обновлений + await application.stop() + logger.info("Bot stopped successfully") + +def signal_handler(sig, frame, application: Application = None): + """Обработчик сигналов для корректного завершения""" + logger = logging.getLogger(__name__) + logger.info(f"Received signal {sig}, shutting down gracefully") + + if application: + # Создаем и запускаем задачу завершения в event loop + loop = asyncio.get_event_loop() + if loop.is_running(): + loop.create_task(shutdown(application)) + else: + loop.run_until_complete(shutdown(application)) + + sys.exit(0) + +def main() -> None: + """Основная функция запуска бота""" + # Настройка логирования + setup_logging() + logger = logging.getLogger(__name__) + + # Проверка наличия токена + if not TELEGRAM_TOKEN: + logger.error("Telegram token is not set! Please configure TELEGRAM_TOKEN in .env file.") + return + + # Создание и настройка приложения бота + logger.info("Starting Synology Power Control Bot") + application = Application.builder().token(TELEGRAM_TOKEN).build() + + # Регистрация обработчиков команд + application.add_handler(CommandHandler("start", start_command)) + application.add_handler(CommandHandler("help", help_command)) + application.add_handler(CommandHandler("status", status_command)) + application.add_handler(CommandHandler("power", power_command)) + + # Регистрация расширенных обработчиков команд + application.add_handler(CommandHandler("storage", storage_command)) + application.add_handler(CommandHandler("shares", shares_command)) + application.add_handler(CommandHandler("system", system_command)) + application.add_handler(CommandHandler("load", load_command)) + application.add_handler(CommandHandler("security", security_command)) + + # Регистрация обработчика callback-запросов + application.add_handler(CallbackQueryHandler(power_callback)) + + # Настройка обработчиков сигналов для корректного завершения + signal.signal(signal.SIGINT, lambda sig, frame: signal_handler(sig, frame, application)) + signal.signal(signal.SIGTERM, lambda sig, frame: signal_handler(sig, frame, application)) + + # Запуск бота + logger.info("Bot started. Press Ctrl+C to stop.") + application.run_polling(allowed_updates=["message", "callback_query"]) + +if __name__ == "__main__": + main() diff --git a/.history/src/bot_20250830075740.py b/.history/src/bot_20250830075740.py new file mode 100644 index 0000000..7620586 --- /dev/null +++ b/.history/src/bot_20250830075740.py @@ -0,0 +1,102 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Главный модуль запуска телеграм-бота для управления Synology NAS +""" + +import os +import signal +import sys +import asyncio +import logging +from telegram.ext import ( + Application, + CommandHandler, + CallbackQueryHandler, + MessageHandler, + filters +) + +from src.config.config import TELEGRAM_TOKEN +from src.handlers.command_handlers import ( + start_command, + help_command, + status_command, + power_command, + power_callback +) +from src.handlers.extended_handlers import ( + storage_command, + shares_command, + system_command, + load_command, + security_command +) +from src.utils.logger import setup_logging + +async def shutdown(application: Application) -> None: + """Корректное завершение работы бота""" + logger = logging.getLogger(__name__) + logger.info("Stopping Synology Power Control Bot...") + + # Останавливаем прием обновлений + await application.stop() + logger.info("Bot stopped successfully") + +def signal_handler(sig, frame, application=None): + """Обработчик сигналов для корректного завершения""" + logger = logging.getLogger(__name__) + logger.info(f"Received signal {sig}, shutting down gracefully") + + if application: + # Создаем и запускаем задачу завершения в event loop + loop = asyncio.get_event_loop() + if loop.is_running(): + loop.create_task(shutdown(application)) + else: + loop.run_until_complete(shutdown(application)) + + sys.exit(0) + +def main() -> None: + """Основная функция запуска бота""" + # Настройка логирования + setup_logging() + logger = logging.getLogger(__name__) + + # Проверка наличия токена + if not TELEGRAM_TOKEN: + logger.error("Telegram token is not set! Please configure TELEGRAM_TOKEN in .env file.") + return + + # Создание и настройка приложения бота + logger.info("Starting Synology Power Control Bot") + application = Application.builder().token(TELEGRAM_TOKEN).build() + + # Регистрация обработчиков команд + application.add_handler(CommandHandler("start", start_command)) + application.add_handler(CommandHandler("help", help_command)) + application.add_handler(CommandHandler("status", status_command)) + application.add_handler(CommandHandler("power", power_command)) + + # Регистрация расширенных обработчиков команд + application.add_handler(CommandHandler("storage", storage_command)) + application.add_handler(CommandHandler("shares", shares_command)) + application.add_handler(CommandHandler("system", system_command)) + application.add_handler(CommandHandler("load", load_command)) + application.add_handler(CommandHandler("security", security_command)) + + # Регистрация обработчика callback-запросов + application.add_handler(CallbackQueryHandler(power_callback)) + + # Настройка обработчиков сигналов для корректного завершения + signal.signal(signal.SIGINT, lambda sig, frame: signal_handler(sig, frame, application)) + signal.signal(signal.SIGTERM, lambda sig, frame: signal_handler(sig, frame, application)) + + # Запуск бота + logger.info("Bot started. Press Ctrl+C to stop.") + application.run_polling(allowed_updates=["message", "callback_query"]) + +if __name__ == "__main__": + main() diff --git a/.history/src/bot_20250830075757.py b/.history/src/bot_20250830075757.py new file mode 100644 index 0000000..7620586 --- /dev/null +++ b/.history/src/bot_20250830075757.py @@ -0,0 +1,102 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Главный модуль запуска телеграм-бота для управления Synology NAS +""" + +import os +import signal +import sys +import asyncio +import logging +from telegram.ext import ( + Application, + CommandHandler, + CallbackQueryHandler, + MessageHandler, + filters +) + +from src.config.config import TELEGRAM_TOKEN +from src.handlers.command_handlers import ( + start_command, + help_command, + status_command, + power_command, + power_callback +) +from src.handlers.extended_handlers import ( + storage_command, + shares_command, + system_command, + load_command, + security_command +) +from src.utils.logger import setup_logging + +async def shutdown(application: Application) -> None: + """Корректное завершение работы бота""" + logger = logging.getLogger(__name__) + logger.info("Stopping Synology Power Control Bot...") + + # Останавливаем прием обновлений + await application.stop() + logger.info("Bot stopped successfully") + +def signal_handler(sig, frame, application=None): + """Обработчик сигналов для корректного завершения""" + logger = logging.getLogger(__name__) + logger.info(f"Received signal {sig}, shutting down gracefully") + + if application: + # Создаем и запускаем задачу завершения в event loop + loop = asyncio.get_event_loop() + if loop.is_running(): + loop.create_task(shutdown(application)) + else: + loop.run_until_complete(shutdown(application)) + + sys.exit(0) + +def main() -> None: + """Основная функция запуска бота""" + # Настройка логирования + setup_logging() + logger = logging.getLogger(__name__) + + # Проверка наличия токена + if not TELEGRAM_TOKEN: + logger.error("Telegram token is not set! Please configure TELEGRAM_TOKEN in .env file.") + return + + # Создание и настройка приложения бота + logger.info("Starting Synology Power Control Bot") + application = Application.builder().token(TELEGRAM_TOKEN).build() + + # Регистрация обработчиков команд + application.add_handler(CommandHandler("start", start_command)) + application.add_handler(CommandHandler("help", help_command)) + application.add_handler(CommandHandler("status", status_command)) + application.add_handler(CommandHandler("power", power_command)) + + # Регистрация расширенных обработчиков команд + application.add_handler(CommandHandler("storage", storage_command)) + application.add_handler(CommandHandler("shares", shares_command)) + application.add_handler(CommandHandler("system", system_command)) + application.add_handler(CommandHandler("load", load_command)) + application.add_handler(CommandHandler("security", security_command)) + + # Регистрация обработчика callback-запросов + application.add_handler(CallbackQueryHandler(power_callback)) + + # Настройка обработчиков сигналов для корректного завершения + signal.signal(signal.SIGINT, lambda sig, frame: signal_handler(sig, frame, application)) + signal.signal(signal.SIGTERM, lambda sig, frame: signal_handler(sig, frame, application)) + + # Запуск бота + logger.info("Bot started. Press Ctrl+C to stop.") + application.run_polling(allowed_updates=["message", "callback_query"]) + +if __name__ == "__main__": + main() diff --git a/.history/src/bot_20250830083325.py b/.history/src/bot_20250830083325.py new file mode 100644 index 0000000..d875d34 --- /dev/null +++ b/.history/src/bot_20250830083325.py @@ -0,0 +1,103 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Главный модуль запуска телеграм-бота для управления Synology NAS +""" + +import os +import signal +import sys +import asyncio +import logging +from telegram.ext import ( + Application, + CommandHandler, + CallbackQueryHandler, + MessageHandler, + filters +) + +from src.config.config import TELEGRAM_TOKEN +from src.handlers.command_handlers import ( + start_command, + help_command, + status_command, + power_command, + power_callback +) +from src.handlers.extended_handlers import ( + storage_command, + shares_command, + system_command, + load_command, + security_command, + check_api_command +) +from src.utils.logger import setup_logging + +async def shutdown(application: Application) -> None: + """Корректное завершение работы бота""" + logger = logging.getLogger(__name__) + logger.info("Stopping Synology Power Control Bot...") + + # Останавливаем прием обновлений + await application.stop() + logger.info("Bot stopped successfully") + +def signal_handler(sig, frame, application=None): + """Обработчик сигналов для корректного завершения""" + logger = logging.getLogger(__name__) + logger.info(f"Received signal {sig}, shutting down gracefully") + + if application: + # Создаем и запускаем задачу завершения в event loop + loop = asyncio.get_event_loop() + if loop.is_running(): + loop.create_task(shutdown(application)) + else: + loop.run_until_complete(shutdown(application)) + + sys.exit(0) + +def main() -> None: + """Основная функция запуска бота""" + # Настройка логирования + setup_logging() + logger = logging.getLogger(__name__) + + # Проверка наличия токена + if not TELEGRAM_TOKEN: + logger.error("Telegram token is not set! Please configure TELEGRAM_TOKEN in .env file.") + return + + # Создание и настройка приложения бота + logger.info("Starting Synology Power Control Bot") + application = Application.builder().token(TELEGRAM_TOKEN).build() + + # Регистрация обработчиков команд + application.add_handler(CommandHandler("start", start_command)) + application.add_handler(CommandHandler("help", help_command)) + application.add_handler(CommandHandler("status", status_command)) + application.add_handler(CommandHandler("power", power_command)) + + # Регистрация расширенных обработчиков команд + application.add_handler(CommandHandler("storage", storage_command)) + application.add_handler(CommandHandler("shares", shares_command)) + application.add_handler(CommandHandler("system", system_command)) + application.add_handler(CommandHandler("load", load_command)) + application.add_handler(CommandHandler("security", security_command)) + + # Регистрация обработчика callback-запросов + application.add_handler(CallbackQueryHandler(power_callback)) + + # Настройка обработчиков сигналов для корректного завершения + signal.signal(signal.SIGINT, lambda sig, frame: signal_handler(sig, frame, application)) + signal.signal(signal.SIGTERM, lambda sig, frame: signal_handler(sig, frame, application)) + + # Запуск бота + logger.info("Bot started. Press Ctrl+C to stop.") + application.run_polling(allowed_updates=["message", "callback_query"]) + +if __name__ == "__main__": + main() diff --git a/.history/src/bot_20250830083341.py b/.history/src/bot_20250830083341.py new file mode 100644 index 0000000..031bb35 --- /dev/null +++ b/.history/src/bot_20250830083341.py @@ -0,0 +1,104 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Главный модуль запуска телеграм-бота для управления Synology NAS +""" + +import os +import signal +import sys +import asyncio +import logging +from telegram.ext import ( + Application, + CommandHandler, + CallbackQueryHandler, + MessageHandler, + filters +) + +from src.config.config import TELEGRAM_TOKEN +from src.handlers.command_handlers import ( + start_command, + help_command, + status_command, + power_command, + power_callback +) +from src.handlers.extended_handlers import ( + storage_command, + shares_command, + system_command, + load_command, + security_command, + check_api_command +) +from src.utils.logger import setup_logging + +async def shutdown(application: Application) -> None: + """Корректное завершение работы бота""" + logger = logging.getLogger(__name__) + logger.info("Stopping Synology Power Control Bot...") + + # Останавливаем прием обновлений + await application.stop() + logger.info("Bot stopped successfully") + +def signal_handler(sig, frame, application=None): + """Обработчик сигналов для корректного завершения""" + logger = logging.getLogger(__name__) + logger.info(f"Received signal {sig}, shutting down gracefully") + + if application: + # Создаем и запускаем задачу завершения в event loop + loop = asyncio.get_event_loop() + if loop.is_running(): + loop.create_task(shutdown(application)) + else: + loop.run_until_complete(shutdown(application)) + + sys.exit(0) + +def main() -> None: + """Основная функция запуска бота""" + # Настройка логирования + setup_logging() + logger = logging.getLogger(__name__) + + # Проверка наличия токена + if not TELEGRAM_TOKEN: + logger.error("Telegram token is not set! Please configure TELEGRAM_TOKEN in .env file.") + return + + # Создание и настройка приложения бота + logger.info("Starting Synology Power Control Bot") + application = Application.builder().token(TELEGRAM_TOKEN).build() + + # Регистрация обработчиков команд + application.add_handler(CommandHandler("start", start_command)) + application.add_handler(CommandHandler("help", help_command)) + application.add_handler(CommandHandler("status", status_command)) + application.add_handler(CommandHandler("power", power_command)) + + # Регистрация расширенных обработчиков команд + application.add_handler(CommandHandler("storage", storage_command)) + application.add_handler(CommandHandler("shares", shares_command)) + application.add_handler(CommandHandler("system", system_command)) + application.add_handler(CommandHandler("load", load_command)) + application.add_handler(CommandHandler("security", security_command)) + application.add_handler(CommandHandler("checkapi", check_api_command)) + + # Регистрация обработчика callback-запросов + application.add_handler(CallbackQueryHandler(power_callback)) + + # Настройка обработчиков сигналов для корректного завершения + signal.signal(signal.SIGINT, lambda sig, frame: signal_handler(sig, frame, application)) + signal.signal(signal.SIGTERM, lambda sig, frame: signal_handler(sig, frame, application)) + + # Запуск бота + logger.info("Bot started. Press Ctrl+C to stop.") + application.run_polling(allowed_updates=["message", "callback_query"]) + +if __name__ == "__main__": + main() diff --git a/.history/src/bot_20250830083502.py b/.history/src/bot_20250830083502.py new file mode 100644 index 0000000..031bb35 --- /dev/null +++ b/.history/src/bot_20250830083502.py @@ -0,0 +1,104 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Главный модуль запуска телеграм-бота для управления Synology NAS +""" + +import os +import signal +import sys +import asyncio +import logging +from telegram.ext import ( + Application, + CommandHandler, + CallbackQueryHandler, + MessageHandler, + filters +) + +from src.config.config import TELEGRAM_TOKEN +from src.handlers.command_handlers import ( + start_command, + help_command, + status_command, + power_command, + power_callback +) +from src.handlers.extended_handlers import ( + storage_command, + shares_command, + system_command, + load_command, + security_command, + check_api_command +) +from src.utils.logger import setup_logging + +async def shutdown(application: Application) -> None: + """Корректное завершение работы бота""" + logger = logging.getLogger(__name__) + logger.info("Stopping Synology Power Control Bot...") + + # Останавливаем прием обновлений + await application.stop() + logger.info("Bot stopped successfully") + +def signal_handler(sig, frame, application=None): + """Обработчик сигналов для корректного завершения""" + logger = logging.getLogger(__name__) + logger.info(f"Received signal {sig}, shutting down gracefully") + + if application: + # Создаем и запускаем задачу завершения в event loop + loop = asyncio.get_event_loop() + if loop.is_running(): + loop.create_task(shutdown(application)) + else: + loop.run_until_complete(shutdown(application)) + + sys.exit(0) + +def main() -> None: + """Основная функция запуска бота""" + # Настройка логирования + setup_logging() + logger = logging.getLogger(__name__) + + # Проверка наличия токена + if not TELEGRAM_TOKEN: + logger.error("Telegram token is not set! Please configure TELEGRAM_TOKEN in .env file.") + return + + # Создание и настройка приложения бота + logger.info("Starting Synology Power Control Bot") + application = Application.builder().token(TELEGRAM_TOKEN).build() + + # Регистрация обработчиков команд + application.add_handler(CommandHandler("start", start_command)) + application.add_handler(CommandHandler("help", help_command)) + application.add_handler(CommandHandler("status", status_command)) + application.add_handler(CommandHandler("power", power_command)) + + # Регистрация расширенных обработчиков команд + application.add_handler(CommandHandler("storage", storage_command)) + application.add_handler(CommandHandler("shares", shares_command)) + application.add_handler(CommandHandler("system", system_command)) + application.add_handler(CommandHandler("load", load_command)) + application.add_handler(CommandHandler("security", security_command)) + application.add_handler(CommandHandler("checkapi", check_api_command)) + + # Регистрация обработчика callback-запросов + application.add_handler(CallbackQueryHandler(power_callback)) + + # Настройка обработчиков сигналов для корректного завершения + signal.signal(signal.SIGINT, lambda sig, frame: signal_handler(sig, frame, application)) + signal.signal(signal.SIGTERM, lambda sig, frame: signal_handler(sig, frame, application)) + + # Запуск бота + logger.info("Bot started. Press Ctrl+C to stop.") + application.run_polling(allowed_updates=["message", "callback_query"]) + +if __name__ == "__main__": + main() diff --git a/.history/src/bot_20250830091533.py b/.history/src/bot_20250830091533.py new file mode 100644 index 0000000..733f399 --- /dev/null +++ b/.history/src/bot_20250830091533.py @@ -0,0 +1,119 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Главный модуль запуска телеграм-бота для управления Synology NAS +""" + +import os +import signal +import sys +import asyncio +import logging +from telegram.ext import ( + Application, + CommandHandler, + CallbackQueryHandler, + MessageHandler, + filters +) + +from src.config.config import TELEGRAM_TOKEN +from src.handlers.command_handlers import ( + start_command, + help_command, + status_command, + power_command, + power_callback +) +from src.handlers.extended_handlers import ( + storage_command, + shares_command, + system_command, + load_command, + security_command, + check_api_command +) +from src.handlers.advanced_handlers import ( + processes_command, + network_command, + temperature_command, + schedule_command, + browse_command, + search_command, + updates_command, + backup_command, + quickreboot_command, + wakeup_command, + quota_command, + schedule_callback, + browse_callback +) +from src.utils.logger import setup_logging + +async def shutdown(application: Application) -> None: + """Корректное завершение работы бота""" + logger = logging.getLogger(__name__) + logger.info("Stopping Synology Power Control Bot...") + + # Останавливаем прием обновлений + await application.stop() + logger.info("Bot stopped successfully") + +def signal_handler(sig, frame, application=None): + """Обработчик сигналов для корректного завершения""" + logger = logging.getLogger(__name__) + logger.info(f"Received signal {sig}, shutting down gracefully") + + if application: + # Создаем и запускаем задачу завершения в event loop + loop = asyncio.get_event_loop() + if loop.is_running(): + loop.create_task(shutdown(application)) + else: + loop.run_until_complete(shutdown(application)) + + sys.exit(0) + +def main() -> None: + """Основная функция запуска бота""" + # Настройка логирования + setup_logging() + logger = logging.getLogger(__name__) + + # Проверка наличия токена + if not TELEGRAM_TOKEN: + logger.error("Telegram token is not set! Please configure TELEGRAM_TOKEN in .env file.") + return + + # Создание и настройка приложения бота + logger.info("Starting Synology Power Control Bot") + application = Application.builder().token(TELEGRAM_TOKEN).build() + + # Регистрация обработчиков команд + application.add_handler(CommandHandler("start", start_command)) + application.add_handler(CommandHandler("help", help_command)) + application.add_handler(CommandHandler("status", status_command)) + application.add_handler(CommandHandler("power", power_command)) + + # Регистрация расширенных обработчиков команд + application.add_handler(CommandHandler("storage", storage_command)) + application.add_handler(CommandHandler("shares", shares_command)) + application.add_handler(CommandHandler("system", system_command)) + application.add_handler(CommandHandler("load", load_command)) + application.add_handler(CommandHandler("security", security_command)) + application.add_handler(CommandHandler("checkapi", check_api_command)) + + # Регистрация обработчика callback-запросов + application.add_handler(CallbackQueryHandler(power_callback)) + + # Настройка обработчиков сигналов для корректного завершения + signal.signal(signal.SIGINT, lambda sig, frame: signal_handler(sig, frame, application)) + signal.signal(signal.SIGTERM, lambda sig, frame: signal_handler(sig, frame, application)) + + # Запуск бота + logger.info("Bot started. Press Ctrl+C to stop.") + application.run_polling(allowed_updates=["message", "callback_query"]) + +if __name__ == "__main__": + main() diff --git a/.history/src/bot_20250830091644.py b/.history/src/bot_20250830091644.py new file mode 100644 index 0000000..bfabb6b --- /dev/null +++ b/.history/src/bot_20250830091644.py @@ -0,0 +1,134 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Главный модуль запуска телеграм-бота для управления Synology NAS +""" + +import os +import signal +import sys +import asyncio +import logging +from telegram.ext import ( + Application, + CommandHandler, + CallbackQueryHandler, + MessageHandler, + filters +) + +from src.config.config import TELEGRAM_TOKEN +from src.handlers.command_handlers import ( + start_command, + help_command, + status_command, + power_command, + power_callback +) +from src.handlers.extended_handlers import ( + storage_command, + shares_command, + system_command, + load_command, + security_command, + check_api_command +) +from src.handlers.advanced_handlers import ( + processes_command, + network_command, + temperature_command, + schedule_command, + browse_command, + search_command, + updates_command, + backup_command, + quickreboot_command, + wakeup_command, + quota_command, + schedule_callback, + browse_callback +) +from src.utils.logger import setup_logging + +async def shutdown(application: Application) -> None: + """Корректное завершение работы бота""" + logger = logging.getLogger(__name__) + logger.info("Stopping Synology Power Control Bot...") + + # Останавливаем прием обновлений + await application.stop() + logger.info("Bot stopped successfully") + +def signal_handler(sig, frame, application=None): + """Обработчик сигналов для корректного завершения""" + logger = logging.getLogger(__name__) + logger.info(f"Received signal {sig}, shutting down gracefully") + + if application: + # Создаем и запускаем задачу завершения в event loop + loop = asyncio.get_event_loop() + if loop.is_running(): + loop.create_task(shutdown(application)) + else: + loop.run_until_complete(shutdown(application)) + + sys.exit(0) + +def main() -> None: + """Основная функция запуска бота""" + # Настройка логирования + setup_logging() + logger = logging.getLogger(__name__) + + # Проверка наличия токена + if not TELEGRAM_TOKEN: + logger.error("Telegram token is not set! Please configure TELEGRAM_TOKEN in .env file.") + return + + # Создание и настройка приложения бота + logger.info("Starting Synology Power Control Bot") + application = Application.builder().token(TELEGRAM_TOKEN).build() + + # Регистрация обработчиков команд + application.add_handler(CommandHandler("start", start_command)) + application.add_handler(CommandHandler("help", help_command)) + application.add_handler(CommandHandler("status", status_command)) + application.add_handler(CommandHandler("power", power_command)) + + # Регистрация расширенных обработчиков команд + application.add_handler(CommandHandler("storage", storage_command)) + application.add_handler(CommandHandler("shares", shares_command)) + application.add_handler(CommandHandler("system", system_command)) + application.add_handler(CommandHandler("load", load_command)) + application.add_handler(CommandHandler("security", security_command)) + application.add_handler(CommandHandler("checkapi", check_api_command)) + + # Регистрация продвинутых обработчиков команд + application.add_handler(CommandHandler("processes", processes_command)) + application.add_handler(CommandHandler("network", network_command)) + application.add_handler(CommandHandler("temperature", temperature_command)) + application.add_handler(CommandHandler("schedule", schedule_command)) + application.add_handler(CommandHandler("browse", browse_command)) + application.add_handler(CommandHandler("search", search_command)) + application.add_handler(CommandHandler("updates", updates_command)) + application.add_handler(CommandHandler("backup", backup_command)) + application.add_handler(CommandHandler("quickreboot", quickreboot_command)) + application.add_handler(CommandHandler("wakeup", wakeup_command)) + application.add_handler(CommandHandler("quota", quota_command)) + + # Регистрация обработчика callback-запросов + application.add_handler(CallbackQueryHandler(power_callback)) + application.add_handler(CallbackQueryHandler(schedule_callback, pattern="^schedule_")) + application.add_handler(CallbackQueryHandler(browse_callback, pattern="^browse_")) + + # Настройка обработчиков сигналов для корректного завершения + signal.signal(signal.SIGINT, lambda sig, frame: signal_handler(sig, frame, application)) + signal.signal(signal.SIGTERM, lambda sig, frame: signal_handler(sig, frame, application)) + + # Запуск бота + logger.info("Bot started. Press Ctrl+C to stop.") + application.run_polling(allowed_updates=["message", "callback_query"]) + +if __name__ == "__main__": + main() diff --git a/.history/src/bot_20250830092152.py b/.history/src/bot_20250830092152.py new file mode 100644 index 0000000..e6dadab --- /dev/null +++ b/.history/src/bot_20250830092152.py @@ -0,0 +1,136 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Главный модуль запуска телеграм-бота для управления Synology NAS +""" + +import os +import signal +import sys +import asyncio +import logging +from telegram.ext import ( + Application, + CommandHandler, + CallbackQueryHandler, + MessageHandler, + filters +) + +from src.config.config import TELEGRAM_TOKEN +from src.handlers.help_handlers import ( + start_command, + help_command +) +from src.handlers.command_handlers import ( + status_command, + power_command, + power_callback +) +from src.handlers.extended_handlers import ( + storage_command, + shares_command, + system_command, + load_command, + security_command, + check_api_command +) +from src.handlers.advanced_handlers import ( + processes_command, + network_command, + temperature_command, + schedule_command, + browse_command, + search_command, + updates_command, + backup_command, + quickreboot_command, + wakeup_command, + quota_command, + schedule_callback, + browse_callback +) +from src.utils.logger import setup_logging + +async def shutdown(application: Application) -> None: + """Корректное завершение работы бота""" + logger = logging.getLogger(__name__) + logger.info("Stopping Synology Power Control Bot...") + + # Останавливаем прием обновлений + await application.stop() + logger.info("Bot stopped successfully") + +def signal_handler(sig, frame, application=None): + """Обработчик сигналов для корректного завершения""" + logger = logging.getLogger(__name__) + logger.info(f"Received signal {sig}, shutting down gracefully") + + if application: + # Создаем и запускаем задачу завершения в event loop + loop = asyncio.get_event_loop() + if loop.is_running(): + loop.create_task(shutdown(application)) + else: + loop.run_until_complete(shutdown(application)) + + sys.exit(0) + +def main() -> None: + """Основная функция запуска бота""" + # Настройка логирования + setup_logging() + logger = logging.getLogger(__name__) + + # Проверка наличия токена + if not TELEGRAM_TOKEN: + logger.error("Telegram token is not set! Please configure TELEGRAM_TOKEN in .env file.") + return + + # Создание и настройка приложения бота + logger.info("Starting Synology Power Control Bot") + application = Application.builder().token(TELEGRAM_TOKEN).build() + + # Регистрация обработчиков команд + application.add_handler(CommandHandler("start", start_command)) + application.add_handler(CommandHandler("help", help_command)) + application.add_handler(CommandHandler("status", status_command)) + application.add_handler(CommandHandler("power", power_command)) + + # Регистрация расширенных обработчиков команд + application.add_handler(CommandHandler("storage", storage_command)) + application.add_handler(CommandHandler("shares", shares_command)) + application.add_handler(CommandHandler("system", system_command)) + application.add_handler(CommandHandler("load", load_command)) + application.add_handler(CommandHandler("security", security_command)) + application.add_handler(CommandHandler("checkapi", check_api_command)) + + # Регистрация продвинутых обработчиков команд + application.add_handler(CommandHandler("processes", processes_command)) + application.add_handler(CommandHandler("network", network_command)) + application.add_handler(CommandHandler("temperature", temperature_command)) + application.add_handler(CommandHandler("schedule", schedule_command)) + application.add_handler(CommandHandler("browse", browse_command)) + application.add_handler(CommandHandler("search", search_command)) + application.add_handler(CommandHandler("updates", updates_command)) + application.add_handler(CommandHandler("backup", backup_command)) + application.add_handler(CommandHandler("quickreboot", quickreboot_command)) + application.add_handler(CommandHandler("wakeup", wakeup_command)) + application.add_handler(CommandHandler("quota", quota_command)) + + # Регистрация обработчика callback-запросов + application.add_handler(CallbackQueryHandler(power_callback)) + application.add_handler(CallbackQueryHandler(schedule_callback, pattern="^schedule_")) + application.add_handler(CallbackQueryHandler(browse_callback, pattern="^browse_")) + + # Настройка обработчиков сигналов для корректного завершения + signal.signal(signal.SIGINT, lambda sig, frame: signal_handler(sig, frame, application)) + signal.signal(signal.SIGTERM, lambda sig, frame: signal_handler(sig, frame, application)) + + # Запуск бота + logger.info("Bot started. Press Ctrl+C to stop.") + application.run_polling(allowed_updates=["message", "callback_query"]) + +if __name__ == "__main__": + main() diff --git a/.history/src/bot_20250830092440.py b/.history/src/bot_20250830092440.py new file mode 100644 index 0000000..e6dadab --- /dev/null +++ b/.history/src/bot_20250830092440.py @@ -0,0 +1,136 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Главный модуль запуска телеграм-бота для управления Synology NAS +""" + +import os +import signal +import sys +import asyncio +import logging +from telegram.ext import ( + Application, + CommandHandler, + CallbackQueryHandler, + MessageHandler, + filters +) + +from src.config.config import TELEGRAM_TOKEN +from src.handlers.help_handlers import ( + start_command, + help_command +) +from src.handlers.command_handlers import ( + status_command, + power_command, + power_callback +) +from src.handlers.extended_handlers import ( + storage_command, + shares_command, + system_command, + load_command, + security_command, + check_api_command +) +from src.handlers.advanced_handlers import ( + processes_command, + network_command, + temperature_command, + schedule_command, + browse_command, + search_command, + updates_command, + backup_command, + quickreboot_command, + wakeup_command, + quota_command, + schedule_callback, + browse_callback +) +from src.utils.logger import setup_logging + +async def shutdown(application: Application) -> None: + """Корректное завершение работы бота""" + logger = logging.getLogger(__name__) + logger.info("Stopping Synology Power Control Bot...") + + # Останавливаем прием обновлений + await application.stop() + logger.info("Bot stopped successfully") + +def signal_handler(sig, frame, application=None): + """Обработчик сигналов для корректного завершения""" + logger = logging.getLogger(__name__) + logger.info(f"Received signal {sig}, shutting down gracefully") + + if application: + # Создаем и запускаем задачу завершения в event loop + loop = asyncio.get_event_loop() + if loop.is_running(): + loop.create_task(shutdown(application)) + else: + loop.run_until_complete(shutdown(application)) + + sys.exit(0) + +def main() -> None: + """Основная функция запуска бота""" + # Настройка логирования + setup_logging() + logger = logging.getLogger(__name__) + + # Проверка наличия токена + if not TELEGRAM_TOKEN: + logger.error("Telegram token is not set! Please configure TELEGRAM_TOKEN in .env file.") + return + + # Создание и настройка приложения бота + logger.info("Starting Synology Power Control Bot") + application = Application.builder().token(TELEGRAM_TOKEN).build() + + # Регистрация обработчиков команд + application.add_handler(CommandHandler("start", start_command)) + application.add_handler(CommandHandler("help", help_command)) + application.add_handler(CommandHandler("status", status_command)) + application.add_handler(CommandHandler("power", power_command)) + + # Регистрация расширенных обработчиков команд + application.add_handler(CommandHandler("storage", storage_command)) + application.add_handler(CommandHandler("shares", shares_command)) + application.add_handler(CommandHandler("system", system_command)) + application.add_handler(CommandHandler("load", load_command)) + application.add_handler(CommandHandler("security", security_command)) + application.add_handler(CommandHandler("checkapi", check_api_command)) + + # Регистрация продвинутых обработчиков команд + application.add_handler(CommandHandler("processes", processes_command)) + application.add_handler(CommandHandler("network", network_command)) + application.add_handler(CommandHandler("temperature", temperature_command)) + application.add_handler(CommandHandler("schedule", schedule_command)) + application.add_handler(CommandHandler("browse", browse_command)) + application.add_handler(CommandHandler("search", search_command)) + application.add_handler(CommandHandler("updates", updates_command)) + application.add_handler(CommandHandler("backup", backup_command)) + application.add_handler(CommandHandler("quickreboot", quickreboot_command)) + application.add_handler(CommandHandler("wakeup", wakeup_command)) + application.add_handler(CommandHandler("quota", quota_command)) + + # Регистрация обработчика callback-запросов + application.add_handler(CallbackQueryHandler(power_callback)) + application.add_handler(CallbackQueryHandler(schedule_callback, pattern="^schedule_")) + application.add_handler(CallbackQueryHandler(browse_callback, pattern="^browse_")) + + # Настройка обработчиков сигналов для корректного завершения + signal.signal(signal.SIGINT, lambda sig, frame: signal_handler(sig, frame, application)) + signal.signal(signal.SIGTERM, lambda sig, frame: signal_handler(sig, frame, application)) + + # Запуск бота + logger.info("Bot started. Press Ctrl+C to stop.") + application.run_polling(allowed_updates=["message", "callback_query"]) + +if __name__ == "__main__": + main() diff --git a/.history/src/bot_20250830093455.py b/.history/src/bot_20250830093455.py new file mode 100644 index 0000000..93f7edd --- /dev/null +++ b/.history/src/bot_20250830093455.py @@ -0,0 +1,139 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Главный модуль запуска телеграм-бота для управления Synology NAS +""" + +import os +import signal +import sys +import asyncio +import logging +from telegram.ext import ( + Application, + CommandHandler, + CallbackQueryHandler, + MessageHandler, + filters +) + +from src.config.config import TELEGRAM_TOKEN +from src.handlers.help_handlers import ( + start_command, + help_command +) +from src.handlers.command_handlers import ( + status_command, + power_command, + power_callback +) +from src.handlers.extended_handlers import ( + storage_command, + shares_command, + system_command, + load_command, + security_command, + check_api_command +) +from src.handlers.advanced_handlers import ( + processes_command, + network_command, + temperature_command, + schedule_command, + browse_command, + search_command, + updates_command, + backup_command, + quickreboot_command, + reboot_command, + sleep_command, + wakeup_command, + quota_command, + schedule_callback, + browse_callback, + power_callback +) +from src.utils.logger import setup_logging + +async def shutdown(application: Application) -> None: + """Корректное завершение работы бота""" + logger = logging.getLogger(__name__) + logger.info("Stopping Synology Power Control Bot...") + + # Останавливаем прием обновлений + await application.stop() + logger.info("Bot stopped successfully") + +def signal_handler(sig, frame, application=None): + """Обработчик сигналов для корректного завершения""" + logger = logging.getLogger(__name__) + logger.info(f"Received signal {sig}, shutting down gracefully") + + if application: + # Создаем и запускаем задачу завершения в event loop + loop = asyncio.get_event_loop() + if loop.is_running(): + loop.create_task(shutdown(application)) + else: + loop.run_until_complete(shutdown(application)) + + sys.exit(0) + +def main() -> None: + """Основная функция запуска бота""" + # Настройка логирования + setup_logging() + logger = logging.getLogger(__name__) + + # Проверка наличия токена + if not TELEGRAM_TOKEN: + logger.error("Telegram token is not set! Please configure TELEGRAM_TOKEN in .env file.") + return + + # Создание и настройка приложения бота + logger.info("Starting Synology Power Control Bot") + application = Application.builder().token(TELEGRAM_TOKEN).build() + + # Регистрация обработчиков команд + application.add_handler(CommandHandler("start", start_command)) + application.add_handler(CommandHandler("help", help_command)) + application.add_handler(CommandHandler("status", status_command)) + application.add_handler(CommandHandler("power", power_command)) + + # Регистрация расширенных обработчиков команд + application.add_handler(CommandHandler("storage", storage_command)) + application.add_handler(CommandHandler("shares", shares_command)) + application.add_handler(CommandHandler("system", system_command)) + application.add_handler(CommandHandler("load", load_command)) + application.add_handler(CommandHandler("security", security_command)) + application.add_handler(CommandHandler("checkapi", check_api_command)) + + # Регистрация продвинутых обработчиков команд + application.add_handler(CommandHandler("processes", processes_command)) + application.add_handler(CommandHandler("network", network_command)) + application.add_handler(CommandHandler("temperature", temperature_command)) + application.add_handler(CommandHandler("schedule", schedule_command)) + application.add_handler(CommandHandler("browse", browse_command)) + application.add_handler(CommandHandler("search", search_command)) + application.add_handler(CommandHandler("updates", updates_command)) + application.add_handler(CommandHandler("backup", backup_command)) + application.add_handler(CommandHandler("quickreboot", quickreboot_command)) + application.add_handler(CommandHandler("wakeup", wakeup_command)) + application.add_handler(CommandHandler("quota", quota_command)) + + # Регистрация обработчика callback-запросов + application.add_handler(CallbackQueryHandler(power_callback)) + application.add_handler(CallbackQueryHandler(schedule_callback, pattern="^schedule_")) + application.add_handler(CallbackQueryHandler(browse_callback, pattern="^browse_")) + + # Настройка обработчиков сигналов для корректного завершения + signal.signal(signal.SIGINT, lambda sig, frame: signal_handler(sig, frame, application)) + signal.signal(signal.SIGTERM, lambda sig, frame: signal_handler(sig, frame, application)) + + # Запуск бота + logger.info("Bot started. Press Ctrl+C to stop.") + application.run_polling(allowed_updates=["message", "callback_query"]) + +if __name__ == "__main__": + main() diff --git a/.history/src/bot_20250830093513.py b/.history/src/bot_20250830093513.py new file mode 100644 index 0000000..066a504 --- /dev/null +++ b/.history/src/bot_20250830093513.py @@ -0,0 +1,141 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Главный модуль запуска телеграм-бота для управления Synology NAS +""" + +import os +import signal +import sys +import asyncio +import logging +from telegram.ext import ( + Application, + CommandHandler, + CallbackQueryHandler, + MessageHandler, + filters +) + +from src.config.config import TELEGRAM_TOKEN +from src.handlers.help_handlers import ( + start_command, + help_command +) +from src.handlers.command_handlers import ( + status_command, + power_command, + power_callback +) +from src.handlers.extended_handlers import ( + storage_command, + shares_command, + system_command, + load_command, + security_command, + check_api_command +) +from src.handlers.advanced_handlers import ( + processes_command, + network_command, + temperature_command, + schedule_command, + browse_command, + search_command, + updates_command, + backup_command, + quickreboot_command, + reboot_command, + sleep_command, + wakeup_command, + quota_command, + schedule_callback, + browse_callback, + power_callback +) +from src.utils.logger import setup_logging + +async def shutdown(application: Application) -> None: + """Корректное завершение работы бота""" + logger = logging.getLogger(__name__) + logger.info("Stopping Synology Power Control Bot...") + + # Останавливаем прием обновлений + await application.stop() + logger.info("Bot stopped successfully") + +def signal_handler(sig, frame, application=None): + """Обработчик сигналов для корректного завершения""" + logger = logging.getLogger(__name__) + logger.info(f"Received signal {sig}, shutting down gracefully") + + if application: + # Создаем и запускаем задачу завершения в event loop + loop = asyncio.get_event_loop() + if loop.is_running(): + loop.create_task(shutdown(application)) + else: + loop.run_until_complete(shutdown(application)) + + sys.exit(0) + +def main() -> None: + """Основная функция запуска бота""" + # Настройка логирования + setup_logging() + logger = logging.getLogger(__name__) + + # Проверка наличия токена + if not TELEGRAM_TOKEN: + logger.error("Telegram token is not set! Please configure TELEGRAM_TOKEN in .env file.") + return + + # Создание и настройка приложения бота + logger.info("Starting Synology Power Control Bot") + application = Application.builder().token(TELEGRAM_TOKEN).build() + + # Регистрация обработчиков команд + application.add_handler(CommandHandler("start", start_command)) + application.add_handler(CommandHandler("help", help_command)) + application.add_handler(CommandHandler("status", status_command)) + application.add_handler(CommandHandler("power", power_command)) + + # Регистрация расширенных обработчиков команд + application.add_handler(CommandHandler("storage", storage_command)) + application.add_handler(CommandHandler("shares", shares_command)) + application.add_handler(CommandHandler("system", system_command)) + application.add_handler(CommandHandler("load", load_command)) + application.add_handler(CommandHandler("security", security_command)) + application.add_handler(CommandHandler("checkapi", check_api_command)) + + # Регистрация продвинутых обработчиков команд + application.add_handler(CommandHandler("processes", processes_command)) + application.add_handler(CommandHandler("network", network_command)) + application.add_handler(CommandHandler("temperature", temperature_command)) + application.add_handler(CommandHandler("schedule", schedule_command)) + application.add_handler(CommandHandler("browse", browse_command)) + application.add_handler(CommandHandler("search", search_command)) + application.add_handler(CommandHandler("updates", updates_command)) + application.add_handler(CommandHandler("backup", backup_command)) + application.add_handler(CommandHandler("quickreboot", quickreboot_command)) + application.add_handler(CommandHandler("reboot", reboot_command)) + application.add_handler(CommandHandler("sleep", sleep_command)) + application.add_handler(CommandHandler("wakeup", wakeup_command)) + application.add_handler(CommandHandler("quota", quota_command)) + + # Регистрация обработчика callback-запросов + application.add_handler(CallbackQueryHandler(power_callback)) + application.add_handler(CallbackQueryHandler(schedule_callback, pattern="^schedule_")) + application.add_handler(CallbackQueryHandler(browse_callback, pattern="^browse_")) + + # Настройка обработчиков сигналов для корректного завершения + signal.signal(signal.SIGINT, lambda sig, frame: signal_handler(sig, frame, application)) + signal.signal(signal.SIGTERM, lambda sig, frame: signal_handler(sig, frame, application)) + + # Запуск бота + logger.info("Bot started. Press Ctrl+C to stop.") + application.run_polling(allowed_updates=["message", "callback_query"]) + +if __name__ == "__main__": + main() diff --git a/.history/src/bot_20250830093531.py b/.history/src/bot_20250830093531.py new file mode 100644 index 0000000..e272c26 --- /dev/null +++ b/.history/src/bot_20250830093531.py @@ -0,0 +1,141 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Главный модуль запуска телеграм-бота для управления Synology NAS +""" + +import os +import signal +import sys +import asyncio +import logging +from telegram.ext import ( + Application, + CommandHandler, + CallbackQueryHandler, + MessageHandler, + filters +) + +from src.config.config import TELEGRAM_TOKEN +from src.handlers.help_handlers import ( + start_command, + help_command +) +from src.handlers.command_handlers import ( + status_command, + power_command, + power_callback +) +from src.handlers.extended_handlers import ( + storage_command, + shares_command, + system_command, + load_command, + security_command, + check_api_command +) +from src.handlers.advanced_handlers import ( + processes_command, + network_command, + temperature_command, + schedule_command, + browse_command, + search_command, + updates_command, + backup_command, + quickreboot_command, + reboot_command, + sleep_command, + wakeup_command, + quota_command, + schedule_callback, + browse_callback, + power_callback +) +from src.utils.logger import setup_logging + +async def shutdown(application: Application) -> None: + """Корректное завершение работы бота""" + logger = logging.getLogger(__name__) + logger.info("Stopping Synology Power Control Bot...") + + # Останавливаем прием обновлений + await application.stop() + logger.info("Bot stopped successfully") + +def signal_handler(sig, frame, application=None): + """Обработчик сигналов для корректного завершения""" + logger = logging.getLogger(__name__) + logger.info(f"Received signal {sig}, shutting down gracefully") + + if application: + # Создаем и запускаем задачу завершения в event loop + loop = asyncio.get_event_loop() + if loop.is_running(): + loop.create_task(shutdown(application)) + else: + loop.run_until_complete(shutdown(application)) + + sys.exit(0) + +def main() -> None: + """Основная функция запуска бота""" + # Настройка логирования + setup_logging() + logger = logging.getLogger(__name__) + + # Проверка наличия токена + if not TELEGRAM_TOKEN: + logger.error("Telegram token is not set! Please configure TELEGRAM_TOKEN in .env file.") + return + + # Создание и настройка приложения бота + logger.info("Starting Synology Power Control Bot") + application = Application.builder().token(TELEGRAM_TOKEN).build() + + # Регистрация обработчиков команд + application.add_handler(CommandHandler("start", start_command)) + application.add_handler(CommandHandler("help", help_command)) + application.add_handler(CommandHandler("status", status_command)) + application.add_handler(CommandHandler("power", power_command)) + + # Регистрация расширенных обработчиков команд + application.add_handler(CommandHandler("storage", storage_command)) + application.add_handler(CommandHandler("shares", shares_command)) + application.add_handler(CommandHandler("system", system_command)) + application.add_handler(CommandHandler("load", load_command)) + application.add_handler(CommandHandler("security", security_command)) + application.add_handler(CommandHandler("checkapi", check_api_command)) + + # Регистрация продвинутых обработчиков команд + application.add_handler(CommandHandler("processes", processes_command)) + application.add_handler(CommandHandler("network", network_command)) + application.add_handler(CommandHandler("temperature", temperature_command)) + application.add_handler(CommandHandler("schedule", schedule_command)) + application.add_handler(CommandHandler("browse", browse_command)) + application.add_handler(CommandHandler("search", search_command)) + application.add_handler(CommandHandler("updates", updates_command)) + application.add_handler(CommandHandler("backup", backup_command)) + application.add_handler(CommandHandler("quickreboot", quickreboot_command)) + application.add_handler(CommandHandler("reboot", reboot_command)) + application.add_handler(CommandHandler("sleep", sleep_command)) + application.add_handler(CommandHandler("wakeup", wakeup_command)) + application.add_handler(CommandHandler("quota", quota_command)) + + # Регистрация обработчика callback-запросов + application.add_handler(CallbackQueryHandler(power_callback, pattern="^(confirm|cancel)_(reboot|sleep)$")) + application.add_handler(CallbackQueryHandler(schedule_callback, pattern="^schedule_")) + application.add_handler(CallbackQueryHandler(browse_callback, pattern="^browse_")) + + # Настройка обработчиков сигналов для корректного завершения + signal.signal(signal.SIGINT, lambda sig, frame: signal_handler(sig, frame, application)) + signal.signal(signal.SIGTERM, lambda sig, frame: signal_handler(sig, frame, application)) + + # Запуск бота + logger.info("Bot started. Press Ctrl+C to stop.") + application.run_polling(allowed_updates=["message", "callback_query"]) + +if __name__ == "__main__": + main() diff --git a/.history/src/bot_20250830093606.py b/.history/src/bot_20250830093606.py new file mode 100644 index 0000000..f9f0a2c --- /dev/null +++ b/.history/src/bot_20250830093606.py @@ -0,0 +1,142 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Главный модуль запуска телеграм-бота для управления Synology NAS +""" + +import os +import signal +import sys +import asyncio +import logging +from telegram.ext import ( + Application, + CommandHandler, + CallbackQueryHandler, + MessageHandler, + filters +) + +from src.config.config import TELEGRAM_TOKEN +from src.handlers.help_handlers import ( + start_command, + help_command +) +from src.handlers.command_handlers import ( + status_command, + power_command, + power_callback +) +from src.handlers.extended_handlers import ( + storage_command, + shares_command, + system_command, + load_command, + security_command, + check_api_command +) +from src.handlers.advanced_handlers import ( + processes_command, + network_command, + temperature_command, + schedule_command, + browse_command, + search_command, + updates_command, + backup_command, + quickreboot_command, + reboot_command, + sleep_command, + wakeup_command, + quota_command, + schedule_callback, + browse_callback, + power_callback +) +from src.utils.logger import setup_logging + +async def shutdown(application: Application) -> None: + """Корректное завершение работы бота""" + logger = logging.getLogger(__name__) + logger.info("Stopping Synology Power Control Bot...") + + # Останавливаем прием обновлений + await application.stop() + logger.info("Bot stopped successfully") + +def signal_handler(sig, frame, application=None): + """Обработчик сигналов для корректного завершения""" + logger = logging.getLogger(__name__) + logger.info(f"Received signal {sig}, shutting down gracefully") + + if application: + # Создаем и запускаем задачу завершения в event loop + loop = asyncio.get_event_loop() + if loop.is_running(): + loop.create_task(shutdown(application)) + else: + loop.run_until_complete(shutdown(application)) + + sys.exit(0) + +def main() -> None: + """Основная функция запуска бота""" + # Настройка логирования + setup_logging() + logger = logging.getLogger(__name__) + + # Проверка наличия токена + if not TELEGRAM_TOKEN: + logger.error("Telegram token is not set! Please configure TELEGRAM_TOKEN in .env file.") + return + + # Создание и настройка приложения бота + logger.info("Starting Synology Power Control Bot") + application = Application.builder().token(TELEGRAM_TOKEN).build() + + # Регистрация обработчиков команд + application.add_handler(CommandHandler("start", start_command)) + application.add_handler(CommandHandler("help", help_command)) + application.add_handler(CommandHandler("status", status_command)) + application.add_handler(CommandHandler("power", power_command)) + + # Регистрация расширенных обработчиков команд + application.add_handler(CommandHandler("storage", storage_command)) + application.add_handler(CommandHandler("shares", shares_command)) + application.add_handler(CommandHandler("system", system_command)) + application.add_handler(CommandHandler("load", load_command)) + application.add_handler(CommandHandler("security", security_command)) + application.add_handler(CommandHandler("checkapi", check_api_command)) + + # Регистрация продвинутых обработчиков команд + application.add_handler(CommandHandler("processes", processes_command)) + application.add_handler(CommandHandler("network", network_command)) + application.add_handler(CommandHandler("temperature", temperature_command)) + application.add_handler(CommandHandler("schedule", schedule_command)) + application.add_handler(CommandHandler("browse", browse_command)) + application.add_handler(CommandHandler("search", search_command)) + application.add_handler(CommandHandler("updates", updates_command)) + application.add_handler(CommandHandler("backup", backup_command)) + application.add_handler(CommandHandler("quickreboot", quickreboot_command)) + application.add_handler(CommandHandler("reboot", reboot_command)) + application.add_handler(CommandHandler("sleep", sleep_command)) + application.add_handler(CommandHandler("wakeup", wakeup_command)) + application.add_handler(CommandHandler("quota", quota_command)) + + # Регистрация обработчиков callback-запросов + application.add_handler(CallbackQueryHandler(power_callback, pattern="^power_|^reboot$|^reboot_|^cancel$")) # Для command_handlers.py + application.add_handler(CallbackQueryHandler(power_callback, pattern="^(confirm|cancel)_(reboot|sleep)$")) # Для advanced_handlers.py + application.add_handler(CallbackQueryHandler(schedule_callback, pattern="^schedule_")) + application.add_handler(CallbackQueryHandler(browse_callback, pattern="^browse_")) + + # Настройка обработчиков сигналов для корректного завершения + signal.signal(signal.SIGINT, lambda sig, frame: signal_handler(sig, frame, application)) + signal.signal(signal.SIGTERM, lambda sig, frame: signal_handler(sig, frame, application)) + + # Запуск бота + logger.info("Bot started. Press Ctrl+C to stop.") + application.run_polling(allowed_updates=["message", "callback_query"]) + +if __name__ == "__main__": + main() diff --git a/.history/src/bot_20250830093645.py b/.history/src/bot_20250830093645.py new file mode 100644 index 0000000..fc47ec1 --- /dev/null +++ b/.history/src/bot_20250830093645.py @@ -0,0 +1,142 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Главный модуль запуска телеграм-бота для управления Synology NAS +""" + +import os +import signal +import sys +import asyncio +import logging +from telegram.ext import ( + Application, + CommandHandler, + CallbackQueryHandler, + MessageHandler, + filters +) + +from src.config.config import TELEGRAM_TOKEN +from src.handlers.help_handlers import ( + start_command, + help_command +) +from src.handlers.command_handlers import ( + status_command, + power_command, + power_callback +) +from src.handlers.extended_handlers import ( + storage_command, + shares_command, + system_command, + load_command, + security_command, + check_api_command +) +from src.handlers.advanced_handlers import ( + processes_command, + network_command, + temperature_command, + schedule_command, + browse_command, + search_command, + updates_command, + backup_command, + quickreboot_command, + reboot_command, + sleep_command, + wakeup_command, + quota_command, + schedule_callback, + browse_callback, + advanced_power_callback +) +from src.utils.logger import setup_logging + +async def shutdown(application: Application) -> None: + """Корректное завершение работы бота""" + logger = logging.getLogger(__name__) + logger.info("Stopping Synology Power Control Bot...") + + # Останавливаем прием обновлений + await application.stop() + logger.info("Bot stopped successfully") + +def signal_handler(sig, frame, application=None): + """Обработчик сигналов для корректного завершения""" + logger = logging.getLogger(__name__) + logger.info(f"Received signal {sig}, shutting down gracefully") + + if application: + # Создаем и запускаем задачу завершения в event loop + loop = asyncio.get_event_loop() + if loop.is_running(): + loop.create_task(shutdown(application)) + else: + loop.run_until_complete(shutdown(application)) + + sys.exit(0) + +def main() -> None: + """Основная функция запуска бота""" + # Настройка логирования + setup_logging() + logger = logging.getLogger(__name__) + + # Проверка наличия токена + if not TELEGRAM_TOKEN: + logger.error("Telegram token is not set! Please configure TELEGRAM_TOKEN in .env file.") + return + + # Создание и настройка приложения бота + logger.info("Starting Synology Power Control Bot") + application = Application.builder().token(TELEGRAM_TOKEN).build() + + # Регистрация обработчиков команд + application.add_handler(CommandHandler("start", start_command)) + application.add_handler(CommandHandler("help", help_command)) + application.add_handler(CommandHandler("status", status_command)) + application.add_handler(CommandHandler("power", power_command)) + + # Регистрация расширенных обработчиков команд + application.add_handler(CommandHandler("storage", storage_command)) + application.add_handler(CommandHandler("shares", shares_command)) + application.add_handler(CommandHandler("system", system_command)) + application.add_handler(CommandHandler("load", load_command)) + application.add_handler(CommandHandler("security", security_command)) + application.add_handler(CommandHandler("checkapi", check_api_command)) + + # Регистрация продвинутых обработчиков команд + application.add_handler(CommandHandler("processes", processes_command)) + application.add_handler(CommandHandler("network", network_command)) + application.add_handler(CommandHandler("temperature", temperature_command)) + application.add_handler(CommandHandler("schedule", schedule_command)) + application.add_handler(CommandHandler("browse", browse_command)) + application.add_handler(CommandHandler("search", search_command)) + application.add_handler(CommandHandler("updates", updates_command)) + application.add_handler(CommandHandler("backup", backup_command)) + application.add_handler(CommandHandler("quickreboot", quickreboot_command)) + application.add_handler(CommandHandler("reboot", reboot_command)) + application.add_handler(CommandHandler("sleep", sleep_command)) + application.add_handler(CommandHandler("wakeup", wakeup_command)) + application.add_handler(CommandHandler("quota", quota_command)) + + # Регистрация обработчиков callback-запросов + application.add_handler(CallbackQueryHandler(power_callback, pattern="^power_|^reboot$|^reboot_|^cancel$")) # Для command_handlers.py + application.add_handler(CallbackQueryHandler(power_callback, pattern="^(confirm|cancel)_(reboot|sleep)$")) # Для advanced_handlers.py + application.add_handler(CallbackQueryHandler(schedule_callback, pattern="^schedule_")) + application.add_handler(CallbackQueryHandler(browse_callback, pattern="^browse_")) + + # Настройка обработчиков сигналов для корректного завершения + signal.signal(signal.SIGINT, lambda sig, frame: signal_handler(sig, frame, application)) + signal.signal(signal.SIGTERM, lambda sig, frame: signal_handler(sig, frame, application)) + + # Запуск бота + logger.info("Bot started. Press Ctrl+C to stop.") + application.run_polling(allowed_updates=["message", "callback_query"]) + +if __name__ == "__main__": + main() diff --git a/.history/src/bot_20250830093703.py b/.history/src/bot_20250830093703.py new file mode 100644 index 0000000..b378a07 --- /dev/null +++ b/.history/src/bot_20250830093703.py @@ -0,0 +1,142 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Главный модуль запуска телеграм-бота для управления Synology NAS +""" + +import os +import signal +import sys +import asyncio +import logging +from telegram.ext import ( + Application, + CommandHandler, + CallbackQueryHandler, + MessageHandler, + filters +) + +from src.config.config import TELEGRAM_TOKEN +from src.handlers.help_handlers import ( + start_command, + help_command +) +from src.handlers.command_handlers import ( + status_command, + power_command, + power_callback +) +from src.handlers.extended_handlers import ( + storage_command, + shares_command, + system_command, + load_command, + security_command, + check_api_command +) +from src.handlers.advanced_handlers import ( + processes_command, + network_command, + temperature_command, + schedule_command, + browse_command, + search_command, + updates_command, + backup_command, + quickreboot_command, + reboot_command, + sleep_command, + wakeup_command, + quota_command, + schedule_callback, + browse_callback, + advanced_power_callback +) +from src.utils.logger import setup_logging + +async def shutdown(application: Application) -> None: + """Корректное завершение работы бота""" + logger = logging.getLogger(__name__) + logger.info("Stopping Synology Power Control Bot...") + + # Останавливаем прием обновлений + await application.stop() + logger.info("Bot stopped successfully") + +def signal_handler(sig, frame, application=None): + """Обработчик сигналов для корректного завершения""" + logger = logging.getLogger(__name__) + logger.info(f"Received signal {sig}, shutting down gracefully") + + if application: + # Создаем и запускаем задачу завершения в event loop + loop = asyncio.get_event_loop() + if loop.is_running(): + loop.create_task(shutdown(application)) + else: + loop.run_until_complete(shutdown(application)) + + sys.exit(0) + +def main() -> None: + """Основная функция запуска бота""" + # Настройка логирования + setup_logging() + logger = logging.getLogger(__name__) + + # Проверка наличия токена + if not TELEGRAM_TOKEN: + logger.error("Telegram token is not set! Please configure TELEGRAM_TOKEN in .env file.") + return + + # Создание и настройка приложения бота + logger.info("Starting Synology Power Control Bot") + application = Application.builder().token(TELEGRAM_TOKEN).build() + + # Регистрация обработчиков команд + application.add_handler(CommandHandler("start", start_command)) + application.add_handler(CommandHandler("help", help_command)) + application.add_handler(CommandHandler("status", status_command)) + application.add_handler(CommandHandler("power", power_command)) + + # Регистрация расширенных обработчиков команд + application.add_handler(CommandHandler("storage", storage_command)) + application.add_handler(CommandHandler("shares", shares_command)) + application.add_handler(CommandHandler("system", system_command)) + application.add_handler(CommandHandler("load", load_command)) + application.add_handler(CommandHandler("security", security_command)) + application.add_handler(CommandHandler("checkapi", check_api_command)) + + # Регистрация продвинутых обработчиков команд + application.add_handler(CommandHandler("processes", processes_command)) + application.add_handler(CommandHandler("network", network_command)) + application.add_handler(CommandHandler("temperature", temperature_command)) + application.add_handler(CommandHandler("schedule", schedule_command)) + application.add_handler(CommandHandler("browse", browse_command)) + application.add_handler(CommandHandler("search", search_command)) + application.add_handler(CommandHandler("updates", updates_command)) + application.add_handler(CommandHandler("backup", backup_command)) + application.add_handler(CommandHandler("quickreboot", quickreboot_command)) + application.add_handler(CommandHandler("reboot", reboot_command)) + application.add_handler(CommandHandler("sleep", sleep_command)) + application.add_handler(CommandHandler("wakeup", wakeup_command)) + application.add_handler(CommandHandler("quota", quota_command)) + + # Регистрация обработчиков callback-запросов + application.add_handler(CallbackQueryHandler(power_callback, pattern="^power_|^reboot$|^reboot_|^cancel$")) # Для command_handlers.py + application.add_handler(CallbackQueryHandler(advanced_power_callback, pattern="^(confirm|cancel)_(reboot|sleep)$")) # Для advanced_handlers.py + application.add_handler(CallbackQueryHandler(schedule_callback, pattern="^schedule_")) + application.add_handler(CallbackQueryHandler(browse_callback, pattern="^browse_")) + + # Настройка обработчиков сигналов для корректного завершения + signal.signal(signal.SIGINT, lambda sig, frame: signal_handler(sig, frame, application)) + signal.signal(signal.SIGTERM, lambda sig, frame: signal_handler(sig, frame, application)) + + # Запуск бота + logger.info("Bot started. Press Ctrl+C to stop.") + application.run_polling(allowed_updates=["message", "callback_query"]) + +if __name__ == "__main__": + main() diff --git a/.history/src/bot_20250830094738.py b/.history/src/bot_20250830094738.py new file mode 100644 index 0000000..b378a07 --- /dev/null +++ b/.history/src/bot_20250830094738.py @@ -0,0 +1,142 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Главный модуль запуска телеграм-бота для управления Synology NAS +""" + +import os +import signal +import sys +import asyncio +import logging +from telegram.ext import ( + Application, + CommandHandler, + CallbackQueryHandler, + MessageHandler, + filters +) + +from src.config.config import TELEGRAM_TOKEN +from src.handlers.help_handlers import ( + start_command, + help_command +) +from src.handlers.command_handlers import ( + status_command, + power_command, + power_callback +) +from src.handlers.extended_handlers import ( + storage_command, + shares_command, + system_command, + load_command, + security_command, + check_api_command +) +from src.handlers.advanced_handlers import ( + processes_command, + network_command, + temperature_command, + schedule_command, + browse_command, + search_command, + updates_command, + backup_command, + quickreboot_command, + reboot_command, + sleep_command, + wakeup_command, + quota_command, + schedule_callback, + browse_callback, + advanced_power_callback +) +from src.utils.logger import setup_logging + +async def shutdown(application: Application) -> None: + """Корректное завершение работы бота""" + logger = logging.getLogger(__name__) + logger.info("Stopping Synology Power Control Bot...") + + # Останавливаем прием обновлений + await application.stop() + logger.info("Bot stopped successfully") + +def signal_handler(sig, frame, application=None): + """Обработчик сигналов для корректного завершения""" + logger = logging.getLogger(__name__) + logger.info(f"Received signal {sig}, shutting down gracefully") + + if application: + # Создаем и запускаем задачу завершения в event loop + loop = asyncio.get_event_loop() + if loop.is_running(): + loop.create_task(shutdown(application)) + else: + loop.run_until_complete(shutdown(application)) + + sys.exit(0) + +def main() -> None: + """Основная функция запуска бота""" + # Настройка логирования + setup_logging() + logger = logging.getLogger(__name__) + + # Проверка наличия токена + if not TELEGRAM_TOKEN: + logger.error("Telegram token is not set! Please configure TELEGRAM_TOKEN in .env file.") + return + + # Создание и настройка приложения бота + logger.info("Starting Synology Power Control Bot") + application = Application.builder().token(TELEGRAM_TOKEN).build() + + # Регистрация обработчиков команд + application.add_handler(CommandHandler("start", start_command)) + application.add_handler(CommandHandler("help", help_command)) + application.add_handler(CommandHandler("status", status_command)) + application.add_handler(CommandHandler("power", power_command)) + + # Регистрация расширенных обработчиков команд + application.add_handler(CommandHandler("storage", storage_command)) + application.add_handler(CommandHandler("shares", shares_command)) + application.add_handler(CommandHandler("system", system_command)) + application.add_handler(CommandHandler("load", load_command)) + application.add_handler(CommandHandler("security", security_command)) + application.add_handler(CommandHandler("checkapi", check_api_command)) + + # Регистрация продвинутых обработчиков команд + application.add_handler(CommandHandler("processes", processes_command)) + application.add_handler(CommandHandler("network", network_command)) + application.add_handler(CommandHandler("temperature", temperature_command)) + application.add_handler(CommandHandler("schedule", schedule_command)) + application.add_handler(CommandHandler("browse", browse_command)) + application.add_handler(CommandHandler("search", search_command)) + application.add_handler(CommandHandler("updates", updates_command)) + application.add_handler(CommandHandler("backup", backup_command)) + application.add_handler(CommandHandler("quickreboot", quickreboot_command)) + application.add_handler(CommandHandler("reboot", reboot_command)) + application.add_handler(CommandHandler("sleep", sleep_command)) + application.add_handler(CommandHandler("wakeup", wakeup_command)) + application.add_handler(CommandHandler("quota", quota_command)) + + # Регистрация обработчиков callback-запросов + application.add_handler(CallbackQueryHandler(power_callback, pattern="^power_|^reboot$|^reboot_|^cancel$")) # Для command_handlers.py + application.add_handler(CallbackQueryHandler(advanced_power_callback, pattern="^(confirm|cancel)_(reboot|sleep)$")) # Для advanced_handlers.py + application.add_handler(CallbackQueryHandler(schedule_callback, pattern="^schedule_")) + application.add_handler(CallbackQueryHandler(browse_callback, pattern="^browse_")) + + # Настройка обработчиков сигналов для корректного завершения + signal.signal(signal.SIGINT, lambda sig, frame: signal_handler(sig, frame, application)) + signal.signal(signal.SIGTERM, lambda sig, frame: signal_handler(sig, frame, application)) + + # Запуск бота + logger.info("Bot started. Press Ctrl+C to stop.") + application.run_polling(allowed_updates=["message", "callback_query"]) + +if __name__ == "__main__": + main() diff --git a/.history/src/bot_20250830100755.py b/.history/src/bot_20250830100755.py new file mode 100644 index 0000000..c4f475c --- /dev/null +++ b/.history/src/bot_20250830100755.py @@ -0,0 +1,142 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Главный модуль запуска телеграм-бота для управления Synology NAS +""" + +import os +import signal +import sys +import asyncio +import logging +from telegram.ext import ( + Application, + CommandHandler, + CallbackQueryHandler, + MessageHandler, + filters +) + +from src.config.config import TELEGRAM_TOKEN +from src.handlers.help_handlers import ( + start_command, + help_command +) +from src.handlers.command_handlers import ( + status_command, + power_command, + power_callback +) +from src.handlers.extended_handlers import ( + storage_command, + shares_command, + system_command, + load_command, + security_command, + check_api_command +) +from src.handlers.advanced_handlers import ( + processes_command, + network_command, + temperature_command, + schedule_command, + browse_command, + search_command, + updates_command, + backup_command, + quickreboot_command, + reboot_command, + sleep_command, + wakeup_command, + quota_command, + schedule_callback, + browse_callback, + advanced_power_callback +) +from src.utils.logger import setup_logging + +async def shutdown(application: Application) -> None: + """Корректное завершение работы бота""" + logger = logging.getLogger(__name__) + logger.info("Stopping Synology Power Control Bot...") + + # Останавливаем прием обновлений + await application.stop() + logger.info("Bot stopped successfully") + +def signal_handler(sig, frame, application=None): + """Обработчик сигналов для корректного завершения""" + logger = logging.getLogger(__name__) + logger.info(f"Received signal {sig}, shutting down gracefully") + + if application: + # Создаем и запускаем задачу завершения в event loop + loop = asyncio.get_event_loop() + if loop.is_running(): + loop.create_task(shutdown(application)) + else: + loop.run_until_complete(shutdown(application)) + + sys.exit(0) + +def main() -> None: + """Основная функция запуска бота""" + # Настройка логирования + setup_logging() + logger = logging.getLogger(__name__) + + # Проверка наличия токена + if not TELEGRAM_TOKEN: + logger.error("Telegram token is not set! Please configure TELEGRAM_TOKEN in .env file.") + return + + # Создание и настройка приложения бота + logger.info("Starting Synology Power Control Bot") + application = Application.builder().token(TELEGRAM_TOKEN).build() + + # Регистрация обработчиков команд + application.add_handler(CommandHandler("start", start_command)) + application.add_handler(CommandHandler("help", help_command)) + application.add_handler(CommandHandler("status", status_command)) + application.add_handler(CommandHandler("power", power_command)) + + # Регистрация расширенных обработчиков команд + application.add_handler(CommandHandler("storage", storage_command)) + application.add_handler(CommandHandler("shares", shares_command)) + application.add_handler(CommandHandler("system", system_command)) + application.add_handler(CommandHandler("load", load_command)) + application.add_handler(CommandHandler("security", security_command)) + application.add_handler(CommandHandler("checkapi", check_api_command)) + + # Регистрация продвинутых обработчиков команд + application.add_handler(CommandHandler("processes", processes_command)) + application.add_handler(CommandHandler("network", network_command)) + application.add_handler(CommandHandler("temperature", temperature_command)) + application.add_handler(CommandHandler("schedule", schedule_command)) + application.add_handler(CommandHandler("browse", browse_command)) + application.add_handler(CommandHandler("search", search_command)) + application.add_handler(CommandHandler("updates", updates_command)) + application.add_handler(CommandHandler("backup", backup_command)) + application.add_handler(CommandHandler("quickreboot", quickreboot_command)) + application.add_handler(CommandHandler("reboot", reboot_command)) + application.add_handler(CommandHandler("sleep", sleep_command)) + application.add_handler(CommandHandler("wakeup", wakeup_command)) + application.add_handler(CommandHandler("quota", quota_command)) + + # Регистрация обработчиков callback-запросов + application.add_handler(CallbackQueryHandler(power_callback, pattern="^power_|^reboot$|^cancel$")) # Для command_handlers.py + application.add_handler(CallbackQueryHandler(advanced_power_callback, pattern="^(confirm|cancel)_(reboot|sleep)$")) # Для advanced_handlers.py + application.add_handler(CallbackQueryHandler(schedule_callback, pattern="^schedule_")) + application.add_handler(CallbackQueryHandler(browse_callback, pattern="^browse_")) + + # Настройка обработчиков сигналов для корректного завершения + signal.signal(signal.SIGINT, lambda sig, frame: signal_handler(sig, frame, application)) + signal.signal(signal.SIGTERM, lambda sig, frame: signal_handler(sig, frame, application)) + + # Запуск бота + logger.info("Bot started. Press Ctrl+C to stop.") + application.run_polling(allowed_updates=["message", "callback_query"]) + +if __name__ == "__main__": + main() diff --git a/.history/src/bot_20250830100926.py b/.history/src/bot_20250830100926.py new file mode 100644 index 0000000..54d7e0e --- /dev/null +++ b/.history/src/bot_20250830100926.py @@ -0,0 +1,144 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Главный модуль запуска телеграм-бота для управления Synology NAS +""" + +import os +import signal +import sys +import asyncio +import logging +from telegram.ext import ( + Application, + CommandHandler, + CallbackQueryHandler, + MessageHandler, + filters +) + +from src.config.config import TELEGRAM_TOKEN +from src.handlers.help_handlers import ( + start_command, + help_command +) +from src.handlers.command_handlers import ( + status_command, + power_command, + power_callback +) +from src.handlers.extended_handlers import ( + storage_command, + shares_command, + system_command, + load_command, + security_command, + check_api_command +) +from src.handlers.advanced_handlers import ( + processes_command, + network_command, + temperature_command, + schedule_command, + browse_command, + search_command, + updates_command, + backup_command, + quickreboot_command, + reboot_command, + sleep_command, + wakeup_command, + quota_command, + schedule_callback, + browse_callback, + advanced_power_callback +) +from src.utils.logger import setup_logging + +async def shutdown(application: Application) -> None: + """Корректное завершение работы бота""" + logger = logging.getLogger(__name__) + logger.info("Stopping Synology Power Control Bot...") + + # Останавливаем прием обновлений + await application.stop() + logger.info("Bot stopped successfully") + +def signal_handler(sig, frame, application=None): + """Обработчик сигналов для корректного завершения""" + logger = logging.getLogger(__name__) + logger.info(f"Received signal {sig}, shutting down gracefully") + + if application: + # Создаем и запускаем задачу завершения в event loop + loop = asyncio.get_event_loop() + if loop.is_running(): + loop.create_task(shutdown(application)) + else: + loop.run_until_complete(shutdown(application)) + + sys.exit(0) + +def main() -> None: + """Основная функция запуска бота""" + # Настройка логирования + setup_logging() + logger = logging.getLogger(__name__) + + # Проверка наличия токена + if not TELEGRAM_TOKEN: + logger.error("Telegram token is not set! Please configure TELEGRAM_TOKEN in .env file.") + return + + # Создание и настройка приложения бота + logger.info("Starting Synology Power Control Bot") + application = Application.builder().token(TELEGRAM_TOKEN).build() + + # Регистрация обработчиков команд + application.add_handler(CommandHandler("start", start_command)) + application.add_handler(CommandHandler("help", help_command)) + application.add_handler(CommandHandler("status", status_command)) + application.add_handler(CommandHandler("power", power_command)) + + # Регистрация расширенных обработчиков команд + application.add_handler(CommandHandler("storage", storage_command)) + application.add_handler(CommandHandler("shares", shares_command)) + application.add_handler(CommandHandler("system", system_command)) + application.add_handler(CommandHandler("load", load_command)) + application.add_handler(CommandHandler("security", security_command)) + application.add_handler(CommandHandler("checkapi", check_api_command)) + + # Регистрация продвинутых обработчиков команд + application.add_handler(CommandHandler("processes", processes_command)) + application.add_handler(CommandHandler("network", network_command)) + application.add_handler(CommandHandler("temperature", temperature_command)) + application.add_handler(CommandHandler("schedule", schedule_command)) + application.add_handler(CommandHandler("browse", browse_command)) + application.add_handler(CommandHandler("search", search_command)) + application.add_handler(CommandHandler("updates", updates_command)) + application.add_handler(CommandHandler("backup", backup_command)) + application.add_handler(CommandHandler("quickreboot", quickreboot_command)) + application.add_handler(CommandHandler("reboot", reboot_command)) + application.add_handler(CommandHandler("sleep", sleep_command)) + application.add_handler(CommandHandler("wakeup", wakeup_command)) + application.add_handler(CommandHandler("quota", quota_command)) + + # Регистрация обработчиков callback-запросов + # Сначала обрабатываем более специфичные паттерны + application.add_handler(CallbackQueryHandler(advanced_power_callback, pattern="^(confirm|cancel)_(reboot|sleep)$")) # Для advanced_handlers.py + # Затем более общие паттерны + application.add_handler(CallbackQueryHandler(power_callback, pattern="^power_|^reboot$|^cancel$")) # Для command_handlers.py + application.add_handler(CallbackQueryHandler(schedule_callback, pattern="^schedule_")) + application.add_handler(CallbackQueryHandler(browse_callback, pattern="^browse_")) + + # Настройка обработчиков сигналов для корректного завершения + signal.signal(signal.SIGINT, lambda sig, frame: signal_handler(sig, frame, application)) + signal.signal(signal.SIGTERM, lambda sig, frame: signal_handler(sig, frame, application)) + + # Запуск бота + logger.info("Bot started. Press Ctrl+C to stop.") + application.run_polling(allowed_updates=["message", "callback_query"]) + +if __name__ == "__main__": + main() diff --git a/.history/src/bot_20250830101843.py b/.history/src/bot_20250830101843.py new file mode 100644 index 0000000..54d7e0e --- /dev/null +++ b/.history/src/bot_20250830101843.py @@ -0,0 +1,144 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Главный модуль запуска телеграм-бота для управления Synology NAS +""" + +import os +import signal +import sys +import asyncio +import logging +from telegram.ext import ( + Application, + CommandHandler, + CallbackQueryHandler, + MessageHandler, + filters +) + +from src.config.config import TELEGRAM_TOKEN +from src.handlers.help_handlers import ( + start_command, + help_command +) +from src.handlers.command_handlers import ( + status_command, + power_command, + power_callback +) +from src.handlers.extended_handlers import ( + storage_command, + shares_command, + system_command, + load_command, + security_command, + check_api_command +) +from src.handlers.advanced_handlers import ( + processes_command, + network_command, + temperature_command, + schedule_command, + browse_command, + search_command, + updates_command, + backup_command, + quickreboot_command, + reboot_command, + sleep_command, + wakeup_command, + quota_command, + schedule_callback, + browse_callback, + advanced_power_callback +) +from src.utils.logger import setup_logging + +async def shutdown(application: Application) -> None: + """Корректное завершение работы бота""" + logger = logging.getLogger(__name__) + logger.info("Stopping Synology Power Control Bot...") + + # Останавливаем прием обновлений + await application.stop() + logger.info("Bot stopped successfully") + +def signal_handler(sig, frame, application=None): + """Обработчик сигналов для корректного завершения""" + logger = logging.getLogger(__name__) + logger.info(f"Received signal {sig}, shutting down gracefully") + + if application: + # Создаем и запускаем задачу завершения в event loop + loop = asyncio.get_event_loop() + if loop.is_running(): + loop.create_task(shutdown(application)) + else: + loop.run_until_complete(shutdown(application)) + + sys.exit(0) + +def main() -> None: + """Основная функция запуска бота""" + # Настройка логирования + setup_logging() + logger = logging.getLogger(__name__) + + # Проверка наличия токена + if not TELEGRAM_TOKEN: + logger.error("Telegram token is not set! Please configure TELEGRAM_TOKEN in .env file.") + return + + # Создание и настройка приложения бота + logger.info("Starting Synology Power Control Bot") + application = Application.builder().token(TELEGRAM_TOKEN).build() + + # Регистрация обработчиков команд + application.add_handler(CommandHandler("start", start_command)) + application.add_handler(CommandHandler("help", help_command)) + application.add_handler(CommandHandler("status", status_command)) + application.add_handler(CommandHandler("power", power_command)) + + # Регистрация расширенных обработчиков команд + application.add_handler(CommandHandler("storage", storage_command)) + application.add_handler(CommandHandler("shares", shares_command)) + application.add_handler(CommandHandler("system", system_command)) + application.add_handler(CommandHandler("load", load_command)) + application.add_handler(CommandHandler("security", security_command)) + application.add_handler(CommandHandler("checkapi", check_api_command)) + + # Регистрация продвинутых обработчиков команд + application.add_handler(CommandHandler("processes", processes_command)) + application.add_handler(CommandHandler("network", network_command)) + application.add_handler(CommandHandler("temperature", temperature_command)) + application.add_handler(CommandHandler("schedule", schedule_command)) + application.add_handler(CommandHandler("browse", browse_command)) + application.add_handler(CommandHandler("search", search_command)) + application.add_handler(CommandHandler("updates", updates_command)) + application.add_handler(CommandHandler("backup", backup_command)) + application.add_handler(CommandHandler("quickreboot", quickreboot_command)) + application.add_handler(CommandHandler("reboot", reboot_command)) + application.add_handler(CommandHandler("sleep", sleep_command)) + application.add_handler(CommandHandler("wakeup", wakeup_command)) + application.add_handler(CommandHandler("quota", quota_command)) + + # Регистрация обработчиков callback-запросов + # Сначала обрабатываем более специфичные паттерны + application.add_handler(CallbackQueryHandler(advanced_power_callback, pattern="^(confirm|cancel)_(reboot|sleep)$")) # Для advanced_handlers.py + # Затем более общие паттерны + application.add_handler(CallbackQueryHandler(power_callback, pattern="^power_|^reboot$|^cancel$")) # Для command_handlers.py + application.add_handler(CallbackQueryHandler(schedule_callback, pattern="^schedule_")) + application.add_handler(CallbackQueryHandler(browse_callback, pattern="^browse_")) + + # Настройка обработчиков сигналов для корректного завершения + signal.signal(signal.SIGINT, lambda sig, frame: signal_handler(sig, frame, application)) + signal.signal(signal.SIGTERM, lambda sig, frame: signal_handler(sig, frame, application)) + + # Запуск бота + logger.info("Bot started. Press Ctrl+C to stop.") + application.run_polling(allowed_updates=["message", "callback_query"]) + +if __name__ == "__main__": + main() diff --git a/.history/src/config/config_20250830063519.py b/.history/src/config/config_20250830063519.py new file mode 100644 index 0000000..70c1dbe --- /dev/null +++ b/.history/src/config/config_20250830063519.py @@ -0,0 +1,28 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Конфигурационный файл для телеграм-бота управления Synology NAS +""" + +import os +from dotenv import load_dotenv + +# Загрузка переменных окружения из .env файла +load_dotenv() + +# Конфигурация для Telegram бота +TELEGRAM_TOKEN = os.getenv("TELEGRAM_TOKEN") +ADMIN_USER_IDS = list(map(int, os.getenv("ADMIN_USER_IDS", "").split(","))) if os.getenv("ADMIN_USER_IDS") else [] + +# Конфигурация для Synology NAS +SYNOLOGY_HOST = os.getenv("SYNOLOGY_HOST") +SYNOLOGY_PORT = int(os.getenv("SYNOLOGY_PORT", 5000)) +SYNOLOGY_USERNAME = os.getenv("SYNOLOGY_USERNAME") +SYNOLOGY_PASSWORD = os.getenv("SYNOLOGY_PASSWORD") +SYNOLOGY_SECURE = os.getenv("SYNOLOGY_SECURE", "False").lower() == "true" +SYNOLOGY_TIMEOUT = int(os.getenv("SYNOLOGY_TIMEOUT", 10)) + +# Конфигурация для Wake-on-LAN +SYNOLOGY_MAC = os.getenv("SYNOLOGY_MAC") +WOL_PORT = int(os.getenv("WOL_PORT", 9)) diff --git a/.history/src/config/config_20250830063839.py b/.history/src/config/config_20250830063839.py new file mode 100644 index 0000000..70c1dbe --- /dev/null +++ b/.history/src/config/config_20250830063839.py @@ -0,0 +1,28 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Конфигурационный файл для телеграм-бота управления Synology NAS +""" + +import os +from dotenv import load_dotenv + +# Загрузка переменных окружения из .env файла +load_dotenv() + +# Конфигурация для Telegram бота +TELEGRAM_TOKEN = os.getenv("TELEGRAM_TOKEN") +ADMIN_USER_IDS = list(map(int, os.getenv("ADMIN_USER_IDS", "").split(","))) if os.getenv("ADMIN_USER_IDS") else [] + +# Конфигурация для Synology NAS +SYNOLOGY_HOST = os.getenv("SYNOLOGY_HOST") +SYNOLOGY_PORT = int(os.getenv("SYNOLOGY_PORT", 5000)) +SYNOLOGY_USERNAME = os.getenv("SYNOLOGY_USERNAME") +SYNOLOGY_PASSWORD = os.getenv("SYNOLOGY_PASSWORD") +SYNOLOGY_SECURE = os.getenv("SYNOLOGY_SECURE", "False").lower() == "true" +SYNOLOGY_TIMEOUT = int(os.getenv("SYNOLOGY_TIMEOUT", 10)) + +# Конфигурация для Wake-on-LAN +SYNOLOGY_MAC = os.getenv("SYNOLOGY_MAC") +WOL_PORT = int(os.getenv("WOL_PORT", 9)) diff --git a/.history/src/config/config_20250830082127.py b/.history/src/config/config_20250830082127.py new file mode 100644 index 0000000..b81c052 --- /dev/null +++ b/.history/src/config/config_20250830082127.py @@ -0,0 +1,29 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Конфигурационный файл для телеграм-бота управления Synology NAS +""" + +import os +from dotenv import load_dotenv + +# Загрузка переменных окружения из .env файла +load_dotenv() + +# Конфигурация для Telegram бота +TELEGRAM_TOKEN = os.getenv("TELEGRAM_TOKEN") +ADMIN_USER_IDS = list(map(int, os.getenv("ADMIN_USER_IDS", "").split(","))) if os.getenv("ADMIN_USER_IDS") else [] + +# Конфигурация для Synology NAS +SYNOLOGY_HOST = os.getenv("SYNOLOGY_HOST") +SYNOLOGY_PORT = int(os.getenv("SYNOLOGY_PORT", 5000)) +SYNOLOGY_USERNAME = os.getenv("SYNOLOGY_USERNAME") +SYNOLOGY_PASSWORD = os.getenv("SYNOLOGY_PASSWORD") +SYNOLOGY_SECURE = os.getenv("SYNOLOGY_SECURE", "False").lower() == "true" +SYNOLOGY_TIMEOUT = int(os.getenv("SYNOLOGY_TIMEOUT", 10)) +SYNOLOGY_API_VERSION = int(os.getenv("SYNOLOGY_API_VERSION", 1)) + +# Конфигурация для Wake-on-LAN +SYNOLOGY_MAC = os.getenv("SYNOLOGY_MAC") +WOL_PORT = int(os.getenv("WOL_PORT", 9)) diff --git a/.history/src/config/config_20250830082144.py b/.history/src/config/config_20250830082144.py new file mode 100644 index 0000000..b81c052 --- /dev/null +++ b/.history/src/config/config_20250830082144.py @@ -0,0 +1,29 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Конфигурационный файл для телеграм-бота управления Synology NAS +""" + +import os +from dotenv import load_dotenv + +# Загрузка переменных окружения из .env файла +load_dotenv() + +# Конфигурация для Telegram бота +TELEGRAM_TOKEN = os.getenv("TELEGRAM_TOKEN") +ADMIN_USER_IDS = list(map(int, os.getenv("ADMIN_USER_IDS", "").split(","))) if os.getenv("ADMIN_USER_IDS") else [] + +# Конфигурация для Synology NAS +SYNOLOGY_HOST = os.getenv("SYNOLOGY_HOST") +SYNOLOGY_PORT = int(os.getenv("SYNOLOGY_PORT", 5000)) +SYNOLOGY_USERNAME = os.getenv("SYNOLOGY_USERNAME") +SYNOLOGY_PASSWORD = os.getenv("SYNOLOGY_PASSWORD") +SYNOLOGY_SECURE = os.getenv("SYNOLOGY_SECURE", "False").lower() == "true" +SYNOLOGY_TIMEOUT = int(os.getenv("SYNOLOGY_TIMEOUT", 10)) +SYNOLOGY_API_VERSION = int(os.getenv("SYNOLOGY_API_VERSION", 1)) + +# Конфигурация для Wake-on-LAN +SYNOLOGY_MAC = os.getenv("SYNOLOGY_MAC") +WOL_PORT = int(os.getenv("WOL_PORT", 9)) diff --git a/.history/src/config/config_20250830082223.py b/.history/src/config/config_20250830082223.py new file mode 100644 index 0000000..79d954b --- /dev/null +++ b/.history/src/config/config_20250830082223.py @@ -0,0 +1,31 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Конфигурационный файл для телеграм-бота управления Synology NAS +""" + +import os +from dotenv import load_dotenv + +# Загрузка переменных окружения из .env файла +load_dotenv() + +# Конфигурация для Telegram бота +TELEGRAM_TOKEN = os.getenv("TELEGRAM_TOKEN") +ADMIN_USER_IDS = list(map(int, os.getenv("ADMIN_USER_IDS", "").split(","))) if os.getenv("ADMIN_USER_IDS") else [] + +# Конфигурация для Synology NAS +SYNOLOGY_HOST = os.getenv("SYNOLOGY_HOST") +SYNOLOGY_PORT = int(os.getenv("SYNOLOGY_PORT", 5000)) +SYNOLOGY_USERNAME = os.getenv("SYNOLOGY_USERNAME") +SYNOLOGY_PASSWORD = os.getenv("SYNOLOGY_PASSWORD") +SYNOLOGY_SECURE = os.getenv("SYNOLOGY_SECURE", "False").lower() == "true" +SYNOLOGY_TIMEOUT = int(os.getenv("SYNOLOGY_TIMEOUT", 10)) +SYNOLOGY_API_VERSION = int(os.getenv("SYNOLOGY_API_VERSION", 1)) +SYNOLOGY_POWER_API = os.getenv("SYNOLOGY_POWER_API", "SYNO.Core.Hardware.PowerRecovery") +SYNOLOGY_INFO_API = os.getenv("SYNOLOGY_INFO_API", "SYNO.DSM.Info") + +# Конфигурация для Wake-on-LAN +SYNOLOGY_MAC = os.getenv("SYNOLOGY_MAC") +WOL_PORT = int(os.getenv("WOL_PORT", 9)) diff --git a/.history/src/config/config_20250830082500.py b/.history/src/config/config_20250830082500.py new file mode 100644 index 0000000..79d954b --- /dev/null +++ b/.history/src/config/config_20250830082500.py @@ -0,0 +1,31 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Конфигурационный файл для телеграм-бота управления Synology NAS +""" + +import os +from dotenv import load_dotenv + +# Загрузка переменных окружения из .env файла +load_dotenv() + +# Конфигурация для Telegram бота +TELEGRAM_TOKEN = os.getenv("TELEGRAM_TOKEN") +ADMIN_USER_IDS = list(map(int, os.getenv("ADMIN_USER_IDS", "").split(","))) if os.getenv("ADMIN_USER_IDS") else [] + +# Конфигурация для Synology NAS +SYNOLOGY_HOST = os.getenv("SYNOLOGY_HOST") +SYNOLOGY_PORT = int(os.getenv("SYNOLOGY_PORT", 5000)) +SYNOLOGY_USERNAME = os.getenv("SYNOLOGY_USERNAME") +SYNOLOGY_PASSWORD = os.getenv("SYNOLOGY_PASSWORD") +SYNOLOGY_SECURE = os.getenv("SYNOLOGY_SECURE", "False").lower() == "true" +SYNOLOGY_TIMEOUT = int(os.getenv("SYNOLOGY_TIMEOUT", 10)) +SYNOLOGY_API_VERSION = int(os.getenv("SYNOLOGY_API_VERSION", 1)) +SYNOLOGY_POWER_API = os.getenv("SYNOLOGY_POWER_API", "SYNO.Core.Hardware.PowerRecovery") +SYNOLOGY_INFO_API = os.getenv("SYNOLOGY_INFO_API", "SYNO.DSM.Info") + +# Конфигурация для Wake-on-LAN +SYNOLOGY_MAC = os.getenv("SYNOLOGY_MAC") +WOL_PORT = int(os.getenv("WOL_PORT", 9)) diff --git a/.history/src/config/config_20250830100958.py b/.history/src/config/config_20250830100958.py new file mode 100644 index 0000000..17840bf --- /dev/null +++ b/.history/src/config/config_20250830100958.py @@ -0,0 +1,31 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Конфигурационный файл для телеграм-бота управления Synology NAS +""" + +import os +from dotenv import load_dotenv + +# Загрузка переменных окружения из .env файла +load_dotenv() + +# Конфигурация для Telegram бота +TELEGRAM_TOKEN = os.getenv("TELEGRAM_TOKEN") +ADMIN_USER_IDS = list(map(int, os.getenv("ADMIN_USER_IDS", "").split(","))) if os.getenv("ADMIN_USER_IDS") else [] + +# Конфигурация для Synology NAS +SYNOLOGY_HOST = os.getenv("SYNOLOGY_HOST") +SYNOLOGY_PORT = int(os.getenv("SYNOLOGY_PORT", 5000)) +SYNOLOGY_USERNAME = os.getenv("SYNOLOGY_USERNAME") +SYNOLOGY_PASSWORD = os.getenv("SYNOLOGY_PASSWORD") +SYNOLOGY_SECURE = os.getenv("SYNOLOGY_SECURE", "False").lower() == "true" +SYNOLOGY_TIMEOUT = int(os.getenv("SYNOLOGY_TIMEOUT", 10)) +SYNOLOGY_API_VERSION = int(os.getenv("SYNOLOGY_API_VERSION", 1)) +SYNOLOGY_POWER_API = os.getenv("SYNOLOGY_POWER_API", "SYNO.Core.System") +SYNOLOGY_INFO_API = os.getenv("SYNOLOGY_INFO_API", "SYNO.DSM.Info") + +# Конфигурация для Wake-on-LAN +SYNOLOGY_MAC = os.getenv("SYNOLOGY_MAC") +WOL_PORT = int(os.getenv("WOL_PORT", 9)) diff --git a/.history/src/config/config_20250830101843.py b/.history/src/config/config_20250830101843.py new file mode 100644 index 0000000..17840bf --- /dev/null +++ b/.history/src/config/config_20250830101843.py @@ -0,0 +1,31 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Конфигурационный файл для телеграм-бота управления Synology NAS +""" + +import os +from dotenv import load_dotenv + +# Загрузка переменных окружения из .env файла +load_dotenv() + +# Конфигурация для Telegram бота +TELEGRAM_TOKEN = os.getenv("TELEGRAM_TOKEN") +ADMIN_USER_IDS = list(map(int, os.getenv("ADMIN_USER_IDS", "").split(","))) if os.getenv("ADMIN_USER_IDS") else [] + +# Конфигурация для Synology NAS +SYNOLOGY_HOST = os.getenv("SYNOLOGY_HOST") +SYNOLOGY_PORT = int(os.getenv("SYNOLOGY_PORT", 5000)) +SYNOLOGY_USERNAME = os.getenv("SYNOLOGY_USERNAME") +SYNOLOGY_PASSWORD = os.getenv("SYNOLOGY_PASSWORD") +SYNOLOGY_SECURE = os.getenv("SYNOLOGY_SECURE", "False").lower() == "true" +SYNOLOGY_TIMEOUT = int(os.getenv("SYNOLOGY_TIMEOUT", 10)) +SYNOLOGY_API_VERSION = int(os.getenv("SYNOLOGY_API_VERSION", 1)) +SYNOLOGY_POWER_API = os.getenv("SYNOLOGY_POWER_API", "SYNO.Core.System") +SYNOLOGY_INFO_API = os.getenv("SYNOLOGY_INFO_API", "SYNO.DSM.Info") + +# Конфигурация для Wake-on-LAN +SYNOLOGY_MAC = os.getenv("SYNOLOGY_MAC") +WOL_PORT = int(os.getenv("WOL_PORT", 9)) diff --git a/.history/src/handlers/advanced_handlers_20250830091501.py b/.history/src/handlers/advanced_handlers_20250830091501.py new file mode 100644 index 0000000..02cef9d --- /dev/null +++ b/.history/src/handlers/advanced_handlers_20250830091501.py @@ -0,0 +1,864 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Расширенные обработчики команд для управления Synology NAS +""" + +import logging +from datetime import datetime +from typing import List, Dict, Any +from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup +from telegram.ext import ContextTypes + +from src.config.config import ADMIN_USER_IDS +from src.api.synology import SynologyAPI + +logger = logging.getLogger(__name__) + +# Инициализация API Synology +synology_api = SynologyAPI() + +async def processes_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /processes для получения списка активных процессов""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + message = await update.message.reply_text("⏳ Получение информации о запущенных процессах...") + + if not synology_api.is_online(): + await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о процессах.", parse_mode="HTML") + return + + try: + processes = synology_api.get_processes(limit=15) # Получаем топ-15 процессов + + if not processes: + await message.edit_text("❌ Ошибка получения информации о процессах\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML") + return + except Exception as e: + await message.edit_text(f"❌ Ошибка получения информации о процессах\n\nПричина: {str(e)}", parse_mode="HTML") + return + + # Формируем сообщение о процессах + reply_text = f"⚙️ Активные процессы Synology NAS\n\n" + + for process in processes: + name = process.get("name", "unknown") + pid = process.get("pid", "?") + cpu_usage = process.get("cpu_usage", 0) + memory_usage = process.get("memory_usage", 0) + + reply_text += f"• {name} (PID: {pid})\n" + reply_text += f" └ CPU: {cpu_usage:.1f}%, Память: {memory_usage:.1f}%\n" + + reply_text += f"\nПоказано {len(processes)} наиболее активных процессов" + await message.edit_text(reply_text, parse_mode="HTML") + +async def network_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /network для получения информации о сетевых подключениях""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + message = await update.message.reply_text("⏳ Получение информации о сетевых подключениях...") + + if not synology_api.is_online(): + await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о сетевых подключениях.", parse_mode="HTML") + return + + try: + network_status = synology_api.get_network_status() + + if not network_status: + await message.edit_text("❌ Ошибка получения информации о сетевых подключениях\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML") + return + except Exception as e: + await message.edit_text(f"❌ Ошибка получения информации о сетевых подключениях\n\nПричина: {str(e)}", parse_mode="HTML") + return + + # Формируем сообщение о сетевых интерфейсах + interfaces = network_status.get("interfaces", []) + + reply_text = f"🌐 Сетевые подключения Synology NAS\n\n" + + for interface in interfaces: + name = interface.get("id", "unknown") + ip = interface.get("ip", "Нет данных") + mac = interface.get("mac", "Нет данных") + status = "Активен" if interface.get("status") else "Неактивен" + + # Информация о трафике + rx_bytes = interface.get("rx", 0) / (1024**2) # Перевод в МБ + tx_bytes = interface.get("tx", 0) / (1024**2) # Перевод в МБ + + reply_text += f"• {name} ({status})\n" + reply_text += f" └ IP: {ip}, MAC: {mac}\n" + + if rx_bytes > 0 or tx_bytes > 0: + reply_text += f" └ Получено: {rx_bytes:.2f} МБ, Отправлено: {tx_bytes:.2f} МБ\n" + + await message.edit_text(reply_text, parse_mode="HTML") + +async def temperature_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /temperature для мониторинга температуры""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + message = await update.message.reply_text("⏳ Получение информации о температуре...") + + if not synology_api.is_online(): + await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о температуре.", parse_mode="HTML") + return + + try: + temp_status = synology_api.get_temperature_status() + + if not temp_status: + await message.edit_text("❌ Ошибка получения информации о температуре\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML") + return + except Exception as e: + await message.edit_text(f"❌ Ошибка получения информации о температуре\n\nПричина: {str(e)}", parse_mode="HTML") + return + + # Формируем сообщение о температуре + system_temp = temp_status.get("system_temperature") + disk_temps = temp_status.get("disk_temperatures", []) + is_warning = temp_status.get("warning", False) + + # Выбор emoji в зависимости от температуры + temp_emoji = "🔥" if is_warning else "🌡️" + + reply_text = f"{temp_emoji} Температура Synology NAS\n\n" + + if system_temp is not None: + temp_status_text = "❗ ПОВЫШЕННАЯ" if is_warning else "✅ Нормальная" + reply_text += f"Температура системы: {system_temp}°C ({temp_status_text})\n\n" + + if disk_temps: + reply_text += "Температура дисков:\n" + for disk in disk_temps: + name = disk.get("name", "unknown") + model = disk.get("model", "unknown") + temp = disk.get("temperature", 0) + + disk_temp_emoji = "🔥" if temp > 45 else "✅" + reply_text += f"• {disk_temp_emoji} {name} ({model}): {temp}°C\n" + + await message.edit_text(reply_text, parse_mode="HTML") + +async def schedule_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /schedule для управления расписанием питания""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + message = await update.message.reply_text("⏳ Получение информации о расписании питания...") + + if not synology_api.is_online(): + await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о расписании питания.", parse_mode="HTML") + return + + try: + schedule = synology_api.get_power_schedule() + + if not schedule: + await message.edit_text("❌ Ошибка получения информации о расписании питания\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML") + return + except Exception as e: + await message.edit_text(f"❌ Ошибка получения информации о расписании питания\n\nПричина: {str(e)}", parse_mode="HTML") + return + + # Формируем сообщение о расписании питания + boot_tasks = schedule.get("boot_tasks", []) + shutdown_tasks = schedule.get("shutdown_tasks", []) + + reply_text = f"⏱️ Расписание питания Synology NAS\n\n" + + if boot_tasks: + reply_text += "Расписание включения:\n" + for task in boot_tasks: + days = task.get("day", []) + time = task.get("time", "00:00") + enabled = task.get("enabled", False) + + # Преобразуем номера дней в названия + day_names = [] + for day in days: + if day == 0: day_names.append("Пн") + elif day == 1: day_names.append("Вт") + elif day == 2: day_names.append("Ср") + elif day == 3: day_names.append("Чт") + elif day == 4: day_names.append("Пт") + elif day == 5: day_names.append("Сб") + elif day == 6: day_names.append("Вс") + + status = "✅ Активно" if enabled else "❌ Отключено" + day_str = ", ".join(day_names) if day_names else "Нет дней" + + reply_text += f"• {status}: {time} ({day_str})\n" + else: + reply_text += "Расписание включения: Не настроено\n" + + reply_text += "\n" + + if shutdown_tasks: + reply_text += "Расписание выключения:\n" + for task in shutdown_tasks: + days = task.get("day", []) + time = task.get("time", "00:00") + enabled = task.get("enabled", False) + + # Преобразуем номера дней в названия + day_names = [] + for day in days: + if day == 0: day_names.append("Пн") + elif day == 1: day_names.append("Вт") + elif day == 2: day_names.append("Ср") + elif day == 3: day_names.append("Чт") + elif day == 4: day_names.append("Пт") + elif day == 5: day_names.append("Сб") + elif day == 6: day_names.append("Вс") + + status = "✅ Активно" if enabled else "❌ Отключено" + day_str = ", ".join(day_names) if day_names else "Нет дней" + + reply_text += f"• {status}: {time} ({day_str})\n" + else: + reply_text += "Расписание выключения: Не настроено\n" + + # Добавляем кнопки для управления расписанием + keyboard = [ + [ + InlineKeyboardButton("➕ Добавить включение", callback_data="schedule_add_boot"), + InlineKeyboardButton("➕ Добавить выключение", callback_data="schedule_add_shutdown") + ], + [ + InlineKeyboardButton("🗑️ Удалить расписание", callback_data="schedule_delete") + ] + ] + reply_markup = InlineKeyboardMarkup(keyboard) + + await message.edit_text(reply_text, parse_mode="HTML", reply_markup=reply_markup) + +async def browse_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /browse для просмотра файлов""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + # Получаем путь из аргументов команды или используем корневую директорию + path = " ".join(context.args) if context.args else "" + + message = await update.message.reply_text(f"⏳ Получение содержимого {'папки ' + path if path else 'общих папок'}...") + + if not synology_api.is_online(): + await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить список файлов.", parse_mode="HTML") + return + + try: + browse_result = synology_api.browse_files(folder_path=path) + + if not browse_result.get("success", False): + error = browse_result.get("error", "unknown") + await message.edit_text(f"❌ Ошибка получения списка файлов\n\nПричина: {error}", parse_mode="HTML") + return + + items = browse_result.get("items", []) + current_path = browse_result.get("path", "") + is_root = browse_result.get("is_root", True) + + except Exception as e: + await message.edit_text(f"❌ Ошибка получения списка файлов\n\nПричина: {str(e)}", parse_mode="HTML") + return + + # Формируем сообщение о файлах и папках + if is_root: + reply_text = f"📁 Общие папки Synology NAS\n\n" + else: + reply_text = f"📁 Содержимое папки\n{current_path}\n\n" + + # Сортируем: сначала папки, потом файлы + folders = [] + files = [] + + for item in items: + if is_root: # Для корневого уровня все элементы - это общие папки + name = item.get("name", "unknown") + path = item.get("path", "") + folders.append((name, path, True)) + else: + name = item.get("name", "unknown") + path = item.get("path", "") + is_dir = item.get("isdir", False) + + if is_dir: + folders.append((name, path, False)) + else: + # Для файлов получаем размер + size = item.get("additional", {}).get("size", 0) + size_str = format_size(size) + files.append((name, path, size_str)) + + # Добавляем папки в сообщение + if folders: + for name, path, is_share in folders: + # Для общих папок добавляем иконку дома + icon = "🏠" if is_share else "📁" + reply_text += f"{icon} {name}\n" + + # Добавляем файлы в сообщение + if files: + for name, path, size in files: + # Выбираем иконку в зависимости от расширения + icon = get_file_icon(name) + reply_text += f"{icon} {name} ({size})\n" + + # Если нет элементов для отображения + if not folders and not files: + reply_text += "📭 Папка пуста\n" + + # Добавляем кнопку возврата наверх, если мы не в корне + if not is_root: + # Определяем родительскую директорию + parent_path = "/".join(current_path.split("/")[:-1]) if "/" in current_path else "" + + keyboard = [ + [InlineKeyboardButton("🔼 Вернуться на уровень выше", callback_data=f"browse_{parent_path}")] + ] + reply_markup = InlineKeyboardMarkup(keyboard) + + await message.edit_text(reply_text, parse_mode="HTML", reply_markup=reply_markup) + else: + await message.edit_text(reply_text, parse_mode="HTML") + +async def search_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /search для поиска файлов""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + # Получаем шаблон поиска из аргументов команды + if not context.args: + await update.message.reply_text("❌ Укажите шаблон для поиска: /search <шаблон>") + return + + pattern = " ".join(context.args) + + message = await update.message.reply_text(f"⏳ Поиск файлов по шаблону «{pattern}»...") + + if not synology_api.is_online(): + await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно выполнить поиск файлов.", parse_mode="HTML") + return + + try: + search_result = synology_api.search_files(pattern=pattern, limit=20) + + if not search_result.get("success", False): + error = search_result.get("error", "unknown") + progress = search_result.get("progress", 0) + + if error == "search_timeout": + await message.edit_text(f"❌ Превышено время ожидания результатов поиска\n\nПроцесс поиска выполнен на {progress}%", parse_mode="HTML") + else: + await message.edit_text(f"❌ Ошибка при поиске файлов\n\nПричина: {error}", parse_mode="HTML") + return + + files = search_result.get("results", []) + total = search_result.get("total", len(files)) + + except Exception as e: + await message.edit_text(f"❌ Ошибка при поиске файлов\n\nПричина: {str(e)}", parse_mode="HTML") + return + + # Формируем сообщение с результатами поиска + reply_text = f"🔍 Результаты поиска по шаблону «{pattern}»\n\n" + + if not files: + reply_text += "📭 Файлы не найдены" + else: + # Сортируем: сначала папки, потом файлы + folders = [] + found_files = [] + + for item in files: + name = item.get("name", "unknown") + path = item.get("path", "") + is_dir = item.get("isdir", False) + + if is_dir: + folders.append((name, path)) + else: + # Для файлов получаем размер и путь к родительской папке + size = item.get("additional", {}).get("size", 0) + size_str = format_size(size) + parent_path = "/".join(path.split("/")[:-1]) + found_files.append((name, path, size_str, parent_path)) + + # Добавляем папки в сообщение + if folders: + reply_text += "Найденные папки:\n" + for name, path in folders[:5]: # Показываем первые 5 папок + reply_text += f"📁 {name}\n" + + if len(folders) > 5: + reply_text += f"...и еще {len(folders) - 5} папок\n" + + reply_text += "\n" + + # Добавляем файлы в сообщение + if found_files: + reply_text += "Найденные файлы:\n" + for name, path, size, parent in found_files[:10]: # Показываем первые 10 файлов + icon = get_file_icon(name) + reply_text += f"{icon} {name} ({size})\n" + reply_text += f" Путь: .../{path.split('/')[-2]}/\n" + + if len(found_files) > 10: + reply_text += f"...и еще {len(found_files) - 10} файлов\n" + + # Добавляем информацию о общем количестве результатов + reply_text += f"\nВсего найдено: {total} элементов" + + await message.edit_text(reply_text, parse_mode="HTML") + +async def updates_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /updates для проверки обновлений""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + message = await update.message.reply_text("⏳ Проверка доступных обновлений...") + + if not synology_api.is_online(): + await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно проверить наличие обновлений.", parse_mode="HTML") + return + + try: + update_info = synology_api.check_for_updates() + + if not update_info.get("success", False): + error = update_info.get("error", "unknown") + await message.edit_text(f"❌ Ошибка при проверке обновлений\n\nПричина: {error}", parse_mode="HTML") + return + + current_version = update_info.get("current_version", "unknown") + update_available = update_info.get("update_available", False) + auto_update = update_info.get("auto_update_enabled", False) + updates = update_info.get("updates", []) + + except Exception as e: + await message.edit_text(f"❌ Ошибка при проверке обновлений\n\nПричина: {str(e)}", parse_mode="HTML") + return + + # Формируем сообщение об обновлениях + if update_available: + reply_text = f"🔄 Доступны обновления DSM\n\n" + reply_text += f"Текущая версия: {current_version}\n" + reply_text += f"Автообновление: {'✅ Включено' if auto_update else '❌ Отключено'}\n\n" + reply_text += "Доступные обновления:\n" + + for update_item in updates: + update_name = update_item.get("name", "unknown") + update_version = update_item.get("version", "unknown") + update_size = update_item.get("size", 0) + update_size_str = format_size(update_size) + + reply_text += f"• {update_name} v{update_version}\n" + reply_text += f" └ Размер: {update_size_str}\n" + else: + reply_text = f"✅ Система в актуальном состоянии\n\n" + reply_text += f"Текущая версия: {current_version}\n" + reply_text += f"Автообновление: {'✅ Включено' if auto_update else '❌ Отключено'}\n" + + await message.edit_text(reply_text, parse_mode="HTML") + +async def backup_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /backup для управления резервным копированием""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + message = await update.message.reply_text("⏳ Получение информации о резервном копировании...") + + if not synology_api.is_online(): + await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о резервном копировании.", parse_mode="HTML") + return + + try: + backup_status = synology_api.get_backup_status() + + if not backup_status.get("success", False): + error = backup_status.get("error", "unknown") + await message.edit_text(f"❌ Ошибка получения информации о резервном копировании\n\nПричина: {error}", parse_mode="HTML") + return + + backups = backup_status.get("backups", {}) + api_status = backup_status.get("available_apis", {}) + + except Exception as e: + await message.edit_text(f"❌ Ошибка получения информации о резервном копировании\n\nПричина: {str(e)}", parse_mode="HTML") + return + + # Формируем сообщение о резервном копировании + reply_text = f"💾 Резервное копирование Synology NAS\n\n" + + # Информация о Hyper Backup + hyper_backups = backups.get("hyper_backup", []) + hyper_api_available = api_status.get("hyper_backup", False) + + if hyper_api_available: + reply_text += "Hyper Backup:\n" + + if hyper_backups: + for backup in hyper_backups: + name = backup.get("name", "unknown") + status = backup.get("status", "unknown") + last_backup = backup.get("last_backup", "never") + + status_emoji = "✅" if status.lower() == "success" else "⚠️" + reply_text += f"• {status_emoji} {name}\n" + reply_text += f" └ Последнее копирование: {last_backup}\n" + else: + reply_text += "Задачи Hyper Backup не настроены\n" + + reply_text += "\n" + + # Информация о Time Backup + time_backups = backups.get("time_backup", []) + time_api_available = api_status.get("time_backup", False) + + if time_api_available: + reply_text += "Time Backup:\n" + + if time_backups: + for backup in time_backups: + name = backup.get("name", "unknown") + status = backup.get("status", "unknown") + + status_emoji = "✅" if status.lower() == "normal" else "⚠️" + reply_text += f"• {status_emoji} {name}\n" + else: + reply_text += "Задачи Time Backup не настроены\n" + + reply_text += "\n" + + # Информация о USB Copy + usb_copy = backups.get("usb_copy", {}) + usb_api_available = api_status.get("usb_copy", False) + + if usb_api_available: + usb_enabled = usb_copy.get("enabled", False) + usb_status = "✅ Включено" if usb_enabled else "❌ Отключено" + + reply_text += f"USB Copy: {usb_status}\n\n" + + # Если ни один из API не доступен + if not any(api_status.values()): + reply_text += "API для резервного копирования не доступны на вашем NAS или требуют дополнительных прав доступа.\n" + + await message.edit_text(reply_text, parse_mode="HTML") + +async def quickreboot_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /quickreboot для быстрой перезагрузки NAS""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + message = await update.message.reply_text("⏳ Выполняется перезагрузка Synology NAS...") + + if not synology_api.is_online(): + await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно выполнить перезагрузку.", parse_mode="HTML") + return + + try: + # Выполняем перезагрузку + result = synology_api.reboot_system() + + if result: + # Формируем сообщение об успешной перезагрузке + reply_text = "🔄 Synology NAS перезагружается\n\n" + reply_text += "Перезагрузка обычно занимает 3-5 минут. После перезагрузки NAS будет снова доступен." + await message.edit_text(reply_text, parse_mode="HTML") + else: + await message.edit_text("❌ Ошибка при выполнении перезагрузки\n\nПроверьте журналы NAS для получения дополнительной информации.", parse_mode="HTML") + + except Exception as e: + await message.edit_text(f"❌ Ошибка при выполнении перезагрузки\n\nПричина: {str(e)}", parse_mode="HTML") + return + +async def wakeup_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /wakeup для включения NAS""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + message = await update.message.reply_text("⏳ Отправка пакета Wake-on-LAN для включения Synology NAS...") + + # Проверяем, не включен ли NAS уже + if synology_api.is_online(force_check=True): + await message.edit_text("ℹ️ Synology NAS уже включен\n\nНет необходимости отправлять сигнал пробуждения.", parse_mode="HTML") + return + + try: + # Отправляем сигнал пробуждения + result = synology_api.power_on() + + if result: + # Формируем сообщение об успешном включении + reply_text = "✅ Synology NAS успешно включен\n\n" + reply_text += "NAS полностью готов к работе." + await message.edit_text(reply_text, parse_mode="HTML") + else: + await message.edit_text("❌ Ошибка при включении NAS\n\nВозможные причины:\n- Функция Wake-on-LAN не настроена на NAS\n- Неверно указан MAC-адрес\n- Проблемы с сетевым подключением", parse_mode="HTML") + + except Exception as e: + await message.edit_text(f"❌ Ошибка при включении NAS\n\nПричина: {str(e)}", parse_mode="HTML") + return + +async def quota_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /quota для просмотра информации о квотах""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + message = await update.message.reply_text("⏳ Получение информации о квотах пользователей...") + + if not synology_api.is_online(): + await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о квотах.", parse_mode="HTML") + return + + try: + quota_info = synology_api.get_quota_info() + + if not quota_info.get("success", False): + error = quota_info.get("error", "unknown") + await message.edit_text(f"❌ Ошибка получения информации о квотах\n\nПричина: {error}", parse_mode="HTML") + return + + user_quotas = quota_info.get("user_quotas", []) + + except Exception as e: + await message.edit_text(f"❌ Ошибка получения информации о квотах\n\nПричина: {str(e)}", parse_mode="HTML") + return + + # Формируем сообщение о квотах + reply_text = f"📊 Квоты пользователей Synology NAS\n\n" + + if not user_quotas: + reply_text += "Квоты пользователей не настроены или недоступны" + else: + for user_quota in user_quotas: + user = user_quota.get("user", "unknown") + quotas = user_quota.get("quotas", []) + + if quotas: + reply_text += f"Пользователь {user}:\n" + + for quota in quotas: + volume = quota.get("volume_name", "unknown") + limit = quota.get("limit", 0) + used = quota.get("used", 0) + + # Переводим байты в ГБ + limit_gb = limit / (1024**3) if limit > 0 else 0 + used_gb = used / (1024**3) + + # Рассчитываем процент использования + if limit_gb > 0: + usage_percent = (used_gb / limit_gb) * 100 + reply_text += f"• Том {volume}: {used_gb:.2f} ГБ из {limit_gb:.2f} ГБ ({usage_percent:.1f}%)\n" + else: + reply_text += f"• Том {volume}: {used_gb:.2f} ГБ (без ограничений)\n" + + reply_text += "\n" + + await message.edit_text(reply_text, parse_mode="HTML") + +async def schedule_callback(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик callback-запросов для управления расписанием питания""" + query = update.callback_query + await query.answer() + + user_id = update.effective_user.id + if user_id not in ADMIN_USER_IDS: + await query.edit_message_text("У вас нет доступа к этому боту.") + return + + action = query.data + + if action.startswith("schedule_"): + action_type = action.split("_")[1] + + if action_type == "add_boot": + # Логика добавления расписания включения + # В реальном боте здесь будет диалог для настройки расписания + await query.edit_message_text("⚙️ Добавление расписания включения\n\nЭта функция находится в разработке.", parse_mode="HTML") + + elif action_type == "add_shutdown": + # Логика добавления расписания выключения + await query.edit_message_text("⚙️ Добавление расписания выключения\n\nЭта функция находится в разработке.", parse_mode="HTML") + + elif action_type == "delete": + # Логика удаления расписания + await query.edit_message_text("⚙️ Удаление расписания\n\nЭта функция находится в разработке.", parse_mode="HTML") + +async def browse_callback(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик callback-запросов для навигации по файловой системе""" + query = update.callback_query + await query.answer() + + user_id = update.effective_user.id + if user_id not in ADMIN_USER_IDS: + await query.edit_message_text("У вас нет доступа к этому боту.") + return + + action = query.data + + if action.startswith("browse_"): + path = action[7:] # Убираем префикс "browse_" + + # Используем команду browse с указанным путем + message = await query.edit_message_text(f"⏳ Получение содержимого {'папки ' + path if path else 'общих папок'}...") + + if not synology_api.is_online(): + await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить список файлов.", parse_mode="HTML") + return + + try: + browse_result = synology_api.browse_files(folder_path=path) + + if not browse_result.get("success", False): + error = browse_result.get("error", "unknown") + await message.edit_text(f"❌ Ошибка получения списка файлов\n\nПричина: {error}", parse_mode="HTML") + return + + items = browse_result.get("items", []) + current_path = browse_result.get("path", "") + is_root = browse_result.get("is_root", True) + + except Exception as e: + await message.edit_text(f"❌ Ошибка получения списка файлов\n\nПричина: {str(e)}", parse_mode="HTML") + return + + # Формируем сообщение о файлах и папках (аналогично функции browse_command) + if is_root: + reply_text = f"📁 Общие папки Synology NAS\n\n" + else: + reply_text = f"📁 Содержимое папки\n{current_path}\n\n" + + # Сортируем: сначала папки, потом файлы + folders = [] + files = [] + + for item in items: + if is_root: # Для корневого уровня все элементы - это общие папки + name = item.get("name", "unknown") + path = item.get("path", "") + folders.append((name, path, True)) + else: + name = item.get("name", "unknown") + path = item.get("path", "") + is_dir = item.get("isdir", False) + + if is_dir: + folders.append((name, path, False)) + else: + # Для файлов получаем размер + size = item.get("additional", {}).get("size", 0) + size_str = format_size(size) + files.append((name, path, size_str)) + + # Добавляем папки в сообщение + if folders: + for name, path, is_share in folders: + # Для общих папок добавляем иконку дома + icon = "🏠" if is_share else "📁" + reply_text += f"{icon} {name}\n" + + # Добавляем файлы в сообщение + if files: + for name, path, size in files: + # Выбираем иконку в зависимости от расширения + icon = get_file_icon(name) + reply_text += f"{icon} {name} ({size})\n" + + # Если нет элементов для отображения + if not folders and not files: + reply_text += "📭 Папка пуста\n" + + # Добавляем кнопку возврата наверх, если мы не в корне + if not is_root: + # Определяем родительскую директорию + parent_path = "/".join(current_path.split("/")[:-1]) if "/" in current_path else "" + + keyboard = [ + [InlineKeyboardButton("🔼 Вернуться на уровень выше", callback_data=f"browse_{parent_path}")] + ] + reply_markup = InlineKeyboardMarkup(keyboard) + + await message.edit_text(reply_text, parse_mode="HTML", reply_markup=reply_markup) + else: + await message.edit_text(reply_text, parse_mode="HTML") + +# Вспомогательные функции + +def format_size(size_bytes: int) -> str: + """Преобразует размер в байтах в человекочитаемый формат""" + if size_bytes < 1024: + return f"{size_bytes} Б" + elif size_bytes < 1024**2: + return f"{size_bytes/1024:.1f} КБ" + elif size_bytes < 1024**3: + return f"{size_bytes/1024**2:.1f} МБ" + else: + return f"{size_bytes/1024**3:.1f} ГБ" + +def get_file_icon(filename: str) -> str: + """Возвращает эмодзи-иконку в зависимости от типа файла""" + extension = filename.lower().split('.')[-1] if '.' in filename else '' + + if extension in ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'tiff', 'webp']: + return "🖼️" + elif extension in ['mp4', 'avi', 'mov', 'mkv', 'wmv', 'flv']: + return "🎬" + elif extension in ['mp3', 'wav', 'ogg', 'flac', 'aac']: + return "🎵" + elif extension in ['doc', 'docx', 'txt', 'rtf', 'odt']: + return "📄" + elif extension in ['xls', 'xlsx', 'csv']: + return "📊" + elif extension in ['ppt', 'pptx']: + return "📑" + elif extension in ['pdf']: + return "📕" + elif extension in ['zip', 'rar', '7z', 'tar', 'gz']: + return "🗜️" + elif extension in ['exe', 'msi']: + return "⚙️" + else: + return "📄" diff --git a/.history/src/handlers/advanced_handlers_20250830092441.py b/.history/src/handlers/advanced_handlers_20250830092441.py new file mode 100644 index 0000000..02cef9d --- /dev/null +++ b/.history/src/handlers/advanced_handlers_20250830092441.py @@ -0,0 +1,864 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Расширенные обработчики команд для управления Synology NAS +""" + +import logging +from datetime import datetime +from typing import List, Dict, Any +from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup +from telegram.ext import ContextTypes + +from src.config.config import ADMIN_USER_IDS +from src.api.synology import SynologyAPI + +logger = logging.getLogger(__name__) + +# Инициализация API Synology +synology_api = SynologyAPI() + +async def processes_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /processes для получения списка активных процессов""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + message = await update.message.reply_text("⏳ Получение информации о запущенных процессах...") + + if not synology_api.is_online(): + await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о процессах.", parse_mode="HTML") + return + + try: + processes = synology_api.get_processes(limit=15) # Получаем топ-15 процессов + + if not processes: + await message.edit_text("❌ Ошибка получения информации о процессах\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML") + return + except Exception as e: + await message.edit_text(f"❌ Ошибка получения информации о процессах\n\nПричина: {str(e)}", parse_mode="HTML") + return + + # Формируем сообщение о процессах + reply_text = f"⚙️ Активные процессы Synology NAS\n\n" + + for process in processes: + name = process.get("name", "unknown") + pid = process.get("pid", "?") + cpu_usage = process.get("cpu_usage", 0) + memory_usage = process.get("memory_usage", 0) + + reply_text += f"• {name} (PID: {pid})\n" + reply_text += f" └ CPU: {cpu_usage:.1f}%, Память: {memory_usage:.1f}%\n" + + reply_text += f"\nПоказано {len(processes)} наиболее активных процессов" + await message.edit_text(reply_text, parse_mode="HTML") + +async def network_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /network для получения информации о сетевых подключениях""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + message = await update.message.reply_text("⏳ Получение информации о сетевых подключениях...") + + if not synology_api.is_online(): + await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о сетевых подключениях.", parse_mode="HTML") + return + + try: + network_status = synology_api.get_network_status() + + if not network_status: + await message.edit_text("❌ Ошибка получения информации о сетевых подключениях\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML") + return + except Exception as e: + await message.edit_text(f"❌ Ошибка получения информации о сетевых подключениях\n\nПричина: {str(e)}", parse_mode="HTML") + return + + # Формируем сообщение о сетевых интерфейсах + interfaces = network_status.get("interfaces", []) + + reply_text = f"🌐 Сетевые подключения Synology NAS\n\n" + + for interface in interfaces: + name = interface.get("id", "unknown") + ip = interface.get("ip", "Нет данных") + mac = interface.get("mac", "Нет данных") + status = "Активен" if interface.get("status") else "Неактивен" + + # Информация о трафике + rx_bytes = interface.get("rx", 0) / (1024**2) # Перевод в МБ + tx_bytes = interface.get("tx", 0) / (1024**2) # Перевод в МБ + + reply_text += f"• {name} ({status})\n" + reply_text += f" └ IP: {ip}, MAC: {mac}\n" + + if rx_bytes > 0 or tx_bytes > 0: + reply_text += f" └ Получено: {rx_bytes:.2f} МБ, Отправлено: {tx_bytes:.2f} МБ\n" + + await message.edit_text(reply_text, parse_mode="HTML") + +async def temperature_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /temperature для мониторинга температуры""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + message = await update.message.reply_text("⏳ Получение информации о температуре...") + + if not synology_api.is_online(): + await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о температуре.", parse_mode="HTML") + return + + try: + temp_status = synology_api.get_temperature_status() + + if not temp_status: + await message.edit_text("❌ Ошибка получения информации о температуре\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML") + return + except Exception as e: + await message.edit_text(f"❌ Ошибка получения информации о температуре\n\nПричина: {str(e)}", parse_mode="HTML") + return + + # Формируем сообщение о температуре + system_temp = temp_status.get("system_temperature") + disk_temps = temp_status.get("disk_temperatures", []) + is_warning = temp_status.get("warning", False) + + # Выбор emoji в зависимости от температуры + temp_emoji = "🔥" if is_warning else "🌡️" + + reply_text = f"{temp_emoji} Температура Synology NAS\n\n" + + if system_temp is not None: + temp_status_text = "❗ ПОВЫШЕННАЯ" if is_warning else "✅ Нормальная" + reply_text += f"Температура системы: {system_temp}°C ({temp_status_text})\n\n" + + if disk_temps: + reply_text += "Температура дисков:\n" + for disk in disk_temps: + name = disk.get("name", "unknown") + model = disk.get("model", "unknown") + temp = disk.get("temperature", 0) + + disk_temp_emoji = "🔥" if temp > 45 else "✅" + reply_text += f"• {disk_temp_emoji} {name} ({model}): {temp}°C\n" + + await message.edit_text(reply_text, parse_mode="HTML") + +async def schedule_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /schedule для управления расписанием питания""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + message = await update.message.reply_text("⏳ Получение информации о расписании питания...") + + if not synology_api.is_online(): + await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о расписании питания.", parse_mode="HTML") + return + + try: + schedule = synology_api.get_power_schedule() + + if not schedule: + await message.edit_text("❌ Ошибка получения информации о расписании питания\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML") + return + except Exception as e: + await message.edit_text(f"❌ Ошибка получения информации о расписании питания\n\nПричина: {str(e)}", parse_mode="HTML") + return + + # Формируем сообщение о расписании питания + boot_tasks = schedule.get("boot_tasks", []) + shutdown_tasks = schedule.get("shutdown_tasks", []) + + reply_text = f"⏱️ Расписание питания Synology NAS\n\n" + + if boot_tasks: + reply_text += "Расписание включения:\n" + for task in boot_tasks: + days = task.get("day", []) + time = task.get("time", "00:00") + enabled = task.get("enabled", False) + + # Преобразуем номера дней в названия + day_names = [] + for day in days: + if day == 0: day_names.append("Пн") + elif day == 1: day_names.append("Вт") + elif day == 2: day_names.append("Ср") + elif day == 3: day_names.append("Чт") + elif day == 4: day_names.append("Пт") + elif day == 5: day_names.append("Сб") + elif day == 6: day_names.append("Вс") + + status = "✅ Активно" if enabled else "❌ Отключено" + day_str = ", ".join(day_names) if day_names else "Нет дней" + + reply_text += f"• {status}: {time} ({day_str})\n" + else: + reply_text += "Расписание включения: Не настроено\n" + + reply_text += "\n" + + if shutdown_tasks: + reply_text += "Расписание выключения:\n" + for task in shutdown_tasks: + days = task.get("day", []) + time = task.get("time", "00:00") + enabled = task.get("enabled", False) + + # Преобразуем номера дней в названия + day_names = [] + for day in days: + if day == 0: day_names.append("Пн") + elif day == 1: day_names.append("Вт") + elif day == 2: day_names.append("Ср") + elif day == 3: day_names.append("Чт") + elif day == 4: day_names.append("Пт") + elif day == 5: day_names.append("Сб") + elif day == 6: day_names.append("Вс") + + status = "✅ Активно" if enabled else "❌ Отключено" + day_str = ", ".join(day_names) if day_names else "Нет дней" + + reply_text += f"• {status}: {time} ({day_str})\n" + else: + reply_text += "Расписание выключения: Не настроено\n" + + # Добавляем кнопки для управления расписанием + keyboard = [ + [ + InlineKeyboardButton("➕ Добавить включение", callback_data="schedule_add_boot"), + InlineKeyboardButton("➕ Добавить выключение", callback_data="schedule_add_shutdown") + ], + [ + InlineKeyboardButton("🗑️ Удалить расписание", callback_data="schedule_delete") + ] + ] + reply_markup = InlineKeyboardMarkup(keyboard) + + await message.edit_text(reply_text, parse_mode="HTML", reply_markup=reply_markup) + +async def browse_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /browse для просмотра файлов""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + # Получаем путь из аргументов команды или используем корневую директорию + path = " ".join(context.args) if context.args else "" + + message = await update.message.reply_text(f"⏳ Получение содержимого {'папки ' + path if path else 'общих папок'}...") + + if not synology_api.is_online(): + await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить список файлов.", parse_mode="HTML") + return + + try: + browse_result = synology_api.browse_files(folder_path=path) + + if not browse_result.get("success", False): + error = browse_result.get("error", "unknown") + await message.edit_text(f"❌ Ошибка получения списка файлов\n\nПричина: {error}", parse_mode="HTML") + return + + items = browse_result.get("items", []) + current_path = browse_result.get("path", "") + is_root = browse_result.get("is_root", True) + + except Exception as e: + await message.edit_text(f"❌ Ошибка получения списка файлов\n\nПричина: {str(e)}", parse_mode="HTML") + return + + # Формируем сообщение о файлах и папках + if is_root: + reply_text = f"📁 Общие папки Synology NAS\n\n" + else: + reply_text = f"📁 Содержимое папки\n{current_path}\n\n" + + # Сортируем: сначала папки, потом файлы + folders = [] + files = [] + + for item in items: + if is_root: # Для корневого уровня все элементы - это общие папки + name = item.get("name", "unknown") + path = item.get("path", "") + folders.append((name, path, True)) + else: + name = item.get("name", "unknown") + path = item.get("path", "") + is_dir = item.get("isdir", False) + + if is_dir: + folders.append((name, path, False)) + else: + # Для файлов получаем размер + size = item.get("additional", {}).get("size", 0) + size_str = format_size(size) + files.append((name, path, size_str)) + + # Добавляем папки в сообщение + if folders: + for name, path, is_share in folders: + # Для общих папок добавляем иконку дома + icon = "🏠" if is_share else "📁" + reply_text += f"{icon} {name}\n" + + # Добавляем файлы в сообщение + if files: + for name, path, size in files: + # Выбираем иконку в зависимости от расширения + icon = get_file_icon(name) + reply_text += f"{icon} {name} ({size})\n" + + # Если нет элементов для отображения + if not folders and not files: + reply_text += "📭 Папка пуста\n" + + # Добавляем кнопку возврата наверх, если мы не в корне + if not is_root: + # Определяем родительскую директорию + parent_path = "/".join(current_path.split("/")[:-1]) if "/" in current_path else "" + + keyboard = [ + [InlineKeyboardButton("🔼 Вернуться на уровень выше", callback_data=f"browse_{parent_path}")] + ] + reply_markup = InlineKeyboardMarkup(keyboard) + + await message.edit_text(reply_text, parse_mode="HTML", reply_markup=reply_markup) + else: + await message.edit_text(reply_text, parse_mode="HTML") + +async def search_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /search для поиска файлов""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + # Получаем шаблон поиска из аргументов команды + if not context.args: + await update.message.reply_text("❌ Укажите шаблон для поиска: /search <шаблон>") + return + + pattern = " ".join(context.args) + + message = await update.message.reply_text(f"⏳ Поиск файлов по шаблону «{pattern}»...") + + if not synology_api.is_online(): + await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно выполнить поиск файлов.", parse_mode="HTML") + return + + try: + search_result = synology_api.search_files(pattern=pattern, limit=20) + + if not search_result.get("success", False): + error = search_result.get("error", "unknown") + progress = search_result.get("progress", 0) + + if error == "search_timeout": + await message.edit_text(f"❌ Превышено время ожидания результатов поиска\n\nПроцесс поиска выполнен на {progress}%", parse_mode="HTML") + else: + await message.edit_text(f"❌ Ошибка при поиске файлов\n\nПричина: {error}", parse_mode="HTML") + return + + files = search_result.get("results", []) + total = search_result.get("total", len(files)) + + except Exception as e: + await message.edit_text(f"❌ Ошибка при поиске файлов\n\nПричина: {str(e)}", parse_mode="HTML") + return + + # Формируем сообщение с результатами поиска + reply_text = f"🔍 Результаты поиска по шаблону «{pattern}»\n\n" + + if not files: + reply_text += "📭 Файлы не найдены" + else: + # Сортируем: сначала папки, потом файлы + folders = [] + found_files = [] + + for item in files: + name = item.get("name", "unknown") + path = item.get("path", "") + is_dir = item.get("isdir", False) + + if is_dir: + folders.append((name, path)) + else: + # Для файлов получаем размер и путь к родительской папке + size = item.get("additional", {}).get("size", 0) + size_str = format_size(size) + parent_path = "/".join(path.split("/")[:-1]) + found_files.append((name, path, size_str, parent_path)) + + # Добавляем папки в сообщение + if folders: + reply_text += "Найденные папки:\n" + for name, path in folders[:5]: # Показываем первые 5 папок + reply_text += f"📁 {name}\n" + + if len(folders) > 5: + reply_text += f"...и еще {len(folders) - 5} папок\n" + + reply_text += "\n" + + # Добавляем файлы в сообщение + if found_files: + reply_text += "Найденные файлы:\n" + for name, path, size, parent in found_files[:10]: # Показываем первые 10 файлов + icon = get_file_icon(name) + reply_text += f"{icon} {name} ({size})\n" + reply_text += f" Путь: .../{path.split('/')[-2]}/\n" + + if len(found_files) > 10: + reply_text += f"...и еще {len(found_files) - 10} файлов\n" + + # Добавляем информацию о общем количестве результатов + reply_text += f"\nВсего найдено: {total} элементов" + + await message.edit_text(reply_text, parse_mode="HTML") + +async def updates_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /updates для проверки обновлений""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + message = await update.message.reply_text("⏳ Проверка доступных обновлений...") + + if not synology_api.is_online(): + await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно проверить наличие обновлений.", parse_mode="HTML") + return + + try: + update_info = synology_api.check_for_updates() + + if not update_info.get("success", False): + error = update_info.get("error", "unknown") + await message.edit_text(f"❌ Ошибка при проверке обновлений\n\nПричина: {error}", parse_mode="HTML") + return + + current_version = update_info.get("current_version", "unknown") + update_available = update_info.get("update_available", False) + auto_update = update_info.get("auto_update_enabled", False) + updates = update_info.get("updates", []) + + except Exception as e: + await message.edit_text(f"❌ Ошибка при проверке обновлений\n\nПричина: {str(e)}", parse_mode="HTML") + return + + # Формируем сообщение об обновлениях + if update_available: + reply_text = f"🔄 Доступны обновления DSM\n\n" + reply_text += f"Текущая версия: {current_version}\n" + reply_text += f"Автообновление: {'✅ Включено' if auto_update else '❌ Отключено'}\n\n" + reply_text += "Доступные обновления:\n" + + for update_item in updates: + update_name = update_item.get("name", "unknown") + update_version = update_item.get("version", "unknown") + update_size = update_item.get("size", 0) + update_size_str = format_size(update_size) + + reply_text += f"• {update_name} v{update_version}\n" + reply_text += f" └ Размер: {update_size_str}\n" + else: + reply_text = f"✅ Система в актуальном состоянии\n\n" + reply_text += f"Текущая версия: {current_version}\n" + reply_text += f"Автообновление: {'✅ Включено' if auto_update else '❌ Отключено'}\n" + + await message.edit_text(reply_text, parse_mode="HTML") + +async def backup_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /backup для управления резервным копированием""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + message = await update.message.reply_text("⏳ Получение информации о резервном копировании...") + + if not synology_api.is_online(): + await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о резервном копировании.", parse_mode="HTML") + return + + try: + backup_status = synology_api.get_backup_status() + + if not backup_status.get("success", False): + error = backup_status.get("error", "unknown") + await message.edit_text(f"❌ Ошибка получения информации о резервном копировании\n\nПричина: {error}", parse_mode="HTML") + return + + backups = backup_status.get("backups", {}) + api_status = backup_status.get("available_apis", {}) + + except Exception as e: + await message.edit_text(f"❌ Ошибка получения информации о резервном копировании\n\nПричина: {str(e)}", parse_mode="HTML") + return + + # Формируем сообщение о резервном копировании + reply_text = f"💾 Резервное копирование Synology NAS\n\n" + + # Информация о Hyper Backup + hyper_backups = backups.get("hyper_backup", []) + hyper_api_available = api_status.get("hyper_backup", False) + + if hyper_api_available: + reply_text += "Hyper Backup:\n" + + if hyper_backups: + for backup in hyper_backups: + name = backup.get("name", "unknown") + status = backup.get("status", "unknown") + last_backup = backup.get("last_backup", "never") + + status_emoji = "✅" if status.lower() == "success" else "⚠️" + reply_text += f"• {status_emoji} {name}\n" + reply_text += f" └ Последнее копирование: {last_backup}\n" + else: + reply_text += "Задачи Hyper Backup не настроены\n" + + reply_text += "\n" + + # Информация о Time Backup + time_backups = backups.get("time_backup", []) + time_api_available = api_status.get("time_backup", False) + + if time_api_available: + reply_text += "Time Backup:\n" + + if time_backups: + for backup in time_backups: + name = backup.get("name", "unknown") + status = backup.get("status", "unknown") + + status_emoji = "✅" if status.lower() == "normal" else "⚠️" + reply_text += f"• {status_emoji} {name}\n" + else: + reply_text += "Задачи Time Backup не настроены\n" + + reply_text += "\n" + + # Информация о USB Copy + usb_copy = backups.get("usb_copy", {}) + usb_api_available = api_status.get("usb_copy", False) + + if usb_api_available: + usb_enabled = usb_copy.get("enabled", False) + usb_status = "✅ Включено" if usb_enabled else "❌ Отключено" + + reply_text += f"USB Copy: {usb_status}\n\n" + + # Если ни один из API не доступен + if not any(api_status.values()): + reply_text += "API для резервного копирования не доступны на вашем NAS или требуют дополнительных прав доступа.\n" + + await message.edit_text(reply_text, parse_mode="HTML") + +async def quickreboot_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /quickreboot для быстрой перезагрузки NAS""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + message = await update.message.reply_text("⏳ Выполняется перезагрузка Synology NAS...") + + if not synology_api.is_online(): + await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно выполнить перезагрузку.", parse_mode="HTML") + return + + try: + # Выполняем перезагрузку + result = synology_api.reboot_system() + + if result: + # Формируем сообщение об успешной перезагрузке + reply_text = "🔄 Synology NAS перезагружается\n\n" + reply_text += "Перезагрузка обычно занимает 3-5 минут. После перезагрузки NAS будет снова доступен." + await message.edit_text(reply_text, parse_mode="HTML") + else: + await message.edit_text("❌ Ошибка при выполнении перезагрузки\n\nПроверьте журналы NAS для получения дополнительной информации.", parse_mode="HTML") + + except Exception as e: + await message.edit_text(f"❌ Ошибка при выполнении перезагрузки\n\nПричина: {str(e)}", parse_mode="HTML") + return + +async def wakeup_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /wakeup для включения NAS""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + message = await update.message.reply_text("⏳ Отправка пакета Wake-on-LAN для включения Synology NAS...") + + # Проверяем, не включен ли NAS уже + if synology_api.is_online(force_check=True): + await message.edit_text("ℹ️ Synology NAS уже включен\n\nНет необходимости отправлять сигнал пробуждения.", parse_mode="HTML") + return + + try: + # Отправляем сигнал пробуждения + result = synology_api.power_on() + + if result: + # Формируем сообщение об успешном включении + reply_text = "✅ Synology NAS успешно включен\n\n" + reply_text += "NAS полностью готов к работе." + await message.edit_text(reply_text, parse_mode="HTML") + else: + await message.edit_text("❌ Ошибка при включении NAS\n\nВозможные причины:\n- Функция Wake-on-LAN не настроена на NAS\n- Неверно указан MAC-адрес\n- Проблемы с сетевым подключением", parse_mode="HTML") + + except Exception as e: + await message.edit_text(f"❌ Ошибка при включении NAS\n\nПричина: {str(e)}", parse_mode="HTML") + return + +async def quota_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /quota для просмотра информации о квотах""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + message = await update.message.reply_text("⏳ Получение информации о квотах пользователей...") + + if not synology_api.is_online(): + await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о квотах.", parse_mode="HTML") + return + + try: + quota_info = synology_api.get_quota_info() + + if not quota_info.get("success", False): + error = quota_info.get("error", "unknown") + await message.edit_text(f"❌ Ошибка получения информации о квотах\n\nПричина: {error}", parse_mode="HTML") + return + + user_quotas = quota_info.get("user_quotas", []) + + except Exception as e: + await message.edit_text(f"❌ Ошибка получения информации о квотах\n\nПричина: {str(e)}", parse_mode="HTML") + return + + # Формируем сообщение о квотах + reply_text = f"📊 Квоты пользователей Synology NAS\n\n" + + if not user_quotas: + reply_text += "Квоты пользователей не настроены или недоступны" + else: + for user_quota in user_quotas: + user = user_quota.get("user", "unknown") + quotas = user_quota.get("quotas", []) + + if quotas: + reply_text += f"Пользователь {user}:\n" + + for quota in quotas: + volume = quota.get("volume_name", "unknown") + limit = quota.get("limit", 0) + used = quota.get("used", 0) + + # Переводим байты в ГБ + limit_gb = limit / (1024**3) if limit > 0 else 0 + used_gb = used / (1024**3) + + # Рассчитываем процент использования + if limit_gb > 0: + usage_percent = (used_gb / limit_gb) * 100 + reply_text += f"• Том {volume}: {used_gb:.2f} ГБ из {limit_gb:.2f} ГБ ({usage_percent:.1f}%)\n" + else: + reply_text += f"• Том {volume}: {used_gb:.2f} ГБ (без ограничений)\n" + + reply_text += "\n" + + await message.edit_text(reply_text, parse_mode="HTML") + +async def schedule_callback(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик callback-запросов для управления расписанием питания""" + query = update.callback_query + await query.answer() + + user_id = update.effective_user.id + if user_id not in ADMIN_USER_IDS: + await query.edit_message_text("У вас нет доступа к этому боту.") + return + + action = query.data + + if action.startswith("schedule_"): + action_type = action.split("_")[1] + + if action_type == "add_boot": + # Логика добавления расписания включения + # В реальном боте здесь будет диалог для настройки расписания + await query.edit_message_text("⚙️ Добавление расписания включения\n\nЭта функция находится в разработке.", parse_mode="HTML") + + elif action_type == "add_shutdown": + # Логика добавления расписания выключения + await query.edit_message_text("⚙️ Добавление расписания выключения\n\nЭта функция находится в разработке.", parse_mode="HTML") + + elif action_type == "delete": + # Логика удаления расписания + await query.edit_message_text("⚙️ Удаление расписания\n\nЭта функция находится в разработке.", parse_mode="HTML") + +async def browse_callback(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик callback-запросов для навигации по файловой системе""" + query = update.callback_query + await query.answer() + + user_id = update.effective_user.id + if user_id not in ADMIN_USER_IDS: + await query.edit_message_text("У вас нет доступа к этому боту.") + return + + action = query.data + + if action.startswith("browse_"): + path = action[7:] # Убираем префикс "browse_" + + # Используем команду browse с указанным путем + message = await query.edit_message_text(f"⏳ Получение содержимого {'папки ' + path if path else 'общих папок'}...") + + if not synology_api.is_online(): + await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить список файлов.", parse_mode="HTML") + return + + try: + browse_result = synology_api.browse_files(folder_path=path) + + if not browse_result.get("success", False): + error = browse_result.get("error", "unknown") + await message.edit_text(f"❌ Ошибка получения списка файлов\n\nПричина: {error}", parse_mode="HTML") + return + + items = browse_result.get("items", []) + current_path = browse_result.get("path", "") + is_root = browse_result.get("is_root", True) + + except Exception as e: + await message.edit_text(f"❌ Ошибка получения списка файлов\n\nПричина: {str(e)}", parse_mode="HTML") + return + + # Формируем сообщение о файлах и папках (аналогично функции browse_command) + if is_root: + reply_text = f"📁 Общие папки Synology NAS\n\n" + else: + reply_text = f"📁 Содержимое папки\n{current_path}\n\n" + + # Сортируем: сначала папки, потом файлы + folders = [] + files = [] + + for item in items: + if is_root: # Для корневого уровня все элементы - это общие папки + name = item.get("name", "unknown") + path = item.get("path", "") + folders.append((name, path, True)) + else: + name = item.get("name", "unknown") + path = item.get("path", "") + is_dir = item.get("isdir", False) + + if is_dir: + folders.append((name, path, False)) + else: + # Для файлов получаем размер + size = item.get("additional", {}).get("size", 0) + size_str = format_size(size) + files.append((name, path, size_str)) + + # Добавляем папки в сообщение + if folders: + for name, path, is_share in folders: + # Для общих папок добавляем иконку дома + icon = "🏠" if is_share else "📁" + reply_text += f"{icon} {name}\n" + + # Добавляем файлы в сообщение + if files: + for name, path, size in files: + # Выбираем иконку в зависимости от расширения + icon = get_file_icon(name) + reply_text += f"{icon} {name} ({size})\n" + + # Если нет элементов для отображения + if not folders and not files: + reply_text += "📭 Папка пуста\n" + + # Добавляем кнопку возврата наверх, если мы не в корне + if not is_root: + # Определяем родительскую директорию + parent_path = "/".join(current_path.split("/")[:-1]) if "/" in current_path else "" + + keyboard = [ + [InlineKeyboardButton("🔼 Вернуться на уровень выше", callback_data=f"browse_{parent_path}")] + ] + reply_markup = InlineKeyboardMarkup(keyboard) + + await message.edit_text(reply_text, parse_mode="HTML", reply_markup=reply_markup) + else: + await message.edit_text(reply_text, parse_mode="HTML") + +# Вспомогательные функции + +def format_size(size_bytes: int) -> str: + """Преобразует размер в байтах в человекочитаемый формат""" + if size_bytes < 1024: + return f"{size_bytes} Б" + elif size_bytes < 1024**2: + return f"{size_bytes/1024:.1f} КБ" + elif size_bytes < 1024**3: + return f"{size_bytes/1024**2:.1f} МБ" + else: + return f"{size_bytes/1024**3:.1f} ГБ" + +def get_file_icon(filename: str) -> str: + """Возвращает эмодзи-иконку в зависимости от типа файла""" + extension = filename.lower().split('.')[-1] if '.' in filename else '' + + if extension in ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'tiff', 'webp']: + return "🖼️" + elif extension in ['mp4', 'avi', 'mov', 'mkv', 'wmv', 'flv']: + return "🎬" + elif extension in ['mp3', 'wav', 'ogg', 'flac', 'aac']: + return "🎵" + elif extension in ['doc', 'docx', 'txt', 'rtf', 'odt']: + return "📄" + elif extension in ['xls', 'xlsx', 'csv']: + return "📊" + elif extension in ['ppt', 'pptx']: + return "📑" + elif extension in ['pdf']: + return "📕" + elif extension in ['zip', 'rar', '7z', 'tar', 'gz']: + return "🗜️" + elif extension in ['exe', 'msi']: + return "⚙️" + else: + return "📄" diff --git a/.history/src/handlers/advanced_handlers_20250830093327.py b/.history/src/handlers/advanced_handlers_20250830093327.py new file mode 100644 index 0000000..e6b8d9b --- /dev/null +++ b/.history/src/handlers/advanced_handlers_20250830093327.py @@ -0,0 +1,912 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Расширенные обработчики команд для управления Synology NAS +""" + +import logging +from datetime import datetime +from typing import List, Dict, Any +from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup +from telegram.ext import ContextTypes + +from src.config.config import ADMIN_USER_IDS +from src.api.synology import SynologyAPI + +logger = logging.getLogger(__name__) + +# Инициализация API Synology +synology_api = SynologyAPI() + +async def processes_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /processes для получения списка активных процессов""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + message = await update.message.reply_text("⏳ Получение информации о запущенных процессах...") + + if not synology_api.is_online(): + await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о процессах.", parse_mode="HTML") + return + + try: + processes = synology_api.get_processes(limit=15) # Получаем топ-15 процессов + + if not processes: + await message.edit_text("❌ Ошибка получения информации о процессах\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML") + return + except Exception as e: + await message.edit_text(f"❌ Ошибка получения информации о процессах\n\nПричина: {str(e)}", parse_mode="HTML") + return + + # Формируем сообщение о процессах + reply_text = f"⚙️ Активные процессы Synology NAS\n\n" + + for process in processes: + name = process.get("name", "unknown") + pid = process.get("pid", "?") + cpu_usage = process.get("cpu_usage", 0) + memory_usage = process.get("memory_usage", 0) + + reply_text += f"• {name} (PID: {pid})\n" + reply_text += f" └ CPU: {cpu_usage:.1f}%, Память: {memory_usage:.1f}%\n" + + reply_text += f"\nПоказано {len(processes)} наиболее активных процессов" + await message.edit_text(reply_text, parse_mode="HTML") + +async def network_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /network для получения информации о сетевых подключениях""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + message = await update.message.reply_text("⏳ Получение информации о сетевых подключениях...") + + if not synology_api.is_online(): + await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о сетевых подключениях.", parse_mode="HTML") + return + + try: + network_status = synology_api.get_network_status() + + if not network_status: + await message.edit_text("❌ Ошибка получения информации о сетевых подключениях\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML") + return + except Exception as e: + await message.edit_text(f"❌ Ошибка получения информации о сетевых подключениях\n\nПричина: {str(e)}", parse_mode="HTML") + return + + # Формируем сообщение о сетевых интерфейсах + interfaces = network_status.get("interfaces", []) + + reply_text = f"🌐 Сетевые подключения Synology NAS\n\n" + + for interface in interfaces: + name = interface.get("id", "unknown") + ip = interface.get("ip", "Нет данных") + mac = interface.get("mac", "Нет данных") + status = "Активен" if interface.get("status") else "Неактивен" + + # Информация о трафике + rx_bytes = interface.get("rx", 0) / (1024**2) # Перевод в МБ + tx_bytes = interface.get("tx", 0) / (1024**2) # Перевод в МБ + + reply_text += f"• {name} ({status})\n" + reply_text += f" └ IP: {ip}, MAC: {mac}\n" + + if rx_bytes > 0 or tx_bytes > 0: + reply_text += f" └ Получено: {rx_bytes:.2f} МБ, Отправлено: {tx_bytes:.2f} МБ\n" + + await message.edit_text(reply_text, parse_mode="HTML") + +async def temperature_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /temperature для мониторинга температуры""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + message = await update.message.reply_text("⏳ Получение информации о температуре...") + + if not synology_api.is_online(): + await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о температуре.", parse_mode="HTML") + return + + try: + temp_status = synology_api.get_temperature_status() + + if not temp_status: + await message.edit_text("❌ Ошибка получения информации о температуре\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML") + return + except Exception as e: + await message.edit_text(f"❌ Ошибка получения информации о температуре\n\nПричина: {str(e)}", parse_mode="HTML") + return + + # Формируем сообщение о температуре + system_temp = temp_status.get("system_temperature") + disk_temps = temp_status.get("disk_temperatures", []) + is_warning = temp_status.get("warning", False) + + # Выбор emoji в зависимости от температуры + temp_emoji = "🔥" if is_warning else "🌡️" + + reply_text = f"{temp_emoji} Температура Synology NAS\n\n" + + if system_temp is not None: + temp_status_text = "❗ ПОВЫШЕННАЯ" if is_warning else "✅ Нормальная" + reply_text += f"Температура системы: {system_temp}°C ({temp_status_text})\n\n" + + if disk_temps: + reply_text += "Температура дисков:\n" + for disk in disk_temps: + name = disk.get("name", "unknown") + model = disk.get("model", "unknown") + temp = disk.get("temperature", 0) + + disk_temp_emoji = "🔥" if temp > 45 else "✅" + reply_text += f"• {disk_temp_emoji} {name} ({model}): {temp}°C\n" + + await message.edit_text(reply_text, parse_mode="HTML") + +async def schedule_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /schedule для управления расписанием питания""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + message = await update.message.reply_text("⏳ Получение информации о расписании питания...") + + if not synology_api.is_online(): + await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о расписании питания.", parse_mode="HTML") + return + + try: + schedule = synology_api.get_power_schedule() + + if not schedule: + await message.edit_text("❌ Ошибка получения информации о расписании питания\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML") + return + except Exception as e: + await message.edit_text(f"❌ Ошибка получения информации о расписании питания\n\nПричина: {str(e)}", parse_mode="HTML") + return + + # Формируем сообщение о расписании питания + boot_tasks = schedule.get("boot_tasks", []) + shutdown_tasks = schedule.get("shutdown_tasks", []) + + reply_text = f"⏱️ Расписание питания Synology NAS\n\n" + + if boot_tasks: + reply_text += "Расписание включения:\n" + for task in boot_tasks: + days = task.get("day", []) + time = task.get("time", "00:00") + enabled = task.get("enabled", False) + + # Преобразуем номера дней в названия + day_names = [] + for day in days: + if day == 0: day_names.append("Пн") + elif day == 1: day_names.append("Вт") + elif day == 2: day_names.append("Ср") + elif day == 3: day_names.append("Чт") + elif day == 4: day_names.append("Пт") + elif day == 5: day_names.append("Сб") + elif day == 6: day_names.append("Вс") + + status = "✅ Активно" if enabled else "❌ Отключено" + day_str = ", ".join(day_names) if day_names else "Нет дней" + + reply_text += f"• {status}: {time} ({day_str})\n" + else: + reply_text += "Расписание включения: Не настроено\n" + + reply_text += "\n" + + if shutdown_tasks: + reply_text += "Расписание выключения:\n" + for task in shutdown_tasks: + days = task.get("day", []) + time = task.get("time", "00:00") + enabled = task.get("enabled", False) + + # Преобразуем номера дней в названия + day_names = [] + for day in days: + if day == 0: day_names.append("Пн") + elif day == 1: day_names.append("Вт") + elif day == 2: day_names.append("Ср") + elif day == 3: day_names.append("Чт") + elif day == 4: day_names.append("Пт") + elif day == 5: day_names.append("Сб") + elif day == 6: day_names.append("Вс") + + status = "✅ Активно" if enabled else "❌ Отключено" + day_str = ", ".join(day_names) if day_names else "Нет дней" + + reply_text += f"• {status}: {time} ({day_str})\n" + else: + reply_text += "Расписание выключения: Не настроено\n" + + # Добавляем кнопки для управления расписанием + keyboard = [ + [ + InlineKeyboardButton("➕ Добавить включение", callback_data="schedule_add_boot"), + InlineKeyboardButton("➕ Добавить выключение", callback_data="schedule_add_shutdown") + ], + [ + InlineKeyboardButton("🗑️ Удалить расписание", callback_data="schedule_delete") + ] + ] + reply_markup = InlineKeyboardMarkup(keyboard) + + await message.edit_text(reply_text, parse_mode="HTML", reply_markup=reply_markup) + +async def browse_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /browse для просмотра файлов""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + # Получаем путь из аргументов команды или используем корневую директорию + path = " ".join(context.args) if context.args else "" + + message = await update.message.reply_text(f"⏳ Получение содержимого {'папки ' + path if path else 'общих папок'}...") + + if not synology_api.is_online(): + await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить список файлов.", parse_mode="HTML") + return + + try: + browse_result = synology_api.browse_files(folder_path=path) + + if not browse_result.get("success", False): + error = browse_result.get("error", "unknown") + await message.edit_text(f"❌ Ошибка получения списка файлов\n\nПричина: {error}", parse_mode="HTML") + return + + items = browse_result.get("items", []) + current_path = browse_result.get("path", "") + is_root = browse_result.get("is_root", True) + + except Exception as e: + await message.edit_text(f"❌ Ошибка получения списка файлов\n\nПричина: {str(e)}", parse_mode="HTML") + return + + # Формируем сообщение о файлах и папках + if is_root: + reply_text = f"📁 Общие папки Synology NAS\n\n" + else: + reply_text = f"📁 Содержимое папки\n{current_path}\n\n" + + # Сортируем: сначала папки, потом файлы + folders = [] + files = [] + + for item in items: + if is_root: # Для корневого уровня все элементы - это общие папки + name = item.get("name", "unknown") + path = item.get("path", "") + folders.append((name, path, True)) + else: + name = item.get("name", "unknown") + path = item.get("path", "") + is_dir = item.get("isdir", False) + + if is_dir: + folders.append((name, path, False)) + else: + # Для файлов получаем размер + size = item.get("additional", {}).get("size", 0) + size_str = format_size(size) + files.append((name, path, size_str)) + + # Добавляем папки в сообщение + if folders: + for name, path, is_share in folders: + # Для общих папок добавляем иконку дома + icon = "🏠" if is_share else "📁" + reply_text += f"{icon} {name}\n" + + # Добавляем файлы в сообщение + if files: + for name, path, size in files: + # Выбираем иконку в зависимости от расширения + icon = get_file_icon(name) + reply_text += f"{icon} {name} ({size})\n" + + # Если нет элементов для отображения + if not folders and not files: + reply_text += "📭 Папка пуста\n" + + # Добавляем кнопку возврата наверх, если мы не в корне + if not is_root: + # Определяем родительскую директорию + parent_path = "/".join(current_path.split("/")[:-1]) if "/" in current_path else "" + + keyboard = [ + [InlineKeyboardButton("🔼 Вернуться на уровень выше", callback_data=f"browse_{parent_path}")] + ] + reply_markup = InlineKeyboardMarkup(keyboard) + + await message.edit_text(reply_text, parse_mode="HTML", reply_markup=reply_markup) + else: + await message.edit_text(reply_text, parse_mode="HTML") + +async def search_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /search для поиска файлов""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + # Получаем шаблон поиска из аргументов команды + if not context.args: + await update.message.reply_text("❌ Укажите шаблон для поиска: /search <шаблон>") + return + + pattern = " ".join(context.args) + + message = await update.message.reply_text(f"⏳ Поиск файлов по шаблону «{pattern}»...") + + if not synology_api.is_online(): + await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно выполнить поиск файлов.", parse_mode="HTML") + return + + try: + search_result = synology_api.search_files(pattern=pattern, limit=20) + + if not search_result.get("success", False): + error = search_result.get("error", "unknown") + progress = search_result.get("progress", 0) + + if error == "search_timeout": + await message.edit_text(f"❌ Превышено время ожидания результатов поиска\n\nПроцесс поиска выполнен на {progress}%", parse_mode="HTML") + else: + await message.edit_text(f"❌ Ошибка при поиске файлов\n\nПричина: {error}", parse_mode="HTML") + return + + files = search_result.get("results", []) + total = search_result.get("total", len(files)) + + except Exception as e: + await message.edit_text(f"❌ Ошибка при поиске файлов\n\nПричина: {str(e)}", parse_mode="HTML") + return + + # Формируем сообщение с результатами поиска + reply_text = f"🔍 Результаты поиска по шаблону «{pattern}»\n\n" + + if not files: + reply_text += "📭 Файлы не найдены" + else: + # Сортируем: сначала папки, потом файлы + folders = [] + found_files = [] + + for item in files: + name = item.get("name", "unknown") + path = item.get("path", "") + is_dir = item.get("isdir", False) + + if is_dir: + folders.append((name, path)) + else: + # Для файлов получаем размер и путь к родительской папке + size = item.get("additional", {}).get("size", 0) + size_str = format_size(size) + parent_path = "/".join(path.split("/")[:-1]) + found_files.append((name, path, size_str, parent_path)) + + # Добавляем папки в сообщение + if folders: + reply_text += "Найденные папки:\n" + for name, path in folders[:5]: # Показываем первые 5 папок + reply_text += f"📁 {name}\n" + + if len(folders) > 5: + reply_text += f"...и еще {len(folders) - 5} папок\n" + + reply_text += "\n" + + # Добавляем файлы в сообщение + if found_files: + reply_text += "Найденные файлы:\n" + for name, path, size, parent in found_files[:10]: # Показываем первые 10 файлов + icon = get_file_icon(name) + reply_text += f"{icon} {name} ({size})\n" + reply_text += f" Путь: .../{path.split('/')[-2]}/\n" + + if len(found_files) > 10: + reply_text += f"...и еще {len(found_files) - 10} файлов\n" + + # Добавляем информацию о общем количестве результатов + reply_text += f"\nВсего найдено: {total} элементов" + + await message.edit_text(reply_text, parse_mode="HTML") + +async def updates_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /updates для проверки обновлений""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + message = await update.message.reply_text("⏳ Проверка доступных обновлений...") + + if not synology_api.is_online(): + await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно проверить наличие обновлений.", parse_mode="HTML") + return + + try: + update_info = synology_api.check_for_updates() + + if not update_info.get("success", False): + error = update_info.get("error", "unknown") + await message.edit_text(f"❌ Ошибка при проверке обновлений\n\nПричина: {error}", parse_mode="HTML") + return + + current_version = update_info.get("current_version", "unknown") + update_available = update_info.get("update_available", False) + auto_update = update_info.get("auto_update_enabled", False) + updates = update_info.get("updates", []) + + except Exception as e: + await message.edit_text(f"❌ Ошибка при проверке обновлений\n\nПричина: {str(e)}", parse_mode="HTML") + return + + # Формируем сообщение об обновлениях + if update_available: + reply_text = f"🔄 Доступны обновления DSM\n\n" + reply_text += f"Текущая версия: {current_version}\n" + reply_text += f"Автообновление: {'✅ Включено' if auto_update else '❌ Отключено'}\n\n" + reply_text += "Доступные обновления:\n" + + for update_item in updates: + update_name = update_item.get("name", "unknown") + update_version = update_item.get("version", "unknown") + update_size = update_item.get("size", 0) + update_size_str = format_size(update_size) + + reply_text += f"• {update_name} v{update_version}\n" + reply_text += f" └ Размер: {update_size_str}\n" + else: + reply_text = f"✅ Система в актуальном состоянии\n\n" + reply_text += f"Текущая версия: {current_version}\n" + reply_text += f"Автообновление: {'✅ Включено' if auto_update else '❌ Отключено'}\n" + + await message.edit_text(reply_text, parse_mode="HTML") + +async def backup_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /backup для управления резервным копированием""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + message = await update.message.reply_text("⏳ Получение информации о резервном копировании...") + + if not synology_api.is_online(): + await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о резервном копировании.", parse_mode="HTML") + return + + try: + backup_status = synology_api.get_backup_status() + + if not backup_status.get("success", False): + error = backup_status.get("error", "unknown") + await message.edit_text(f"❌ Ошибка получения информации о резервном копировании\n\nПричина: {error}", parse_mode="HTML") + return + + backups = backup_status.get("backups", {}) + api_status = backup_status.get("available_apis", {}) + + except Exception as e: + await message.edit_text(f"❌ Ошибка получения информации о резервном копировании\n\nПричина: {str(e)}", parse_mode="HTML") + return + + # Формируем сообщение о резервном копировании + reply_text = f"💾 Резервное копирование Synology NAS\n\n" + + # Информация о Hyper Backup + hyper_backups = backups.get("hyper_backup", []) + hyper_api_available = api_status.get("hyper_backup", False) + + if hyper_api_available: + reply_text += "Hyper Backup:\n" + + if hyper_backups: + for backup in hyper_backups: + name = backup.get("name", "unknown") + status = backup.get("status", "unknown") + last_backup = backup.get("last_backup", "never") + + status_emoji = "✅" if status.lower() == "success" else "⚠️" + reply_text += f"• {status_emoji} {name}\n" + reply_text += f" └ Последнее копирование: {last_backup}\n" + else: + reply_text += "Задачи Hyper Backup не настроены\n" + + reply_text += "\n" + + # Информация о Time Backup + time_backups = backups.get("time_backup", []) + time_api_available = api_status.get("time_backup", False) + + if time_api_available: + reply_text += "Time Backup:\n" + + if time_backups: + for backup in time_backups: + name = backup.get("name", "unknown") + status = backup.get("status", "unknown") + + status_emoji = "✅" if status.lower() == "normal" else "⚠️" + reply_text += f"• {status_emoji} {name}\n" + else: + reply_text += "Задачи Time Backup не настроены\n" + + reply_text += "\n" + + # Информация о USB Copy + usb_copy = backups.get("usb_copy", {}) + usb_api_available = api_status.get("usb_copy", False) + + if usb_api_available: + usb_enabled = usb_copy.get("enabled", False) + usb_status = "✅ Включено" if usb_enabled else "❌ Отключено" + + reply_text += f"USB Copy: {usb_status}\n\n" + + # Если ни один из API не доступен + if not any(api_status.values()): + reply_text += "API для резервного копирования не доступны на вашем NAS или требуют дополнительных прав доступа.\n" + + await message.edit_text(reply_text, parse_mode="HTML") + +async def reboot_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /reboot для перезагрузки NAS""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + # Добавляем подтверждение перед перезагрузкой + keyboard = [ + [ + InlineKeyboardButton("✅ Да, перезагрузить", callback_data="confirm_reboot"), + InlineKeyboardButton("❌ Отмена", callback_data="cancel_reboot") + ] + ] + reply_markup = InlineKeyboardMarkup(keyboard) + + await update.message.reply_text( + "⚠️ Вы уверены, что хотите перезагрузить Synology NAS?\n\n" + "Это действие может привести к прерыванию работы всех сервисов.", + parse_mode="HTML", + reply_markup=reply_markup + ) + +async def sleep_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /sleep для перевода NAS в спящий режим""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + # Добавляем подтверждение перед отправкой в спящий режим + keyboard = [ + [ + InlineKeyboardButton("✅ Да, усыпить", callback_data="confirm_sleep"), + InlineKeyboardButton("❌ Отмена", callback_data="cancel_sleep") + ] + ] + reply_markup = InlineKeyboardMarkup(keyboard) + + await update.message.reply_text( + "⚠️ Вы уверены, что хотите перевести Synology NAS в спящий режим?\n\n" + "Это действие приведет к остановке всех сервисов и отключению NAS.", + parse_mode="HTML", + reply_markup=reply_markup + ) + +async def quickreboot_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /quickreboot для быстрой перезагрузки NAS""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + message = await update.message.reply_text("⏳ Выполняется перезагрузка Synology NAS...") + + if not synology_api.is_online(): + await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно выполнить перезагрузку.", parse_mode="HTML") + return + + try: + # Выполняем перезагрузку + result = synology_api.reboot_system() + + if result: + # Формируем сообщение об успешной перезагрузке + reply_text = "🔄 Synology NAS перезагружается\n\n" + reply_text += "Перезагрузка обычно занимает 3-5 минут. После перезагрузки NAS будет снова доступен." + await message.edit_text(reply_text, parse_mode="HTML") + else: + await message.edit_text("❌ Ошибка при выполнении перезагрузки\n\nПроверьте журналы NAS для получения дополнительной информации.", parse_mode="HTML") + + except Exception as e: + await message.edit_text(f"❌ Ошибка при выполнении перезагрузки\n\nПричина: {str(e)}", parse_mode="HTML") + return + +async def wakeup_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /wakeup для включения NAS""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + message = await update.message.reply_text("⏳ Отправка пакета Wake-on-LAN для включения Synology NAS...") + + # Проверяем, не включен ли NAS уже + if synology_api.is_online(force_check=True): + await message.edit_text("ℹ️ Synology NAS уже включен\n\nНет необходимости отправлять сигнал пробуждения.", parse_mode="HTML") + return + + try: + # Отправляем сигнал пробуждения + result = synology_api.power_on() + + if result: + # Формируем сообщение об успешном включении + reply_text = "✅ Synology NAS успешно включен\n\n" + reply_text += "NAS полностью готов к работе." + await message.edit_text(reply_text, parse_mode="HTML") + else: + await message.edit_text("❌ Ошибка при включении NAS\n\nВозможные причины:\n- Функция Wake-on-LAN не настроена на NAS\n- Неверно указан MAC-адрес\n- Проблемы с сетевым подключением", parse_mode="HTML") + + except Exception as e: + await message.edit_text(f"❌ Ошибка при включении NAS\n\nПричина: {str(e)}", parse_mode="HTML") + return + +async def quota_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /quota для просмотра информации о квотах""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + message = await update.message.reply_text("⏳ Получение информации о квотах пользователей...") + + if not synology_api.is_online(): + await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о квотах.", parse_mode="HTML") + return + + try: + quota_info = synology_api.get_quota_info() + + if not quota_info.get("success", False): + error = quota_info.get("error", "unknown") + await message.edit_text(f"❌ Ошибка получения информации о квотах\n\nПричина: {error}", parse_mode="HTML") + return + + user_quotas = quota_info.get("user_quotas", []) + + except Exception as e: + await message.edit_text(f"❌ Ошибка получения информации о квотах\n\nПричина: {str(e)}", parse_mode="HTML") + return + + # Формируем сообщение о квотах + reply_text = f"📊 Квоты пользователей Synology NAS\n\n" + + if not user_quotas: + reply_text += "Квоты пользователей не настроены или недоступны" + else: + for user_quota in user_quotas: + user = user_quota.get("user", "unknown") + quotas = user_quota.get("quotas", []) + + if quotas: + reply_text += f"Пользователь {user}:\n" + + for quota in quotas: + volume = quota.get("volume_name", "unknown") + limit = quota.get("limit", 0) + used = quota.get("used", 0) + + # Переводим байты в ГБ + limit_gb = limit / (1024**3) if limit > 0 else 0 + used_gb = used / (1024**3) + + # Рассчитываем процент использования + if limit_gb > 0: + usage_percent = (used_gb / limit_gb) * 100 + reply_text += f"• Том {volume}: {used_gb:.2f} ГБ из {limit_gb:.2f} ГБ ({usage_percent:.1f}%)\n" + else: + reply_text += f"• Том {volume}: {used_gb:.2f} ГБ (без ограничений)\n" + + reply_text += "\n" + + await message.edit_text(reply_text, parse_mode="HTML") + +async def schedule_callback(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик callback-запросов для управления расписанием питания""" + query = update.callback_query + await query.answer() + + user_id = update.effective_user.id + if user_id not in ADMIN_USER_IDS: + await query.edit_message_text("У вас нет доступа к этому боту.") + return + + action = query.data + + if action.startswith("schedule_"): + action_type = action.split("_")[1] + + if action_type == "add_boot": + # Логика добавления расписания включения + # В реальном боте здесь будет диалог для настройки расписания + await query.edit_message_text("⚙️ Добавление расписания включения\n\nЭта функция находится в разработке.", parse_mode="HTML") + + elif action_type == "add_shutdown": + # Логика добавления расписания выключения + await query.edit_message_text("⚙️ Добавление расписания выключения\n\nЭта функция находится в разработке.", parse_mode="HTML") + + elif action_type == "delete": + # Логика удаления расписания + await query.edit_message_text("⚙️ Удаление расписания\n\nЭта функция находится в разработке.", parse_mode="HTML") + +async def browse_callback(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик callback-запросов для навигации по файловой системе""" + query = update.callback_query + await query.answer() + + user_id = update.effective_user.id + if user_id not in ADMIN_USER_IDS: + await query.edit_message_text("У вас нет доступа к этому боту.") + return + + action = query.data + + if action.startswith("browse_"): + path = action[7:] # Убираем префикс "browse_" + + # Используем команду browse с указанным путем + message = await query.edit_message_text(f"⏳ Получение содержимого {'папки ' + path if path else 'общих папок'}...") + + if not synology_api.is_online(): + await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить список файлов.", parse_mode="HTML") + return + + try: + browse_result = synology_api.browse_files(folder_path=path) + + if not browse_result.get("success", False): + error = browse_result.get("error", "unknown") + await message.edit_text(f"❌ Ошибка получения списка файлов\n\nПричина: {error}", parse_mode="HTML") + return + + items = browse_result.get("items", []) + current_path = browse_result.get("path", "") + is_root = browse_result.get("is_root", True) + + except Exception as e: + await message.edit_text(f"❌ Ошибка получения списка файлов\n\nПричина: {str(e)}", parse_mode="HTML") + return + + # Формируем сообщение о файлах и папках (аналогично функции browse_command) + if is_root: + reply_text = f"📁 Общие папки Synology NAS\n\n" + else: + reply_text = f"📁 Содержимое папки\n{current_path}\n\n" + + # Сортируем: сначала папки, потом файлы + folders = [] + files = [] + + for item in items: + if is_root: # Для корневого уровня все элементы - это общие папки + name = item.get("name", "unknown") + path = item.get("path", "") + folders.append((name, path, True)) + else: + name = item.get("name", "unknown") + path = item.get("path", "") + is_dir = item.get("isdir", False) + + if is_dir: + folders.append((name, path, False)) + else: + # Для файлов получаем размер + size = item.get("additional", {}).get("size", 0) + size_str = format_size(size) + files.append((name, path, size_str)) + + # Добавляем папки в сообщение + if folders: + for name, path, is_share in folders: + # Для общих папок добавляем иконку дома + icon = "🏠" if is_share else "📁" + reply_text += f"{icon} {name}\n" + + # Добавляем файлы в сообщение + if files: + for name, path, size in files: + # Выбираем иконку в зависимости от расширения + icon = get_file_icon(name) + reply_text += f"{icon} {name} ({size})\n" + + # Если нет элементов для отображения + if not folders and not files: + reply_text += "📭 Папка пуста\n" + + # Добавляем кнопку возврата наверх, если мы не в корне + if not is_root: + # Определяем родительскую директорию + parent_path = "/".join(current_path.split("/")[:-1]) if "/" in current_path else "" + + keyboard = [ + [InlineKeyboardButton("🔼 Вернуться на уровень выше", callback_data=f"browse_{parent_path}")] + ] + reply_markup = InlineKeyboardMarkup(keyboard) + + await message.edit_text(reply_text, parse_mode="HTML", reply_markup=reply_markup) + else: + await message.edit_text(reply_text, parse_mode="HTML") + +# Вспомогательные функции + +def format_size(size_bytes: int) -> str: + """Преобразует размер в байтах в человекочитаемый формат""" + if size_bytes < 1024: + return f"{size_bytes} Б" + elif size_bytes < 1024**2: + return f"{size_bytes/1024:.1f} КБ" + elif size_bytes < 1024**3: + return f"{size_bytes/1024**2:.1f} МБ" + else: + return f"{size_bytes/1024**3:.1f} ГБ" + +def get_file_icon(filename: str) -> str: + """Возвращает эмодзи-иконку в зависимости от типа файла""" + extension = filename.lower().split('.')[-1] if '.' in filename else '' + + if extension in ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'tiff', 'webp']: + return "🖼️" + elif extension in ['mp4', 'avi', 'mov', 'mkv', 'wmv', 'flv']: + return "🎬" + elif extension in ['mp3', 'wav', 'ogg', 'flac', 'aac']: + return "🎵" + elif extension in ['doc', 'docx', 'txt', 'rtf', 'odt']: + return "📄" + elif extension in ['xls', 'xlsx', 'csv']: + return "📊" + elif extension in ['ppt', 'pptx']: + return "📑" + elif extension in ['pdf']: + return "📕" + elif extension in ['zip', 'rar', '7z', 'tar', 'gz']: + return "🗜️" + elif extension in ['exe', 'msi']: + return "⚙️" + else: + return "📄" diff --git a/.history/src/handlers/advanced_handlers_20250830093424.py b/.history/src/handlers/advanced_handlers_20250830093424.py new file mode 100644 index 0000000..a7493f2 --- /dev/null +++ b/.history/src/handlers/advanced_handlers_20250830093424.py @@ -0,0 +1,972 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Расширенные обработчики команд для управления Synology NAS +""" + +import logging +from datetime import datetime +from typing import List, Dict, Any +from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup +from telegram.ext import ContextTypes + +from src.config.config import ADMIN_USER_IDS +from src.api.synology import SynologyAPI + +logger = logging.getLogger(__name__) + +# Инициализация API Synology +synology_api = SynologyAPI() + +async def processes_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /processes для получения списка активных процессов""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + message = await update.message.reply_text("⏳ Получение информации о запущенных процессах...") + + if not synology_api.is_online(): + await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о процессах.", parse_mode="HTML") + return + + try: + processes = synology_api.get_processes(limit=15) # Получаем топ-15 процессов + + if not processes: + await message.edit_text("❌ Ошибка получения информации о процессах\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML") + return + except Exception as e: + await message.edit_text(f"❌ Ошибка получения информации о процессах\n\nПричина: {str(e)}", parse_mode="HTML") + return + + # Формируем сообщение о процессах + reply_text = f"⚙️ Активные процессы Synology NAS\n\n" + + for process in processes: + name = process.get("name", "unknown") + pid = process.get("pid", "?") + cpu_usage = process.get("cpu_usage", 0) + memory_usage = process.get("memory_usage", 0) + + reply_text += f"• {name} (PID: {pid})\n" + reply_text += f" └ CPU: {cpu_usage:.1f}%, Память: {memory_usage:.1f}%\n" + + reply_text += f"\nПоказано {len(processes)} наиболее активных процессов" + await message.edit_text(reply_text, parse_mode="HTML") + +async def network_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /network для получения информации о сетевых подключениях""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + message = await update.message.reply_text("⏳ Получение информации о сетевых подключениях...") + + if not synology_api.is_online(): + await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о сетевых подключениях.", parse_mode="HTML") + return + + try: + network_status = synology_api.get_network_status() + + if not network_status: + await message.edit_text("❌ Ошибка получения информации о сетевых подключениях\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML") + return + except Exception as e: + await message.edit_text(f"❌ Ошибка получения информации о сетевых подключениях\n\nПричина: {str(e)}", parse_mode="HTML") + return + + # Формируем сообщение о сетевых интерфейсах + interfaces = network_status.get("interfaces", []) + + reply_text = f"🌐 Сетевые подключения Synology NAS\n\n" + + for interface in interfaces: + name = interface.get("id", "unknown") + ip = interface.get("ip", "Нет данных") + mac = interface.get("mac", "Нет данных") + status = "Активен" if interface.get("status") else "Неактивен" + + # Информация о трафике + rx_bytes = interface.get("rx", 0) / (1024**2) # Перевод в МБ + tx_bytes = interface.get("tx", 0) / (1024**2) # Перевод в МБ + + reply_text += f"• {name} ({status})\n" + reply_text += f" └ IP: {ip}, MAC: {mac}\n" + + if rx_bytes > 0 or tx_bytes > 0: + reply_text += f" └ Получено: {rx_bytes:.2f} МБ, Отправлено: {tx_bytes:.2f} МБ\n" + + await message.edit_text(reply_text, parse_mode="HTML") + +async def temperature_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /temperature для мониторинга температуры""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + message = await update.message.reply_text("⏳ Получение информации о температуре...") + + if not synology_api.is_online(): + await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о температуре.", parse_mode="HTML") + return + + try: + temp_status = synology_api.get_temperature_status() + + if not temp_status: + await message.edit_text("❌ Ошибка получения информации о температуре\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML") + return + except Exception as e: + await message.edit_text(f"❌ Ошибка получения информации о температуре\n\nПричина: {str(e)}", parse_mode="HTML") + return + + # Формируем сообщение о температуре + system_temp = temp_status.get("system_temperature") + disk_temps = temp_status.get("disk_temperatures", []) + is_warning = temp_status.get("warning", False) + + # Выбор emoji в зависимости от температуры + temp_emoji = "🔥" if is_warning else "🌡️" + + reply_text = f"{temp_emoji} Температура Synology NAS\n\n" + + if system_temp is not None: + temp_status_text = "❗ ПОВЫШЕННАЯ" if is_warning else "✅ Нормальная" + reply_text += f"Температура системы: {system_temp}°C ({temp_status_text})\n\n" + + if disk_temps: + reply_text += "Температура дисков:\n" + for disk in disk_temps: + name = disk.get("name", "unknown") + model = disk.get("model", "unknown") + temp = disk.get("temperature", 0) + + disk_temp_emoji = "🔥" if temp > 45 else "✅" + reply_text += f"• {disk_temp_emoji} {name} ({model}): {temp}°C\n" + + await message.edit_text(reply_text, parse_mode="HTML") + +async def schedule_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /schedule для управления расписанием питания""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + message = await update.message.reply_text("⏳ Получение информации о расписании питания...") + + if not synology_api.is_online(): + await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о расписании питания.", parse_mode="HTML") + return + + try: + schedule = synology_api.get_power_schedule() + + if not schedule: + await message.edit_text("❌ Ошибка получения информации о расписании питания\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML") + return + except Exception as e: + await message.edit_text(f"❌ Ошибка получения информации о расписании питания\n\nПричина: {str(e)}", parse_mode="HTML") + return + + # Формируем сообщение о расписании питания + boot_tasks = schedule.get("boot_tasks", []) + shutdown_tasks = schedule.get("shutdown_tasks", []) + + reply_text = f"⏱️ Расписание питания Synology NAS\n\n" + + if boot_tasks: + reply_text += "Расписание включения:\n" + for task in boot_tasks: + days = task.get("day", []) + time = task.get("time", "00:00") + enabled = task.get("enabled", False) + + # Преобразуем номера дней в названия + day_names = [] + for day in days: + if day == 0: day_names.append("Пн") + elif day == 1: day_names.append("Вт") + elif day == 2: day_names.append("Ср") + elif day == 3: day_names.append("Чт") + elif day == 4: day_names.append("Пт") + elif day == 5: day_names.append("Сб") + elif day == 6: day_names.append("Вс") + + status = "✅ Активно" if enabled else "❌ Отключено" + day_str = ", ".join(day_names) if day_names else "Нет дней" + + reply_text += f"• {status}: {time} ({day_str})\n" + else: + reply_text += "Расписание включения: Не настроено\n" + + reply_text += "\n" + + if shutdown_tasks: + reply_text += "Расписание выключения:\n" + for task in shutdown_tasks: + days = task.get("day", []) + time = task.get("time", "00:00") + enabled = task.get("enabled", False) + + # Преобразуем номера дней в названия + day_names = [] + for day in days: + if day == 0: day_names.append("Пн") + elif day == 1: day_names.append("Вт") + elif day == 2: day_names.append("Ср") + elif day == 3: day_names.append("Чт") + elif day == 4: day_names.append("Пт") + elif day == 5: day_names.append("Сб") + elif day == 6: day_names.append("Вс") + + status = "✅ Активно" if enabled else "❌ Отключено" + day_str = ", ".join(day_names) if day_names else "Нет дней" + + reply_text += f"• {status}: {time} ({day_str})\n" + else: + reply_text += "Расписание выключения: Не настроено\n" + + # Добавляем кнопки для управления расписанием + keyboard = [ + [ + InlineKeyboardButton("➕ Добавить включение", callback_data="schedule_add_boot"), + InlineKeyboardButton("➕ Добавить выключение", callback_data="schedule_add_shutdown") + ], + [ + InlineKeyboardButton("🗑️ Удалить расписание", callback_data="schedule_delete") + ] + ] + reply_markup = InlineKeyboardMarkup(keyboard) + + await message.edit_text(reply_text, parse_mode="HTML", reply_markup=reply_markup) + +async def browse_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /browse для просмотра файлов""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + # Получаем путь из аргументов команды или используем корневую директорию + path = " ".join(context.args) if context.args else "" + + message = await update.message.reply_text(f"⏳ Получение содержимого {'папки ' + path if path else 'общих папок'}...") + + if not synology_api.is_online(): + await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить список файлов.", parse_mode="HTML") + return + + try: + browse_result = synology_api.browse_files(folder_path=path) + + if not browse_result.get("success", False): + error = browse_result.get("error", "unknown") + await message.edit_text(f"❌ Ошибка получения списка файлов\n\nПричина: {error}", parse_mode="HTML") + return + + items = browse_result.get("items", []) + current_path = browse_result.get("path", "") + is_root = browse_result.get("is_root", True) + + except Exception as e: + await message.edit_text(f"❌ Ошибка получения списка файлов\n\nПричина: {str(e)}", parse_mode="HTML") + return + + # Формируем сообщение о файлах и папках + if is_root: + reply_text = f"📁 Общие папки Synology NAS\n\n" + else: + reply_text = f"📁 Содержимое папки\n{current_path}\n\n" + + # Сортируем: сначала папки, потом файлы + folders = [] + files = [] + + for item in items: + if is_root: # Для корневого уровня все элементы - это общие папки + name = item.get("name", "unknown") + path = item.get("path", "") + folders.append((name, path, True)) + else: + name = item.get("name", "unknown") + path = item.get("path", "") + is_dir = item.get("isdir", False) + + if is_dir: + folders.append((name, path, False)) + else: + # Для файлов получаем размер + size = item.get("additional", {}).get("size", 0) + size_str = format_size(size) + files.append((name, path, size_str)) + + # Добавляем папки в сообщение + if folders: + for name, path, is_share in folders: + # Для общих папок добавляем иконку дома + icon = "🏠" if is_share else "📁" + reply_text += f"{icon} {name}\n" + + # Добавляем файлы в сообщение + if files: + for name, path, size in files: + # Выбираем иконку в зависимости от расширения + icon = get_file_icon(name) + reply_text += f"{icon} {name} ({size})\n" + + # Если нет элементов для отображения + if not folders and not files: + reply_text += "📭 Папка пуста\n" + + # Добавляем кнопку возврата наверх, если мы не в корне + if not is_root: + # Определяем родительскую директорию + parent_path = "/".join(current_path.split("/")[:-1]) if "/" in current_path else "" + + keyboard = [ + [InlineKeyboardButton("🔼 Вернуться на уровень выше", callback_data=f"browse_{parent_path}")] + ] + reply_markup = InlineKeyboardMarkup(keyboard) + + await message.edit_text(reply_text, parse_mode="HTML", reply_markup=reply_markup) + else: + await message.edit_text(reply_text, parse_mode="HTML") + +async def search_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /search для поиска файлов""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + # Получаем шаблон поиска из аргументов команды + if not context.args: + await update.message.reply_text("❌ Укажите шаблон для поиска: /search <шаблон>") + return + + pattern = " ".join(context.args) + + message = await update.message.reply_text(f"⏳ Поиск файлов по шаблону «{pattern}»...") + + if not synology_api.is_online(): + await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно выполнить поиск файлов.", parse_mode="HTML") + return + + try: + search_result = synology_api.search_files(pattern=pattern, limit=20) + + if not search_result.get("success", False): + error = search_result.get("error", "unknown") + progress = search_result.get("progress", 0) + + if error == "search_timeout": + await message.edit_text(f"❌ Превышено время ожидания результатов поиска\n\nПроцесс поиска выполнен на {progress}%", parse_mode="HTML") + else: + await message.edit_text(f"❌ Ошибка при поиске файлов\n\nПричина: {error}", parse_mode="HTML") + return + + files = search_result.get("results", []) + total = search_result.get("total", len(files)) + + except Exception as e: + await message.edit_text(f"❌ Ошибка при поиске файлов\n\nПричина: {str(e)}", parse_mode="HTML") + return + + # Формируем сообщение с результатами поиска + reply_text = f"🔍 Результаты поиска по шаблону «{pattern}»\n\n" + + if not files: + reply_text += "📭 Файлы не найдены" + else: + # Сортируем: сначала папки, потом файлы + folders = [] + found_files = [] + + for item in files: + name = item.get("name", "unknown") + path = item.get("path", "") + is_dir = item.get("isdir", False) + + if is_dir: + folders.append((name, path)) + else: + # Для файлов получаем размер и путь к родительской папке + size = item.get("additional", {}).get("size", 0) + size_str = format_size(size) + parent_path = "/".join(path.split("/")[:-1]) + found_files.append((name, path, size_str, parent_path)) + + # Добавляем папки в сообщение + if folders: + reply_text += "Найденные папки:\n" + for name, path in folders[:5]: # Показываем первые 5 папок + reply_text += f"📁 {name}\n" + + if len(folders) > 5: + reply_text += f"...и еще {len(folders) - 5} папок\n" + + reply_text += "\n" + + # Добавляем файлы в сообщение + if found_files: + reply_text += "Найденные файлы:\n" + for name, path, size, parent in found_files[:10]: # Показываем первые 10 файлов + icon = get_file_icon(name) + reply_text += f"{icon} {name} ({size})\n" + reply_text += f" Путь: .../{path.split('/')[-2]}/\n" + + if len(found_files) > 10: + reply_text += f"...и еще {len(found_files) - 10} файлов\n" + + # Добавляем информацию о общем количестве результатов + reply_text += f"\nВсего найдено: {total} элементов" + + await message.edit_text(reply_text, parse_mode="HTML") + +async def updates_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /updates для проверки обновлений""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + message = await update.message.reply_text("⏳ Проверка доступных обновлений...") + + if not synology_api.is_online(): + await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно проверить наличие обновлений.", parse_mode="HTML") + return + + try: + update_info = synology_api.check_for_updates() + + if not update_info.get("success", False): + error = update_info.get("error", "unknown") + await message.edit_text(f"❌ Ошибка при проверке обновлений\n\nПричина: {error}", parse_mode="HTML") + return + + current_version = update_info.get("current_version", "unknown") + update_available = update_info.get("update_available", False) + auto_update = update_info.get("auto_update_enabled", False) + updates = update_info.get("updates", []) + + except Exception as e: + await message.edit_text(f"❌ Ошибка при проверке обновлений\n\nПричина: {str(e)}", parse_mode="HTML") + return + + # Формируем сообщение об обновлениях + if update_available: + reply_text = f"🔄 Доступны обновления DSM\n\n" + reply_text += f"Текущая версия: {current_version}\n" + reply_text += f"Автообновление: {'✅ Включено' if auto_update else '❌ Отключено'}\n\n" + reply_text += "Доступные обновления:\n" + + for update_item in updates: + update_name = update_item.get("name", "unknown") + update_version = update_item.get("version", "unknown") + update_size = update_item.get("size", 0) + update_size_str = format_size(update_size) + + reply_text += f"• {update_name} v{update_version}\n" + reply_text += f" └ Размер: {update_size_str}\n" + else: + reply_text = f"✅ Система в актуальном состоянии\n\n" + reply_text += f"Текущая версия: {current_version}\n" + reply_text += f"Автообновление: {'✅ Включено' if auto_update else '❌ Отключено'}\n" + + await message.edit_text(reply_text, parse_mode="HTML") + +async def backup_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /backup для управления резервным копированием""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + message = await update.message.reply_text("⏳ Получение информации о резервном копировании...") + + if not synology_api.is_online(): + await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о резервном копировании.", parse_mode="HTML") + return + + try: + backup_status = synology_api.get_backup_status() + + if not backup_status.get("success", False): + error = backup_status.get("error", "unknown") + await message.edit_text(f"❌ Ошибка получения информации о резервном копировании\n\nПричина: {error}", parse_mode="HTML") + return + + backups = backup_status.get("backups", {}) + api_status = backup_status.get("available_apis", {}) + + except Exception as e: + await message.edit_text(f"❌ Ошибка получения информации о резервном копировании\n\nПричина: {str(e)}", parse_mode="HTML") + return + + # Формируем сообщение о резервном копировании + reply_text = f"💾 Резервное копирование Synology NAS\n\n" + + # Информация о Hyper Backup + hyper_backups = backups.get("hyper_backup", []) + hyper_api_available = api_status.get("hyper_backup", False) + + if hyper_api_available: + reply_text += "Hyper Backup:\n" + + if hyper_backups: + for backup in hyper_backups: + name = backup.get("name", "unknown") + status = backup.get("status", "unknown") + last_backup = backup.get("last_backup", "never") + + status_emoji = "✅" if status.lower() == "success" else "⚠️" + reply_text += f"• {status_emoji} {name}\n" + reply_text += f" └ Последнее копирование: {last_backup}\n" + else: + reply_text += "Задачи Hyper Backup не настроены\n" + + reply_text += "\n" + + # Информация о Time Backup + time_backups = backups.get("time_backup", []) + time_api_available = api_status.get("time_backup", False) + + if time_api_available: + reply_text += "Time Backup:\n" + + if time_backups: + for backup in time_backups: + name = backup.get("name", "unknown") + status = backup.get("status", "unknown") + + status_emoji = "✅" if status.lower() == "normal" else "⚠️" + reply_text += f"• {status_emoji} {name}\n" + else: + reply_text += "Задачи Time Backup не настроены\n" + + reply_text += "\n" + + # Информация о USB Copy + usb_copy = backups.get("usb_copy", {}) + usb_api_available = api_status.get("usb_copy", False) + + if usb_api_available: + usb_enabled = usb_copy.get("enabled", False) + usb_status = "✅ Включено" if usb_enabled else "❌ Отключено" + + reply_text += f"USB Copy: {usb_status}\n\n" + + # Если ни один из API не доступен + if not any(api_status.values()): + reply_text += "API для резервного копирования не доступны на вашем NAS или требуют дополнительных прав доступа.\n" + + await message.edit_text(reply_text, parse_mode="HTML") + +async def reboot_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /reboot для перезагрузки NAS""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + # Добавляем подтверждение перед перезагрузкой + keyboard = [ + [ + InlineKeyboardButton("✅ Да, перезагрузить", callback_data="confirm_reboot"), + InlineKeyboardButton("❌ Отмена", callback_data="cancel_reboot") + ] + ] + reply_markup = InlineKeyboardMarkup(keyboard) + + await update.message.reply_text( + "⚠️ Вы уверены, что хотите перезагрузить Synology NAS?\n\n" + "Это действие может привести к прерыванию работы всех сервисов.", + parse_mode="HTML", + reply_markup=reply_markup + ) + +async def sleep_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /sleep для перевода NAS в спящий режим""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + # Добавляем подтверждение перед отправкой в спящий режим + keyboard = [ + [ + InlineKeyboardButton("✅ Да, усыпить", callback_data="confirm_sleep"), + InlineKeyboardButton("❌ Отмена", callback_data="cancel_sleep") + ] + ] + reply_markup = InlineKeyboardMarkup(keyboard) + + await update.message.reply_text( + "⚠️ Вы уверены, что хотите перевести Synology NAS в спящий режим?\n\n" + "Это действие приведет к остановке всех сервисов и отключению NAS.", + parse_mode="HTML", + reply_markup=reply_markup + ) + +async def quickreboot_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /quickreboot для быстрой перезагрузки NAS""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + message = await update.message.reply_text("⏳ Выполняется перезагрузка Synology NAS...") + + if not synology_api.is_online(): + await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно выполнить перезагрузку.", parse_mode="HTML") + return + + try: + # Выполняем перезагрузку + result = synology_api.reboot_system() + + if result: + # Формируем сообщение об успешной перезагрузке + reply_text = "🔄 Synology NAS перезагружается\n\n" + reply_text += "Перезагрузка обычно занимает 3-5 минут. После перезагрузки NAS будет снова доступен." + await message.edit_text(reply_text, parse_mode="HTML") + else: + await message.edit_text("❌ Ошибка при выполнении перезагрузки\n\nПроверьте журналы NAS для получения дополнительной информации.", parse_mode="HTML") + + except Exception as e: + await message.edit_text(f"❌ Ошибка при выполнении перезагрузки\n\nПричина: {str(e)}", parse_mode="HTML") + return + +async def wakeup_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /wakeup для включения NAS""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + message = await update.message.reply_text("⏳ Отправка пакета Wake-on-LAN для включения Synology NAS...") + + # Проверяем, не включен ли NAS уже + if synology_api.is_online(force_check=True): + await message.edit_text("ℹ️ Synology NAS уже включен\n\nНет необходимости отправлять сигнал пробуждения.", parse_mode="HTML") + return + + try: + # Отправляем сигнал пробуждения + result = synology_api.power_on() + + if result: + # Формируем сообщение об успешном включении + reply_text = "✅ Synology NAS успешно включен\n\n" + reply_text += "NAS полностью готов к работе." + await message.edit_text(reply_text, parse_mode="HTML") + else: + await message.edit_text("❌ Ошибка при включении NAS\n\nВозможные причины:\n- Функция Wake-on-LAN не настроена на NAS\n- Неверно указан MAC-адрес\n- Проблемы с сетевым подключением", parse_mode="HTML") + + except Exception as e: + await message.edit_text(f"❌ Ошибка при включении NAS\n\nПричина: {str(e)}", parse_mode="HTML") + return + +async def quota_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /quota для просмотра информации о квотах""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + message = await update.message.reply_text("⏳ Получение информации о квотах пользователей...") + + if not synology_api.is_online(): + await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о квотах.", parse_mode="HTML") + return + + try: + quota_info = synology_api.get_quota_info() + + if not quota_info.get("success", False): + error = quota_info.get("error", "unknown") + await message.edit_text(f"❌ Ошибка получения информации о квотах\n\nПричина: {error}", parse_mode="HTML") + return + + user_quotas = quota_info.get("user_quotas", []) + + except Exception as e: + await message.edit_text(f"❌ Ошибка получения информации о квотах\n\nПричина: {str(e)}", parse_mode="HTML") + return + + # Формируем сообщение о квотах + reply_text = f"📊 Квоты пользователей Synology NAS\n\n" + + if not user_quotas: + reply_text += "Квоты пользователей не настроены или недоступны" + else: + for user_quota in user_quotas: + user = user_quota.get("user", "unknown") + quotas = user_quota.get("quotas", []) + + if quotas: + reply_text += f"Пользователь {user}:\n" + + for quota in quotas: + volume = quota.get("volume_name", "unknown") + limit = quota.get("limit", 0) + used = quota.get("used", 0) + + # Переводим байты в ГБ + limit_gb = limit / (1024**3) if limit > 0 else 0 + used_gb = used / (1024**3) + + # Рассчитываем процент использования + if limit_gb > 0: + usage_percent = (used_gb / limit_gb) * 100 + reply_text += f"• Том {volume}: {used_gb:.2f} ГБ из {limit_gb:.2f} ГБ ({usage_percent:.1f}%)\n" + else: + reply_text += f"• Том {volume}: {used_gb:.2f} ГБ (без ограничений)\n" + + reply_text += "\n" + + await message.edit_text(reply_text, parse_mode="HTML") + +async def schedule_callback(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик callback-запросов для управления расписанием питания""" + query = update.callback_query + await query.answer() + + user_id = update.effective_user.id + if user_id not in ADMIN_USER_IDS: + await query.edit_message_text("У вас нет доступа к этому боту.") + return + + action = query.data + + if action.startswith("schedule_"): + action_type = action.split("_")[1] + + if action_type == "add_boot": + # Логика добавления расписания включения + # В реальном боте здесь будет диалог для настройки расписания + await query.edit_message_text("⚙️ Добавление расписания включения\n\nЭта функция находится в разработке.", parse_mode="HTML") + + elif action_type == "add_shutdown": + # Логика добавления расписания выключения + await query.edit_message_text("⚙️ Добавление расписания выключения\n\nЭта функция находится в разработке.", parse_mode="HTML") + + elif action_type == "delete": + # Логика удаления расписания + await query.edit_message_text("⚙️ Удаление расписания\n\nЭта функция находится в разработке.", parse_mode="HTML") + +async def browse_callback(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик callback-запросов для навигации по файловой системе""" + query = update.callback_query + await query.answer() + + user_id = update.effective_user.id + if user_id not in ADMIN_USER_IDS: + await query.edit_message_text("У вас нет доступа к этому боту.") + return + + action = query.data + + if action.startswith("browse_"): + path = action[7:] # Убираем префикс "browse_" + + # Используем команду browse с указанным путем + message = await query.edit_message_text(f"⏳ Получение содержимого {'папки ' + path if path else 'общих папок'}...") + + if not synology_api.is_online(): + await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить список файлов.", parse_mode="HTML") + return + + try: + browse_result = synology_api.browse_files(folder_path=path) + + if not browse_result.get("success", False): + error = browse_result.get("error", "unknown") + await message.edit_text(f"❌ Ошибка получения списка файлов\n\nПричина: {error}", parse_mode="HTML") + return + + items = browse_result.get("items", []) + current_path = browse_result.get("path", "") + is_root = browse_result.get("is_root", True) + + except Exception as e: + await message.edit_text(f"❌ Ошибка получения списка файлов\n\nПричина: {str(e)}", parse_mode="HTML") + return + + # Формируем сообщение о файлах и папках (аналогично функции browse_command) + if is_root: + reply_text = f"📁 Общие папки Synology NAS\n\n" + else: + reply_text = f"📁 Содержимое папки\n{current_path}\n\n" + + # Сортируем: сначала папки, потом файлы + folders = [] + files = [] + + for item in items: + if is_root: # Для корневого уровня все элементы - это общие папки + name = item.get("name", "unknown") + path = item.get("path", "") + folders.append((name, path, True)) + else: + name = item.get("name", "unknown") + path = item.get("path", "") + is_dir = item.get("isdir", False) + + if is_dir: + folders.append((name, path, False)) + else: + # Для файлов получаем размер + size = item.get("additional", {}).get("size", 0) + size_str = format_size(size) + files.append((name, path, size_str)) + + # Добавляем папки в сообщение + if folders: + for name, path, is_share in folders: + # Для общих папок добавляем иконку дома + icon = "🏠" if is_share else "📁" + reply_text += f"{icon} {name}\n" + + # Добавляем файлы в сообщение + if files: + for name, path, size in files: + # Выбираем иконку в зависимости от расширения + icon = get_file_icon(name) + reply_text += f"{icon} {name} ({size})\n" + + # Если нет элементов для отображения + if not folders and not files: + reply_text += "📭 Папка пуста\n" + + # Добавляем кнопку возврата наверх, если мы не в корне + if not is_root: + # Определяем родительскую директорию + parent_path = "/".join(current_path.split("/")[:-1]) if "/" in current_path else "" + + keyboard = [ + [InlineKeyboardButton("🔼 Вернуться на уровень выше", callback_data=f"browse_{parent_path}")] + ] + reply_markup = InlineKeyboardMarkup(keyboard) + + await message.edit_text(reply_text, parse_mode="HTML", reply_markup=reply_markup) + else: + await message.edit_text(reply_text, parse_mode="HTML") + +async def power_callback(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик callback-запросов для управления питанием""" + query = update.callback_query + await query.answer() + + user_id = update.effective_user.id + if user_id not in ADMIN_USER_IDS: + await query.edit_message_text("У вас нет доступа к этому боту.") + return + + action = query.data + + if action == "confirm_reboot": + # Выполняем перезагрузку + message = await query.edit_message_text("⏳ Выполняется перезагрузка Synology NAS...") + + if not synology_api.is_online(): + await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно выполнить перезагрузку.", parse_mode="HTML") + return + + try: + result = synology_api.reboot_system() + + if result: + reply_text = "🔄 Synology NAS перезагружается\n\n" + reply_text += "Перезагрузка обычно занимает 3-5 минут. После перезагрузки NAS будет снова доступен." + await message.edit_text(reply_text, parse_mode="HTML") + else: + await message.edit_text("❌ Ошибка при выполнении перезагрузки\n\nПроверьте журналы NAS для получения дополнительной информации.", parse_mode="HTML") + except Exception as e: + await message.edit_text(f"❌ Ошибка при выполнении перезагрузки\n\nПричина: {str(e)}", parse_mode="HTML") + + elif action == "cancel_reboot": + # Отменяем перезагрузку + await query.edit_message_text("✅ Перезагрузка отменена", parse_mode="HTML") + + elif action == "confirm_sleep": + # Выполняем переход в спящий режим (выключение) + message = await query.edit_message_text("⏳ Перевод Synology NAS в спящий режим...") + + if not synology_api.is_online(): + await message.edit_text("❌ Synology NAS уже оффлайн\n\nНевозможно выполнить переход в спящий режим.", parse_mode="HTML") + return + + try: + result = synology_api.power_off() + + if result: + reply_text = "💤 Synology NAS переведен в спящий режим\n\n" + reply_text += "Для пробуждения используйте команду /wakeup" + await message.edit_text(reply_text, parse_mode="HTML") + else: + await message.edit_text("❌ Ошибка при переходе в спящий режим\n\nПроверьте журналы NAS для получения дополнительной информации.", parse_mode="HTML") + except Exception as e: + await message.edit_text(f"❌ Ошибка при переходе в спящий режим\n\nПричина: {str(e)}", parse_mode="HTML") + + elif action == "cancel_sleep": + # Отменяем переход в спящий режим + await query.edit_message_text("✅ Переход в спящий режим отменен", parse_mode="HTML") + +# Вспомогательные функции + +def format_size(size_bytes: int) -> str: + """Преобразует размер в байтах в человекочитаемый формат""" + if size_bytes < 1024: + return f"{size_bytes} Б" + elif size_bytes < 1024**2: + return f"{size_bytes/1024:.1f} КБ" + elif size_bytes < 1024**3: + return f"{size_bytes/1024**2:.1f} МБ" + else: + return f"{size_bytes/1024**3:.1f} ГБ" + +def get_file_icon(filename: str) -> str: + """Возвращает эмодзи-иконку в зависимости от типа файла""" + extension = filename.lower().split('.')[-1] if '.' in filename else '' + + if extension in ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'tiff', 'webp']: + return "🖼️" + elif extension in ['mp4', 'avi', 'mov', 'mkv', 'wmv', 'flv']: + return "🎬" + elif extension in ['mp3', 'wav', 'ogg', 'flac', 'aac']: + return "🎵" + elif extension in ['doc', 'docx', 'txt', 'rtf', 'odt']: + return "📄" + elif extension in ['xls', 'xlsx', 'csv']: + return "📊" + elif extension in ['ppt', 'pptx']: + return "📑" + elif extension in ['pdf']: + return "📕" + elif extension in ['zip', 'rar', '7z', 'tar', 'gz']: + return "🗜️" + elif extension in ['exe', 'msi']: + return "⚙️" + else: + return "📄" diff --git a/.history/src/handlers/advanced_handlers_20250830093627.py b/.history/src/handlers/advanced_handlers_20250830093627.py new file mode 100644 index 0000000..9cc9ee6 --- /dev/null +++ b/.history/src/handlers/advanced_handlers_20250830093627.py @@ -0,0 +1,972 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Расширенные обработчики команд для управления Synology NAS +""" + +import logging +from datetime import datetime +from typing import List, Dict, Any +from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup +from telegram.ext import ContextTypes + +from src.config.config import ADMIN_USER_IDS +from src.api.synology import SynologyAPI + +logger = logging.getLogger(__name__) + +# Инициализация API Synology +synology_api = SynologyAPI() + +async def processes_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /processes для получения списка активных процессов""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + message = await update.message.reply_text("⏳ Получение информации о запущенных процессах...") + + if not synology_api.is_online(): + await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о процессах.", parse_mode="HTML") + return + + try: + processes = synology_api.get_processes(limit=15) # Получаем топ-15 процессов + + if not processes: + await message.edit_text("❌ Ошибка получения информации о процессах\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML") + return + except Exception as e: + await message.edit_text(f"❌ Ошибка получения информации о процессах\n\nПричина: {str(e)}", parse_mode="HTML") + return + + # Формируем сообщение о процессах + reply_text = f"⚙️ Активные процессы Synology NAS\n\n" + + for process in processes: + name = process.get("name", "unknown") + pid = process.get("pid", "?") + cpu_usage = process.get("cpu_usage", 0) + memory_usage = process.get("memory_usage", 0) + + reply_text += f"• {name} (PID: {pid})\n" + reply_text += f" └ CPU: {cpu_usage:.1f}%, Память: {memory_usage:.1f}%\n" + + reply_text += f"\nПоказано {len(processes)} наиболее активных процессов" + await message.edit_text(reply_text, parse_mode="HTML") + +async def network_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /network для получения информации о сетевых подключениях""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + message = await update.message.reply_text("⏳ Получение информации о сетевых подключениях...") + + if not synology_api.is_online(): + await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о сетевых подключениях.", parse_mode="HTML") + return + + try: + network_status = synology_api.get_network_status() + + if not network_status: + await message.edit_text("❌ Ошибка получения информации о сетевых подключениях\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML") + return + except Exception as e: + await message.edit_text(f"❌ Ошибка получения информации о сетевых подключениях\n\nПричина: {str(e)}", parse_mode="HTML") + return + + # Формируем сообщение о сетевых интерфейсах + interfaces = network_status.get("interfaces", []) + + reply_text = f"🌐 Сетевые подключения Synology NAS\n\n" + + for interface in interfaces: + name = interface.get("id", "unknown") + ip = interface.get("ip", "Нет данных") + mac = interface.get("mac", "Нет данных") + status = "Активен" if interface.get("status") else "Неактивен" + + # Информация о трафике + rx_bytes = interface.get("rx", 0) / (1024**2) # Перевод в МБ + tx_bytes = interface.get("tx", 0) / (1024**2) # Перевод в МБ + + reply_text += f"• {name} ({status})\n" + reply_text += f" └ IP: {ip}, MAC: {mac}\n" + + if rx_bytes > 0 or tx_bytes > 0: + reply_text += f" └ Получено: {rx_bytes:.2f} МБ, Отправлено: {tx_bytes:.2f} МБ\n" + + await message.edit_text(reply_text, parse_mode="HTML") + +async def temperature_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /temperature для мониторинга температуры""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + message = await update.message.reply_text("⏳ Получение информации о температуре...") + + if not synology_api.is_online(): + await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о температуре.", parse_mode="HTML") + return + + try: + temp_status = synology_api.get_temperature_status() + + if not temp_status: + await message.edit_text("❌ Ошибка получения информации о температуре\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML") + return + except Exception as e: + await message.edit_text(f"❌ Ошибка получения информации о температуре\n\nПричина: {str(e)}", parse_mode="HTML") + return + + # Формируем сообщение о температуре + system_temp = temp_status.get("system_temperature") + disk_temps = temp_status.get("disk_temperatures", []) + is_warning = temp_status.get("warning", False) + + # Выбор emoji в зависимости от температуры + temp_emoji = "🔥" if is_warning else "🌡️" + + reply_text = f"{temp_emoji} Температура Synology NAS\n\n" + + if system_temp is not None: + temp_status_text = "❗ ПОВЫШЕННАЯ" if is_warning else "✅ Нормальная" + reply_text += f"Температура системы: {system_temp}°C ({temp_status_text})\n\n" + + if disk_temps: + reply_text += "Температура дисков:\n" + for disk in disk_temps: + name = disk.get("name", "unknown") + model = disk.get("model", "unknown") + temp = disk.get("temperature", 0) + + disk_temp_emoji = "🔥" if temp > 45 else "✅" + reply_text += f"• {disk_temp_emoji} {name} ({model}): {temp}°C\n" + + await message.edit_text(reply_text, parse_mode="HTML") + +async def schedule_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /schedule для управления расписанием питания""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + message = await update.message.reply_text("⏳ Получение информации о расписании питания...") + + if not synology_api.is_online(): + await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о расписании питания.", parse_mode="HTML") + return + + try: + schedule = synology_api.get_power_schedule() + + if not schedule: + await message.edit_text("❌ Ошибка получения информации о расписании питания\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML") + return + except Exception as e: + await message.edit_text(f"❌ Ошибка получения информации о расписании питания\n\nПричина: {str(e)}", parse_mode="HTML") + return + + # Формируем сообщение о расписании питания + boot_tasks = schedule.get("boot_tasks", []) + shutdown_tasks = schedule.get("shutdown_tasks", []) + + reply_text = f"⏱️ Расписание питания Synology NAS\n\n" + + if boot_tasks: + reply_text += "Расписание включения:\n" + for task in boot_tasks: + days = task.get("day", []) + time = task.get("time", "00:00") + enabled = task.get("enabled", False) + + # Преобразуем номера дней в названия + day_names = [] + for day in days: + if day == 0: day_names.append("Пн") + elif day == 1: day_names.append("Вт") + elif day == 2: day_names.append("Ср") + elif day == 3: day_names.append("Чт") + elif day == 4: day_names.append("Пт") + elif day == 5: day_names.append("Сб") + elif day == 6: day_names.append("Вс") + + status = "✅ Активно" if enabled else "❌ Отключено" + day_str = ", ".join(day_names) if day_names else "Нет дней" + + reply_text += f"• {status}: {time} ({day_str})\n" + else: + reply_text += "Расписание включения: Не настроено\n" + + reply_text += "\n" + + if shutdown_tasks: + reply_text += "Расписание выключения:\n" + for task in shutdown_tasks: + days = task.get("day", []) + time = task.get("time", "00:00") + enabled = task.get("enabled", False) + + # Преобразуем номера дней в названия + day_names = [] + for day in days: + if day == 0: day_names.append("Пн") + elif day == 1: day_names.append("Вт") + elif day == 2: day_names.append("Ср") + elif day == 3: day_names.append("Чт") + elif day == 4: day_names.append("Пт") + elif day == 5: day_names.append("Сб") + elif day == 6: day_names.append("Вс") + + status = "✅ Активно" if enabled else "❌ Отключено" + day_str = ", ".join(day_names) if day_names else "Нет дней" + + reply_text += f"• {status}: {time} ({day_str})\n" + else: + reply_text += "Расписание выключения: Не настроено\n" + + # Добавляем кнопки для управления расписанием + keyboard = [ + [ + InlineKeyboardButton("➕ Добавить включение", callback_data="schedule_add_boot"), + InlineKeyboardButton("➕ Добавить выключение", callback_data="schedule_add_shutdown") + ], + [ + InlineKeyboardButton("🗑️ Удалить расписание", callback_data="schedule_delete") + ] + ] + reply_markup = InlineKeyboardMarkup(keyboard) + + await message.edit_text(reply_text, parse_mode="HTML", reply_markup=reply_markup) + +async def browse_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /browse для просмотра файлов""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + # Получаем путь из аргументов команды или используем корневую директорию + path = " ".join(context.args) if context.args else "" + + message = await update.message.reply_text(f"⏳ Получение содержимого {'папки ' + path if path else 'общих папок'}...") + + if not synology_api.is_online(): + await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить список файлов.", parse_mode="HTML") + return + + try: + browse_result = synology_api.browse_files(folder_path=path) + + if not browse_result.get("success", False): + error = browse_result.get("error", "unknown") + await message.edit_text(f"❌ Ошибка получения списка файлов\n\nПричина: {error}", parse_mode="HTML") + return + + items = browse_result.get("items", []) + current_path = browse_result.get("path", "") + is_root = browse_result.get("is_root", True) + + except Exception as e: + await message.edit_text(f"❌ Ошибка получения списка файлов\n\nПричина: {str(e)}", parse_mode="HTML") + return + + # Формируем сообщение о файлах и папках + if is_root: + reply_text = f"📁 Общие папки Synology NAS\n\n" + else: + reply_text = f"📁 Содержимое папки\n{current_path}\n\n" + + # Сортируем: сначала папки, потом файлы + folders = [] + files = [] + + for item in items: + if is_root: # Для корневого уровня все элементы - это общие папки + name = item.get("name", "unknown") + path = item.get("path", "") + folders.append((name, path, True)) + else: + name = item.get("name", "unknown") + path = item.get("path", "") + is_dir = item.get("isdir", False) + + if is_dir: + folders.append((name, path, False)) + else: + # Для файлов получаем размер + size = item.get("additional", {}).get("size", 0) + size_str = format_size(size) + files.append((name, path, size_str)) + + # Добавляем папки в сообщение + if folders: + for name, path, is_share in folders: + # Для общих папок добавляем иконку дома + icon = "🏠" if is_share else "📁" + reply_text += f"{icon} {name}\n" + + # Добавляем файлы в сообщение + if files: + for name, path, size in files: + # Выбираем иконку в зависимости от расширения + icon = get_file_icon(name) + reply_text += f"{icon} {name} ({size})\n" + + # Если нет элементов для отображения + if not folders and not files: + reply_text += "📭 Папка пуста\n" + + # Добавляем кнопку возврата наверх, если мы не в корне + if not is_root: + # Определяем родительскую директорию + parent_path = "/".join(current_path.split("/")[:-1]) if "/" in current_path else "" + + keyboard = [ + [InlineKeyboardButton("🔼 Вернуться на уровень выше", callback_data=f"browse_{parent_path}")] + ] + reply_markup = InlineKeyboardMarkup(keyboard) + + await message.edit_text(reply_text, parse_mode="HTML", reply_markup=reply_markup) + else: + await message.edit_text(reply_text, parse_mode="HTML") + +async def search_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /search для поиска файлов""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + # Получаем шаблон поиска из аргументов команды + if not context.args: + await update.message.reply_text("❌ Укажите шаблон для поиска: /search <шаблон>") + return + + pattern = " ".join(context.args) + + message = await update.message.reply_text(f"⏳ Поиск файлов по шаблону «{pattern}»...") + + if not synology_api.is_online(): + await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно выполнить поиск файлов.", parse_mode="HTML") + return + + try: + search_result = synology_api.search_files(pattern=pattern, limit=20) + + if not search_result.get("success", False): + error = search_result.get("error", "unknown") + progress = search_result.get("progress", 0) + + if error == "search_timeout": + await message.edit_text(f"❌ Превышено время ожидания результатов поиска\n\nПроцесс поиска выполнен на {progress}%", parse_mode="HTML") + else: + await message.edit_text(f"❌ Ошибка при поиске файлов\n\nПричина: {error}", parse_mode="HTML") + return + + files = search_result.get("results", []) + total = search_result.get("total", len(files)) + + except Exception as e: + await message.edit_text(f"❌ Ошибка при поиске файлов\n\nПричина: {str(e)}", parse_mode="HTML") + return + + # Формируем сообщение с результатами поиска + reply_text = f"🔍 Результаты поиска по шаблону «{pattern}»\n\n" + + if not files: + reply_text += "📭 Файлы не найдены" + else: + # Сортируем: сначала папки, потом файлы + folders = [] + found_files = [] + + for item in files: + name = item.get("name", "unknown") + path = item.get("path", "") + is_dir = item.get("isdir", False) + + if is_dir: + folders.append((name, path)) + else: + # Для файлов получаем размер и путь к родительской папке + size = item.get("additional", {}).get("size", 0) + size_str = format_size(size) + parent_path = "/".join(path.split("/")[:-1]) + found_files.append((name, path, size_str, parent_path)) + + # Добавляем папки в сообщение + if folders: + reply_text += "Найденные папки:\n" + for name, path in folders[:5]: # Показываем первые 5 папок + reply_text += f"📁 {name}\n" + + if len(folders) > 5: + reply_text += f"...и еще {len(folders) - 5} папок\n" + + reply_text += "\n" + + # Добавляем файлы в сообщение + if found_files: + reply_text += "Найденные файлы:\n" + for name, path, size, parent in found_files[:10]: # Показываем первые 10 файлов + icon = get_file_icon(name) + reply_text += f"{icon} {name} ({size})\n" + reply_text += f" Путь: .../{path.split('/')[-2]}/\n" + + if len(found_files) > 10: + reply_text += f"...и еще {len(found_files) - 10} файлов\n" + + # Добавляем информацию о общем количестве результатов + reply_text += f"\nВсего найдено: {total} элементов" + + await message.edit_text(reply_text, parse_mode="HTML") + +async def updates_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /updates для проверки обновлений""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + message = await update.message.reply_text("⏳ Проверка доступных обновлений...") + + if not synology_api.is_online(): + await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно проверить наличие обновлений.", parse_mode="HTML") + return + + try: + update_info = synology_api.check_for_updates() + + if not update_info.get("success", False): + error = update_info.get("error", "unknown") + await message.edit_text(f"❌ Ошибка при проверке обновлений\n\nПричина: {error}", parse_mode="HTML") + return + + current_version = update_info.get("current_version", "unknown") + update_available = update_info.get("update_available", False) + auto_update = update_info.get("auto_update_enabled", False) + updates = update_info.get("updates", []) + + except Exception as e: + await message.edit_text(f"❌ Ошибка при проверке обновлений\n\nПричина: {str(e)}", parse_mode="HTML") + return + + # Формируем сообщение об обновлениях + if update_available: + reply_text = f"🔄 Доступны обновления DSM\n\n" + reply_text += f"Текущая версия: {current_version}\n" + reply_text += f"Автообновление: {'✅ Включено' if auto_update else '❌ Отключено'}\n\n" + reply_text += "Доступные обновления:\n" + + for update_item in updates: + update_name = update_item.get("name", "unknown") + update_version = update_item.get("version", "unknown") + update_size = update_item.get("size", 0) + update_size_str = format_size(update_size) + + reply_text += f"• {update_name} v{update_version}\n" + reply_text += f" └ Размер: {update_size_str}\n" + else: + reply_text = f"✅ Система в актуальном состоянии\n\n" + reply_text += f"Текущая версия: {current_version}\n" + reply_text += f"Автообновление: {'✅ Включено' if auto_update else '❌ Отключено'}\n" + + await message.edit_text(reply_text, parse_mode="HTML") + +async def backup_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /backup для управления резервным копированием""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + message = await update.message.reply_text("⏳ Получение информации о резервном копировании...") + + if not synology_api.is_online(): + await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о резервном копировании.", parse_mode="HTML") + return + + try: + backup_status = synology_api.get_backup_status() + + if not backup_status.get("success", False): + error = backup_status.get("error", "unknown") + await message.edit_text(f"❌ Ошибка получения информации о резервном копировании\n\nПричина: {error}", parse_mode="HTML") + return + + backups = backup_status.get("backups", {}) + api_status = backup_status.get("available_apis", {}) + + except Exception as e: + await message.edit_text(f"❌ Ошибка получения информации о резервном копировании\n\nПричина: {str(e)}", parse_mode="HTML") + return + + # Формируем сообщение о резервном копировании + reply_text = f"💾 Резервное копирование Synology NAS\n\n" + + # Информация о Hyper Backup + hyper_backups = backups.get("hyper_backup", []) + hyper_api_available = api_status.get("hyper_backup", False) + + if hyper_api_available: + reply_text += "Hyper Backup:\n" + + if hyper_backups: + for backup in hyper_backups: + name = backup.get("name", "unknown") + status = backup.get("status", "unknown") + last_backup = backup.get("last_backup", "never") + + status_emoji = "✅" if status.lower() == "success" else "⚠️" + reply_text += f"• {status_emoji} {name}\n" + reply_text += f" └ Последнее копирование: {last_backup}\n" + else: + reply_text += "Задачи Hyper Backup не настроены\n" + + reply_text += "\n" + + # Информация о Time Backup + time_backups = backups.get("time_backup", []) + time_api_available = api_status.get("time_backup", False) + + if time_api_available: + reply_text += "Time Backup:\n" + + if time_backups: + for backup in time_backups: + name = backup.get("name", "unknown") + status = backup.get("status", "unknown") + + status_emoji = "✅" if status.lower() == "normal" else "⚠️" + reply_text += f"• {status_emoji} {name}\n" + else: + reply_text += "Задачи Time Backup не настроены\n" + + reply_text += "\n" + + # Информация о USB Copy + usb_copy = backups.get("usb_copy", {}) + usb_api_available = api_status.get("usb_copy", False) + + if usb_api_available: + usb_enabled = usb_copy.get("enabled", False) + usb_status = "✅ Включено" if usb_enabled else "❌ Отключено" + + reply_text += f"USB Copy: {usb_status}\n\n" + + # Если ни один из API не доступен + if not any(api_status.values()): + reply_text += "API для резервного копирования не доступны на вашем NAS или требуют дополнительных прав доступа.\n" + + await message.edit_text(reply_text, parse_mode="HTML") + +async def reboot_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /reboot для перезагрузки NAS""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + # Добавляем подтверждение перед перезагрузкой + keyboard = [ + [ + InlineKeyboardButton("✅ Да, перезагрузить", callback_data="confirm_reboot"), + InlineKeyboardButton("❌ Отмена", callback_data="cancel_reboot") + ] + ] + reply_markup = InlineKeyboardMarkup(keyboard) + + await update.message.reply_text( + "⚠️ Вы уверены, что хотите перезагрузить Synology NAS?\n\n" + "Это действие может привести к прерыванию работы всех сервисов.", + parse_mode="HTML", + reply_markup=reply_markup + ) + +async def sleep_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /sleep для перевода NAS в спящий режим""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + # Добавляем подтверждение перед отправкой в спящий режим + keyboard = [ + [ + InlineKeyboardButton("✅ Да, усыпить", callback_data="confirm_sleep"), + InlineKeyboardButton("❌ Отмена", callback_data="cancel_sleep") + ] + ] + reply_markup = InlineKeyboardMarkup(keyboard) + + await update.message.reply_text( + "⚠️ Вы уверены, что хотите перевести Synology NAS в спящий режим?\n\n" + "Это действие приведет к остановке всех сервисов и отключению NAS.", + parse_mode="HTML", + reply_markup=reply_markup + ) + +async def quickreboot_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /quickreboot для быстрой перезагрузки NAS""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + message = await update.message.reply_text("⏳ Выполняется перезагрузка Synology NAS...") + + if not synology_api.is_online(): + await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно выполнить перезагрузку.", parse_mode="HTML") + return + + try: + # Выполняем перезагрузку + result = synology_api.reboot_system() + + if result: + # Формируем сообщение об успешной перезагрузке + reply_text = "🔄 Synology NAS перезагружается\n\n" + reply_text += "Перезагрузка обычно занимает 3-5 минут. После перезагрузки NAS будет снова доступен." + await message.edit_text(reply_text, parse_mode="HTML") + else: + await message.edit_text("❌ Ошибка при выполнении перезагрузки\n\nПроверьте журналы NAS для получения дополнительной информации.", parse_mode="HTML") + + except Exception as e: + await message.edit_text(f"❌ Ошибка при выполнении перезагрузки\n\nПричина: {str(e)}", parse_mode="HTML") + return + +async def wakeup_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /wakeup для включения NAS""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + message = await update.message.reply_text("⏳ Отправка пакета Wake-on-LAN для включения Synology NAS...") + + # Проверяем, не включен ли NAS уже + if synology_api.is_online(force_check=True): + await message.edit_text("ℹ️ Synology NAS уже включен\n\nНет необходимости отправлять сигнал пробуждения.", parse_mode="HTML") + return + + try: + # Отправляем сигнал пробуждения + result = synology_api.power_on() + + if result: + # Формируем сообщение об успешном включении + reply_text = "✅ Synology NAS успешно включен\n\n" + reply_text += "NAS полностью готов к работе." + await message.edit_text(reply_text, parse_mode="HTML") + else: + await message.edit_text("❌ Ошибка при включении NAS\n\nВозможные причины:\n- Функция Wake-on-LAN не настроена на NAS\n- Неверно указан MAC-адрес\n- Проблемы с сетевым подключением", parse_mode="HTML") + + except Exception as e: + await message.edit_text(f"❌ Ошибка при включении NAS\n\nПричина: {str(e)}", parse_mode="HTML") + return + +async def quota_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /quota для просмотра информации о квотах""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + message = await update.message.reply_text("⏳ Получение информации о квотах пользователей...") + + if not synology_api.is_online(): + await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о квотах.", parse_mode="HTML") + return + + try: + quota_info = synology_api.get_quota_info() + + if not quota_info.get("success", False): + error = quota_info.get("error", "unknown") + await message.edit_text(f"❌ Ошибка получения информации о квотах\n\nПричина: {error}", parse_mode="HTML") + return + + user_quotas = quota_info.get("user_quotas", []) + + except Exception as e: + await message.edit_text(f"❌ Ошибка получения информации о квотах\n\nПричина: {str(e)}", parse_mode="HTML") + return + + # Формируем сообщение о квотах + reply_text = f"📊 Квоты пользователей Synology NAS\n\n" + + if not user_quotas: + reply_text += "Квоты пользователей не настроены или недоступны" + else: + for user_quota in user_quotas: + user = user_quota.get("user", "unknown") + quotas = user_quota.get("quotas", []) + + if quotas: + reply_text += f"Пользователь {user}:\n" + + for quota in quotas: + volume = quota.get("volume_name", "unknown") + limit = quota.get("limit", 0) + used = quota.get("used", 0) + + # Переводим байты в ГБ + limit_gb = limit / (1024**3) if limit > 0 else 0 + used_gb = used / (1024**3) + + # Рассчитываем процент использования + if limit_gb > 0: + usage_percent = (used_gb / limit_gb) * 100 + reply_text += f"• Том {volume}: {used_gb:.2f} ГБ из {limit_gb:.2f} ГБ ({usage_percent:.1f}%)\n" + else: + reply_text += f"• Том {volume}: {used_gb:.2f} ГБ (без ограничений)\n" + + reply_text += "\n" + + await message.edit_text(reply_text, parse_mode="HTML") + +async def schedule_callback(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик callback-запросов для управления расписанием питания""" + query = update.callback_query + await query.answer() + + user_id = update.effective_user.id + if user_id not in ADMIN_USER_IDS: + await query.edit_message_text("У вас нет доступа к этому боту.") + return + + action = query.data + + if action.startswith("schedule_"): + action_type = action.split("_")[1] + + if action_type == "add_boot": + # Логика добавления расписания включения + # В реальном боте здесь будет диалог для настройки расписания + await query.edit_message_text("⚙️ Добавление расписания включения\n\nЭта функция находится в разработке.", parse_mode="HTML") + + elif action_type == "add_shutdown": + # Логика добавления расписания выключения + await query.edit_message_text("⚙️ Добавление расписания выключения\n\nЭта функция находится в разработке.", parse_mode="HTML") + + elif action_type == "delete": + # Логика удаления расписания + await query.edit_message_text("⚙️ Удаление расписания\n\nЭта функция находится в разработке.", parse_mode="HTML") + +async def browse_callback(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик callback-запросов для навигации по файловой системе""" + query = update.callback_query + await query.answer() + + user_id = update.effective_user.id + if user_id not in ADMIN_USER_IDS: + await query.edit_message_text("У вас нет доступа к этому боту.") + return + + action = query.data + + if action.startswith("browse_"): + path = action[7:] # Убираем префикс "browse_" + + # Используем команду browse с указанным путем + message = await query.edit_message_text(f"⏳ Получение содержимого {'папки ' + path if path else 'общих папок'}...") + + if not synology_api.is_online(): + await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить список файлов.", parse_mode="HTML") + return + + try: + browse_result = synology_api.browse_files(folder_path=path) + + if not browse_result.get("success", False): + error = browse_result.get("error", "unknown") + await message.edit_text(f"❌ Ошибка получения списка файлов\n\nПричина: {error}", parse_mode="HTML") + return + + items = browse_result.get("items", []) + current_path = browse_result.get("path", "") + is_root = browse_result.get("is_root", True) + + except Exception as e: + await message.edit_text(f"❌ Ошибка получения списка файлов\n\nПричина: {str(e)}", parse_mode="HTML") + return + + # Формируем сообщение о файлах и папках (аналогично функции browse_command) + if is_root: + reply_text = f"📁 Общие папки Synology NAS\n\n" + else: + reply_text = f"📁 Содержимое папки\n{current_path}\n\n" + + # Сортируем: сначала папки, потом файлы + folders = [] + files = [] + + for item in items: + if is_root: # Для корневого уровня все элементы - это общие папки + name = item.get("name", "unknown") + path = item.get("path", "") + folders.append((name, path, True)) + else: + name = item.get("name", "unknown") + path = item.get("path", "") + is_dir = item.get("isdir", False) + + if is_dir: + folders.append((name, path, False)) + else: + # Для файлов получаем размер + size = item.get("additional", {}).get("size", 0) + size_str = format_size(size) + files.append((name, path, size_str)) + + # Добавляем папки в сообщение + if folders: + for name, path, is_share in folders: + # Для общих папок добавляем иконку дома + icon = "🏠" if is_share else "📁" + reply_text += f"{icon} {name}\n" + + # Добавляем файлы в сообщение + if files: + for name, path, size in files: + # Выбираем иконку в зависимости от расширения + icon = get_file_icon(name) + reply_text += f"{icon} {name} ({size})\n" + + # Если нет элементов для отображения + if not folders and not files: + reply_text += "📭 Папка пуста\n" + + # Добавляем кнопку возврата наверх, если мы не в корне + if not is_root: + # Определяем родительскую директорию + parent_path = "/".join(current_path.split("/")[:-1]) if "/" in current_path else "" + + keyboard = [ + [InlineKeyboardButton("🔼 Вернуться на уровень выше", callback_data=f"browse_{parent_path}")] + ] + reply_markup = InlineKeyboardMarkup(keyboard) + + await message.edit_text(reply_text, parse_mode="HTML", reply_markup=reply_markup) + else: + await message.edit_text(reply_text, parse_mode="HTML") + +async def advanced_power_callback(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик callback-запросов для управления питанием""" + query = update.callback_query + await query.answer() + + user_id = update.effective_user.id + if user_id not in ADMIN_USER_IDS: + await query.edit_message_text("У вас нет доступа к этому боту.") + return + + action = query.data + + if action == "confirm_reboot": + # Выполняем перезагрузку + message = await query.edit_message_text("⏳ Выполняется перезагрузка Synology NAS...") + + if not synology_api.is_online(): + await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно выполнить перезагрузку.", parse_mode="HTML") + return + + try: + result = synology_api.reboot_system() + + if result: + reply_text = "🔄 Synology NAS перезагружается\n\n" + reply_text += "Перезагрузка обычно занимает 3-5 минут. После перезагрузки NAS будет снова доступен." + await message.edit_text(reply_text, parse_mode="HTML") + else: + await message.edit_text("❌ Ошибка при выполнении перезагрузки\n\nПроверьте журналы NAS для получения дополнительной информации.", parse_mode="HTML") + except Exception as e: + await message.edit_text(f"❌ Ошибка при выполнении перезагрузки\n\nПричина: {str(e)}", parse_mode="HTML") + + elif action == "cancel_reboot": + # Отменяем перезагрузку + await query.edit_message_text("✅ Перезагрузка отменена", parse_mode="HTML") + + elif action == "confirm_sleep": + # Выполняем переход в спящий режим (выключение) + message = await query.edit_message_text("⏳ Перевод Synology NAS в спящий режим...") + + if not synology_api.is_online(): + await message.edit_text("❌ Synology NAS уже оффлайн\n\nНевозможно выполнить переход в спящий режим.", parse_mode="HTML") + return + + try: + result = synology_api.power_off() + + if result: + reply_text = "💤 Synology NAS переведен в спящий режим\n\n" + reply_text += "Для пробуждения используйте команду /wakeup" + await message.edit_text(reply_text, parse_mode="HTML") + else: + await message.edit_text("❌ Ошибка при переходе в спящий режим\n\nПроверьте журналы NAS для получения дополнительной информации.", parse_mode="HTML") + except Exception as e: + await message.edit_text(f"❌ Ошибка при переходе в спящий режим\n\nПричина: {str(e)}", parse_mode="HTML") + + elif action == "cancel_sleep": + # Отменяем переход в спящий режим + await query.edit_message_text("✅ Переход в спящий режим отменен", parse_mode="HTML") + +# Вспомогательные функции + +def format_size(size_bytes: int) -> str: + """Преобразует размер в байтах в человекочитаемый формат""" + if size_bytes < 1024: + return f"{size_bytes} Б" + elif size_bytes < 1024**2: + return f"{size_bytes/1024:.1f} КБ" + elif size_bytes < 1024**3: + return f"{size_bytes/1024**2:.1f} МБ" + else: + return f"{size_bytes/1024**3:.1f} ГБ" + +def get_file_icon(filename: str) -> str: + """Возвращает эмодзи-иконку в зависимости от типа файла""" + extension = filename.lower().split('.')[-1] if '.' in filename else '' + + if extension in ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'tiff', 'webp']: + return "🖼️" + elif extension in ['mp4', 'avi', 'mov', 'mkv', 'wmv', 'flv']: + return "🎬" + elif extension in ['mp3', 'wav', 'ogg', 'flac', 'aac']: + return "🎵" + elif extension in ['doc', 'docx', 'txt', 'rtf', 'odt']: + return "📄" + elif extension in ['xls', 'xlsx', 'csv']: + return "📊" + elif extension in ['ppt', 'pptx']: + return "📑" + elif extension in ['pdf']: + return "📕" + elif extension in ['zip', 'rar', '7z', 'tar', 'gz']: + return "🗜️" + elif extension in ['exe', 'msi']: + return "⚙️" + else: + return "📄" diff --git a/.history/src/handlers/advanced_handlers_20250830094738.py b/.history/src/handlers/advanced_handlers_20250830094738.py new file mode 100644 index 0000000..9cc9ee6 --- /dev/null +++ b/.history/src/handlers/advanced_handlers_20250830094738.py @@ -0,0 +1,972 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Расширенные обработчики команд для управления Synology NAS +""" + +import logging +from datetime import datetime +from typing import List, Dict, Any +from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup +from telegram.ext import ContextTypes + +from src.config.config import ADMIN_USER_IDS +from src.api.synology import SynologyAPI + +logger = logging.getLogger(__name__) + +# Инициализация API Synology +synology_api = SynologyAPI() + +async def processes_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /processes для получения списка активных процессов""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + message = await update.message.reply_text("⏳ Получение информации о запущенных процессах...") + + if not synology_api.is_online(): + await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о процессах.", parse_mode="HTML") + return + + try: + processes = synology_api.get_processes(limit=15) # Получаем топ-15 процессов + + if not processes: + await message.edit_text("❌ Ошибка получения информации о процессах\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML") + return + except Exception as e: + await message.edit_text(f"❌ Ошибка получения информации о процессах\n\nПричина: {str(e)}", parse_mode="HTML") + return + + # Формируем сообщение о процессах + reply_text = f"⚙️ Активные процессы Synology NAS\n\n" + + for process in processes: + name = process.get("name", "unknown") + pid = process.get("pid", "?") + cpu_usage = process.get("cpu_usage", 0) + memory_usage = process.get("memory_usage", 0) + + reply_text += f"• {name} (PID: {pid})\n" + reply_text += f" └ CPU: {cpu_usage:.1f}%, Память: {memory_usage:.1f}%\n" + + reply_text += f"\nПоказано {len(processes)} наиболее активных процессов" + await message.edit_text(reply_text, parse_mode="HTML") + +async def network_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /network для получения информации о сетевых подключениях""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + message = await update.message.reply_text("⏳ Получение информации о сетевых подключениях...") + + if not synology_api.is_online(): + await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о сетевых подключениях.", parse_mode="HTML") + return + + try: + network_status = synology_api.get_network_status() + + if not network_status: + await message.edit_text("❌ Ошибка получения информации о сетевых подключениях\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML") + return + except Exception as e: + await message.edit_text(f"❌ Ошибка получения информации о сетевых подключениях\n\nПричина: {str(e)}", parse_mode="HTML") + return + + # Формируем сообщение о сетевых интерфейсах + interfaces = network_status.get("interfaces", []) + + reply_text = f"🌐 Сетевые подключения Synology NAS\n\n" + + for interface in interfaces: + name = interface.get("id", "unknown") + ip = interface.get("ip", "Нет данных") + mac = interface.get("mac", "Нет данных") + status = "Активен" if interface.get("status") else "Неактивен" + + # Информация о трафике + rx_bytes = interface.get("rx", 0) / (1024**2) # Перевод в МБ + tx_bytes = interface.get("tx", 0) / (1024**2) # Перевод в МБ + + reply_text += f"• {name} ({status})\n" + reply_text += f" └ IP: {ip}, MAC: {mac}\n" + + if rx_bytes > 0 or tx_bytes > 0: + reply_text += f" └ Получено: {rx_bytes:.2f} МБ, Отправлено: {tx_bytes:.2f} МБ\n" + + await message.edit_text(reply_text, parse_mode="HTML") + +async def temperature_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /temperature для мониторинга температуры""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + message = await update.message.reply_text("⏳ Получение информации о температуре...") + + if not synology_api.is_online(): + await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о температуре.", parse_mode="HTML") + return + + try: + temp_status = synology_api.get_temperature_status() + + if not temp_status: + await message.edit_text("❌ Ошибка получения информации о температуре\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML") + return + except Exception as e: + await message.edit_text(f"❌ Ошибка получения информации о температуре\n\nПричина: {str(e)}", parse_mode="HTML") + return + + # Формируем сообщение о температуре + system_temp = temp_status.get("system_temperature") + disk_temps = temp_status.get("disk_temperatures", []) + is_warning = temp_status.get("warning", False) + + # Выбор emoji в зависимости от температуры + temp_emoji = "🔥" if is_warning else "🌡️" + + reply_text = f"{temp_emoji} Температура Synology NAS\n\n" + + if system_temp is not None: + temp_status_text = "❗ ПОВЫШЕННАЯ" if is_warning else "✅ Нормальная" + reply_text += f"Температура системы: {system_temp}°C ({temp_status_text})\n\n" + + if disk_temps: + reply_text += "Температура дисков:\n" + for disk in disk_temps: + name = disk.get("name", "unknown") + model = disk.get("model", "unknown") + temp = disk.get("temperature", 0) + + disk_temp_emoji = "🔥" if temp > 45 else "✅" + reply_text += f"• {disk_temp_emoji} {name} ({model}): {temp}°C\n" + + await message.edit_text(reply_text, parse_mode="HTML") + +async def schedule_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /schedule для управления расписанием питания""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + message = await update.message.reply_text("⏳ Получение информации о расписании питания...") + + if not synology_api.is_online(): + await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о расписании питания.", parse_mode="HTML") + return + + try: + schedule = synology_api.get_power_schedule() + + if not schedule: + await message.edit_text("❌ Ошибка получения информации о расписании питания\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML") + return + except Exception as e: + await message.edit_text(f"❌ Ошибка получения информации о расписании питания\n\nПричина: {str(e)}", parse_mode="HTML") + return + + # Формируем сообщение о расписании питания + boot_tasks = schedule.get("boot_tasks", []) + shutdown_tasks = schedule.get("shutdown_tasks", []) + + reply_text = f"⏱️ Расписание питания Synology NAS\n\n" + + if boot_tasks: + reply_text += "Расписание включения:\n" + for task in boot_tasks: + days = task.get("day", []) + time = task.get("time", "00:00") + enabled = task.get("enabled", False) + + # Преобразуем номера дней в названия + day_names = [] + for day in days: + if day == 0: day_names.append("Пн") + elif day == 1: day_names.append("Вт") + elif day == 2: day_names.append("Ср") + elif day == 3: day_names.append("Чт") + elif day == 4: day_names.append("Пт") + elif day == 5: day_names.append("Сб") + elif day == 6: day_names.append("Вс") + + status = "✅ Активно" if enabled else "❌ Отключено" + day_str = ", ".join(day_names) if day_names else "Нет дней" + + reply_text += f"• {status}: {time} ({day_str})\n" + else: + reply_text += "Расписание включения: Не настроено\n" + + reply_text += "\n" + + if shutdown_tasks: + reply_text += "Расписание выключения:\n" + for task in shutdown_tasks: + days = task.get("day", []) + time = task.get("time", "00:00") + enabled = task.get("enabled", False) + + # Преобразуем номера дней в названия + day_names = [] + for day in days: + if day == 0: day_names.append("Пн") + elif day == 1: day_names.append("Вт") + elif day == 2: day_names.append("Ср") + elif day == 3: day_names.append("Чт") + elif day == 4: day_names.append("Пт") + elif day == 5: day_names.append("Сб") + elif day == 6: day_names.append("Вс") + + status = "✅ Активно" if enabled else "❌ Отключено" + day_str = ", ".join(day_names) if day_names else "Нет дней" + + reply_text += f"• {status}: {time} ({day_str})\n" + else: + reply_text += "Расписание выключения: Не настроено\n" + + # Добавляем кнопки для управления расписанием + keyboard = [ + [ + InlineKeyboardButton("➕ Добавить включение", callback_data="schedule_add_boot"), + InlineKeyboardButton("➕ Добавить выключение", callback_data="schedule_add_shutdown") + ], + [ + InlineKeyboardButton("🗑️ Удалить расписание", callback_data="schedule_delete") + ] + ] + reply_markup = InlineKeyboardMarkup(keyboard) + + await message.edit_text(reply_text, parse_mode="HTML", reply_markup=reply_markup) + +async def browse_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /browse для просмотра файлов""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + # Получаем путь из аргументов команды или используем корневую директорию + path = " ".join(context.args) if context.args else "" + + message = await update.message.reply_text(f"⏳ Получение содержимого {'папки ' + path if path else 'общих папок'}...") + + if not synology_api.is_online(): + await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить список файлов.", parse_mode="HTML") + return + + try: + browse_result = synology_api.browse_files(folder_path=path) + + if not browse_result.get("success", False): + error = browse_result.get("error", "unknown") + await message.edit_text(f"❌ Ошибка получения списка файлов\n\nПричина: {error}", parse_mode="HTML") + return + + items = browse_result.get("items", []) + current_path = browse_result.get("path", "") + is_root = browse_result.get("is_root", True) + + except Exception as e: + await message.edit_text(f"❌ Ошибка получения списка файлов\n\nПричина: {str(e)}", parse_mode="HTML") + return + + # Формируем сообщение о файлах и папках + if is_root: + reply_text = f"📁 Общие папки Synology NAS\n\n" + else: + reply_text = f"📁 Содержимое папки\n{current_path}\n\n" + + # Сортируем: сначала папки, потом файлы + folders = [] + files = [] + + for item in items: + if is_root: # Для корневого уровня все элементы - это общие папки + name = item.get("name", "unknown") + path = item.get("path", "") + folders.append((name, path, True)) + else: + name = item.get("name", "unknown") + path = item.get("path", "") + is_dir = item.get("isdir", False) + + if is_dir: + folders.append((name, path, False)) + else: + # Для файлов получаем размер + size = item.get("additional", {}).get("size", 0) + size_str = format_size(size) + files.append((name, path, size_str)) + + # Добавляем папки в сообщение + if folders: + for name, path, is_share in folders: + # Для общих папок добавляем иконку дома + icon = "🏠" if is_share else "📁" + reply_text += f"{icon} {name}\n" + + # Добавляем файлы в сообщение + if files: + for name, path, size in files: + # Выбираем иконку в зависимости от расширения + icon = get_file_icon(name) + reply_text += f"{icon} {name} ({size})\n" + + # Если нет элементов для отображения + if not folders and not files: + reply_text += "📭 Папка пуста\n" + + # Добавляем кнопку возврата наверх, если мы не в корне + if not is_root: + # Определяем родительскую директорию + parent_path = "/".join(current_path.split("/")[:-1]) if "/" in current_path else "" + + keyboard = [ + [InlineKeyboardButton("🔼 Вернуться на уровень выше", callback_data=f"browse_{parent_path}")] + ] + reply_markup = InlineKeyboardMarkup(keyboard) + + await message.edit_text(reply_text, parse_mode="HTML", reply_markup=reply_markup) + else: + await message.edit_text(reply_text, parse_mode="HTML") + +async def search_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /search для поиска файлов""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + # Получаем шаблон поиска из аргументов команды + if not context.args: + await update.message.reply_text("❌ Укажите шаблон для поиска: /search <шаблон>") + return + + pattern = " ".join(context.args) + + message = await update.message.reply_text(f"⏳ Поиск файлов по шаблону «{pattern}»...") + + if not synology_api.is_online(): + await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно выполнить поиск файлов.", parse_mode="HTML") + return + + try: + search_result = synology_api.search_files(pattern=pattern, limit=20) + + if not search_result.get("success", False): + error = search_result.get("error", "unknown") + progress = search_result.get("progress", 0) + + if error == "search_timeout": + await message.edit_text(f"❌ Превышено время ожидания результатов поиска\n\nПроцесс поиска выполнен на {progress}%", parse_mode="HTML") + else: + await message.edit_text(f"❌ Ошибка при поиске файлов\n\nПричина: {error}", parse_mode="HTML") + return + + files = search_result.get("results", []) + total = search_result.get("total", len(files)) + + except Exception as e: + await message.edit_text(f"❌ Ошибка при поиске файлов\n\nПричина: {str(e)}", parse_mode="HTML") + return + + # Формируем сообщение с результатами поиска + reply_text = f"🔍 Результаты поиска по шаблону «{pattern}»\n\n" + + if not files: + reply_text += "📭 Файлы не найдены" + else: + # Сортируем: сначала папки, потом файлы + folders = [] + found_files = [] + + for item in files: + name = item.get("name", "unknown") + path = item.get("path", "") + is_dir = item.get("isdir", False) + + if is_dir: + folders.append((name, path)) + else: + # Для файлов получаем размер и путь к родительской папке + size = item.get("additional", {}).get("size", 0) + size_str = format_size(size) + parent_path = "/".join(path.split("/")[:-1]) + found_files.append((name, path, size_str, parent_path)) + + # Добавляем папки в сообщение + if folders: + reply_text += "Найденные папки:\n" + for name, path in folders[:5]: # Показываем первые 5 папок + reply_text += f"📁 {name}\n" + + if len(folders) > 5: + reply_text += f"...и еще {len(folders) - 5} папок\n" + + reply_text += "\n" + + # Добавляем файлы в сообщение + if found_files: + reply_text += "Найденные файлы:\n" + for name, path, size, parent in found_files[:10]: # Показываем первые 10 файлов + icon = get_file_icon(name) + reply_text += f"{icon} {name} ({size})\n" + reply_text += f" Путь: .../{path.split('/')[-2]}/\n" + + if len(found_files) > 10: + reply_text += f"...и еще {len(found_files) - 10} файлов\n" + + # Добавляем информацию о общем количестве результатов + reply_text += f"\nВсего найдено: {total} элементов" + + await message.edit_text(reply_text, parse_mode="HTML") + +async def updates_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /updates для проверки обновлений""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + message = await update.message.reply_text("⏳ Проверка доступных обновлений...") + + if not synology_api.is_online(): + await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно проверить наличие обновлений.", parse_mode="HTML") + return + + try: + update_info = synology_api.check_for_updates() + + if not update_info.get("success", False): + error = update_info.get("error", "unknown") + await message.edit_text(f"❌ Ошибка при проверке обновлений\n\nПричина: {error}", parse_mode="HTML") + return + + current_version = update_info.get("current_version", "unknown") + update_available = update_info.get("update_available", False) + auto_update = update_info.get("auto_update_enabled", False) + updates = update_info.get("updates", []) + + except Exception as e: + await message.edit_text(f"❌ Ошибка при проверке обновлений\n\nПричина: {str(e)}", parse_mode="HTML") + return + + # Формируем сообщение об обновлениях + if update_available: + reply_text = f"🔄 Доступны обновления DSM\n\n" + reply_text += f"Текущая версия: {current_version}\n" + reply_text += f"Автообновление: {'✅ Включено' if auto_update else '❌ Отключено'}\n\n" + reply_text += "Доступные обновления:\n" + + for update_item in updates: + update_name = update_item.get("name", "unknown") + update_version = update_item.get("version", "unknown") + update_size = update_item.get("size", 0) + update_size_str = format_size(update_size) + + reply_text += f"• {update_name} v{update_version}\n" + reply_text += f" └ Размер: {update_size_str}\n" + else: + reply_text = f"✅ Система в актуальном состоянии\n\n" + reply_text += f"Текущая версия: {current_version}\n" + reply_text += f"Автообновление: {'✅ Включено' if auto_update else '❌ Отключено'}\n" + + await message.edit_text(reply_text, parse_mode="HTML") + +async def backup_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /backup для управления резервным копированием""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + message = await update.message.reply_text("⏳ Получение информации о резервном копировании...") + + if not synology_api.is_online(): + await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о резервном копировании.", parse_mode="HTML") + return + + try: + backup_status = synology_api.get_backup_status() + + if not backup_status.get("success", False): + error = backup_status.get("error", "unknown") + await message.edit_text(f"❌ Ошибка получения информации о резервном копировании\n\nПричина: {error}", parse_mode="HTML") + return + + backups = backup_status.get("backups", {}) + api_status = backup_status.get("available_apis", {}) + + except Exception as e: + await message.edit_text(f"❌ Ошибка получения информации о резервном копировании\n\nПричина: {str(e)}", parse_mode="HTML") + return + + # Формируем сообщение о резервном копировании + reply_text = f"💾 Резервное копирование Synology NAS\n\n" + + # Информация о Hyper Backup + hyper_backups = backups.get("hyper_backup", []) + hyper_api_available = api_status.get("hyper_backup", False) + + if hyper_api_available: + reply_text += "Hyper Backup:\n" + + if hyper_backups: + for backup in hyper_backups: + name = backup.get("name", "unknown") + status = backup.get("status", "unknown") + last_backup = backup.get("last_backup", "never") + + status_emoji = "✅" if status.lower() == "success" else "⚠️" + reply_text += f"• {status_emoji} {name}\n" + reply_text += f" └ Последнее копирование: {last_backup}\n" + else: + reply_text += "Задачи Hyper Backup не настроены\n" + + reply_text += "\n" + + # Информация о Time Backup + time_backups = backups.get("time_backup", []) + time_api_available = api_status.get("time_backup", False) + + if time_api_available: + reply_text += "Time Backup:\n" + + if time_backups: + for backup in time_backups: + name = backup.get("name", "unknown") + status = backup.get("status", "unknown") + + status_emoji = "✅" if status.lower() == "normal" else "⚠️" + reply_text += f"• {status_emoji} {name}\n" + else: + reply_text += "Задачи Time Backup не настроены\n" + + reply_text += "\n" + + # Информация о USB Copy + usb_copy = backups.get("usb_copy", {}) + usb_api_available = api_status.get("usb_copy", False) + + if usb_api_available: + usb_enabled = usb_copy.get("enabled", False) + usb_status = "✅ Включено" if usb_enabled else "❌ Отключено" + + reply_text += f"USB Copy: {usb_status}\n\n" + + # Если ни один из API не доступен + if not any(api_status.values()): + reply_text += "API для резервного копирования не доступны на вашем NAS или требуют дополнительных прав доступа.\n" + + await message.edit_text(reply_text, parse_mode="HTML") + +async def reboot_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /reboot для перезагрузки NAS""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + # Добавляем подтверждение перед перезагрузкой + keyboard = [ + [ + InlineKeyboardButton("✅ Да, перезагрузить", callback_data="confirm_reboot"), + InlineKeyboardButton("❌ Отмена", callback_data="cancel_reboot") + ] + ] + reply_markup = InlineKeyboardMarkup(keyboard) + + await update.message.reply_text( + "⚠️ Вы уверены, что хотите перезагрузить Synology NAS?\n\n" + "Это действие может привести к прерыванию работы всех сервисов.", + parse_mode="HTML", + reply_markup=reply_markup + ) + +async def sleep_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /sleep для перевода NAS в спящий режим""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + # Добавляем подтверждение перед отправкой в спящий режим + keyboard = [ + [ + InlineKeyboardButton("✅ Да, усыпить", callback_data="confirm_sleep"), + InlineKeyboardButton("❌ Отмена", callback_data="cancel_sleep") + ] + ] + reply_markup = InlineKeyboardMarkup(keyboard) + + await update.message.reply_text( + "⚠️ Вы уверены, что хотите перевести Synology NAS в спящий режим?\n\n" + "Это действие приведет к остановке всех сервисов и отключению NAS.", + parse_mode="HTML", + reply_markup=reply_markup + ) + +async def quickreboot_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /quickreboot для быстрой перезагрузки NAS""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + message = await update.message.reply_text("⏳ Выполняется перезагрузка Synology NAS...") + + if not synology_api.is_online(): + await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно выполнить перезагрузку.", parse_mode="HTML") + return + + try: + # Выполняем перезагрузку + result = synology_api.reboot_system() + + if result: + # Формируем сообщение об успешной перезагрузке + reply_text = "🔄 Synology NAS перезагружается\n\n" + reply_text += "Перезагрузка обычно занимает 3-5 минут. После перезагрузки NAS будет снова доступен." + await message.edit_text(reply_text, parse_mode="HTML") + else: + await message.edit_text("❌ Ошибка при выполнении перезагрузки\n\nПроверьте журналы NAS для получения дополнительной информации.", parse_mode="HTML") + + except Exception as e: + await message.edit_text(f"❌ Ошибка при выполнении перезагрузки\n\nПричина: {str(e)}", parse_mode="HTML") + return + +async def wakeup_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /wakeup для включения NAS""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + message = await update.message.reply_text("⏳ Отправка пакета Wake-on-LAN для включения Synology NAS...") + + # Проверяем, не включен ли NAS уже + if synology_api.is_online(force_check=True): + await message.edit_text("ℹ️ Synology NAS уже включен\n\nНет необходимости отправлять сигнал пробуждения.", parse_mode="HTML") + return + + try: + # Отправляем сигнал пробуждения + result = synology_api.power_on() + + if result: + # Формируем сообщение об успешном включении + reply_text = "✅ Synology NAS успешно включен\n\n" + reply_text += "NAS полностью готов к работе." + await message.edit_text(reply_text, parse_mode="HTML") + else: + await message.edit_text("❌ Ошибка при включении NAS\n\nВозможные причины:\n- Функция Wake-on-LAN не настроена на NAS\n- Неверно указан MAC-адрес\n- Проблемы с сетевым подключением", parse_mode="HTML") + + except Exception as e: + await message.edit_text(f"❌ Ошибка при включении NAS\n\nПричина: {str(e)}", parse_mode="HTML") + return + +async def quota_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /quota для просмотра информации о квотах""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + message = await update.message.reply_text("⏳ Получение информации о квотах пользователей...") + + if not synology_api.is_online(): + await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о квотах.", parse_mode="HTML") + return + + try: + quota_info = synology_api.get_quota_info() + + if not quota_info.get("success", False): + error = quota_info.get("error", "unknown") + await message.edit_text(f"❌ Ошибка получения информации о квотах\n\nПричина: {error}", parse_mode="HTML") + return + + user_quotas = quota_info.get("user_quotas", []) + + except Exception as e: + await message.edit_text(f"❌ Ошибка получения информации о квотах\n\nПричина: {str(e)}", parse_mode="HTML") + return + + # Формируем сообщение о квотах + reply_text = f"📊 Квоты пользователей Synology NAS\n\n" + + if not user_quotas: + reply_text += "Квоты пользователей не настроены или недоступны" + else: + for user_quota in user_quotas: + user = user_quota.get("user", "unknown") + quotas = user_quota.get("quotas", []) + + if quotas: + reply_text += f"Пользователь {user}:\n" + + for quota in quotas: + volume = quota.get("volume_name", "unknown") + limit = quota.get("limit", 0) + used = quota.get("used", 0) + + # Переводим байты в ГБ + limit_gb = limit / (1024**3) if limit > 0 else 0 + used_gb = used / (1024**3) + + # Рассчитываем процент использования + if limit_gb > 0: + usage_percent = (used_gb / limit_gb) * 100 + reply_text += f"• Том {volume}: {used_gb:.2f} ГБ из {limit_gb:.2f} ГБ ({usage_percent:.1f}%)\n" + else: + reply_text += f"• Том {volume}: {used_gb:.2f} ГБ (без ограничений)\n" + + reply_text += "\n" + + await message.edit_text(reply_text, parse_mode="HTML") + +async def schedule_callback(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик callback-запросов для управления расписанием питания""" + query = update.callback_query + await query.answer() + + user_id = update.effective_user.id + if user_id not in ADMIN_USER_IDS: + await query.edit_message_text("У вас нет доступа к этому боту.") + return + + action = query.data + + if action.startswith("schedule_"): + action_type = action.split("_")[1] + + if action_type == "add_boot": + # Логика добавления расписания включения + # В реальном боте здесь будет диалог для настройки расписания + await query.edit_message_text("⚙️ Добавление расписания включения\n\nЭта функция находится в разработке.", parse_mode="HTML") + + elif action_type == "add_shutdown": + # Логика добавления расписания выключения + await query.edit_message_text("⚙️ Добавление расписания выключения\n\nЭта функция находится в разработке.", parse_mode="HTML") + + elif action_type == "delete": + # Логика удаления расписания + await query.edit_message_text("⚙️ Удаление расписания\n\nЭта функция находится в разработке.", parse_mode="HTML") + +async def browse_callback(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик callback-запросов для навигации по файловой системе""" + query = update.callback_query + await query.answer() + + user_id = update.effective_user.id + if user_id not in ADMIN_USER_IDS: + await query.edit_message_text("У вас нет доступа к этому боту.") + return + + action = query.data + + if action.startswith("browse_"): + path = action[7:] # Убираем префикс "browse_" + + # Используем команду browse с указанным путем + message = await query.edit_message_text(f"⏳ Получение содержимого {'папки ' + path if path else 'общих папок'}...") + + if not synology_api.is_online(): + await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить список файлов.", parse_mode="HTML") + return + + try: + browse_result = synology_api.browse_files(folder_path=path) + + if not browse_result.get("success", False): + error = browse_result.get("error", "unknown") + await message.edit_text(f"❌ Ошибка получения списка файлов\n\nПричина: {error}", parse_mode="HTML") + return + + items = browse_result.get("items", []) + current_path = browse_result.get("path", "") + is_root = browse_result.get("is_root", True) + + except Exception as e: + await message.edit_text(f"❌ Ошибка получения списка файлов\n\nПричина: {str(e)}", parse_mode="HTML") + return + + # Формируем сообщение о файлах и папках (аналогично функции browse_command) + if is_root: + reply_text = f"📁 Общие папки Synology NAS\n\n" + else: + reply_text = f"📁 Содержимое папки\n{current_path}\n\n" + + # Сортируем: сначала папки, потом файлы + folders = [] + files = [] + + for item in items: + if is_root: # Для корневого уровня все элементы - это общие папки + name = item.get("name", "unknown") + path = item.get("path", "") + folders.append((name, path, True)) + else: + name = item.get("name", "unknown") + path = item.get("path", "") + is_dir = item.get("isdir", False) + + if is_dir: + folders.append((name, path, False)) + else: + # Для файлов получаем размер + size = item.get("additional", {}).get("size", 0) + size_str = format_size(size) + files.append((name, path, size_str)) + + # Добавляем папки в сообщение + if folders: + for name, path, is_share in folders: + # Для общих папок добавляем иконку дома + icon = "🏠" if is_share else "📁" + reply_text += f"{icon} {name}\n" + + # Добавляем файлы в сообщение + if files: + for name, path, size in files: + # Выбираем иконку в зависимости от расширения + icon = get_file_icon(name) + reply_text += f"{icon} {name} ({size})\n" + + # Если нет элементов для отображения + if not folders and not files: + reply_text += "📭 Папка пуста\n" + + # Добавляем кнопку возврата наверх, если мы не в корне + if not is_root: + # Определяем родительскую директорию + parent_path = "/".join(current_path.split("/")[:-1]) if "/" in current_path else "" + + keyboard = [ + [InlineKeyboardButton("🔼 Вернуться на уровень выше", callback_data=f"browse_{parent_path}")] + ] + reply_markup = InlineKeyboardMarkup(keyboard) + + await message.edit_text(reply_text, parse_mode="HTML", reply_markup=reply_markup) + else: + await message.edit_text(reply_text, parse_mode="HTML") + +async def advanced_power_callback(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик callback-запросов для управления питанием""" + query = update.callback_query + await query.answer() + + user_id = update.effective_user.id + if user_id not in ADMIN_USER_IDS: + await query.edit_message_text("У вас нет доступа к этому боту.") + return + + action = query.data + + if action == "confirm_reboot": + # Выполняем перезагрузку + message = await query.edit_message_text("⏳ Выполняется перезагрузка Synology NAS...") + + if not synology_api.is_online(): + await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно выполнить перезагрузку.", parse_mode="HTML") + return + + try: + result = synology_api.reboot_system() + + if result: + reply_text = "🔄 Synology NAS перезагружается\n\n" + reply_text += "Перезагрузка обычно занимает 3-5 минут. После перезагрузки NAS будет снова доступен." + await message.edit_text(reply_text, parse_mode="HTML") + else: + await message.edit_text("❌ Ошибка при выполнении перезагрузки\n\nПроверьте журналы NAS для получения дополнительной информации.", parse_mode="HTML") + except Exception as e: + await message.edit_text(f"❌ Ошибка при выполнении перезагрузки\n\nПричина: {str(e)}", parse_mode="HTML") + + elif action == "cancel_reboot": + # Отменяем перезагрузку + await query.edit_message_text("✅ Перезагрузка отменена", parse_mode="HTML") + + elif action == "confirm_sleep": + # Выполняем переход в спящий режим (выключение) + message = await query.edit_message_text("⏳ Перевод Synology NAS в спящий режим...") + + if not synology_api.is_online(): + await message.edit_text("❌ Synology NAS уже оффлайн\n\nНевозможно выполнить переход в спящий режим.", parse_mode="HTML") + return + + try: + result = synology_api.power_off() + + if result: + reply_text = "💤 Synology NAS переведен в спящий режим\n\n" + reply_text += "Для пробуждения используйте команду /wakeup" + await message.edit_text(reply_text, parse_mode="HTML") + else: + await message.edit_text("❌ Ошибка при переходе в спящий режим\n\nПроверьте журналы NAS для получения дополнительной информации.", parse_mode="HTML") + except Exception as e: + await message.edit_text(f"❌ Ошибка при переходе в спящий режим\n\nПричина: {str(e)}", parse_mode="HTML") + + elif action == "cancel_sleep": + # Отменяем переход в спящий режим + await query.edit_message_text("✅ Переход в спящий режим отменен", parse_mode="HTML") + +# Вспомогательные функции + +def format_size(size_bytes: int) -> str: + """Преобразует размер в байтах в человекочитаемый формат""" + if size_bytes < 1024: + return f"{size_bytes} Б" + elif size_bytes < 1024**2: + return f"{size_bytes/1024:.1f} КБ" + elif size_bytes < 1024**3: + return f"{size_bytes/1024**2:.1f} МБ" + else: + return f"{size_bytes/1024**3:.1f} ГБ" + +def get_file_icon(filename: str) -> str: + """Возвращает эмодзи-иконку в зависимости от типа файла""" + extension = filename.lower().split('.')[-1] if '.' in filename else '' + + if extension in ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'tiff', 'webp']: + return "🖼️" + elif extension in ['mp4', 'avi', 'mov', 'mkv', 'wmv', 'flv']: + return "🎬" + elif extension in ['mp3', 'wav', 'ogg', 'flac', 'aac']: + return "🎵" + elif extension in ['doc', 'docx', 'txt', 'rtf', 'odt']: + return "📄" + elif extension in ['xls', 'xlsx', 'csv']: + return "📊" + elif extension in ['ppt', 'pptx']: + return "📑" + elif extension in ['pdf']: + return "📕" + elif extension in ['zip', 'rar', '7z', 'tar', 'gz']: + return "🗜️" + elif extension in ['exe', 'msi']: + return "⚙️" + else: + return "📄" diff --git a/.history/src/handlers/command_handlers_20250830063638.py b/.history/src/handlers/command_handlers_20250830063638.py new file mode 100644 index 0000000..05c2c7d --- /dev/null +++ b/.history/src/handlers/command_handlers_20250830063638.py @@ -0,0 +1,275 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Обработчики команд для телеграм-бота +""" + +import logging +from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup +from telegram.ext import ContextTypes + +from src.config.config import ADMIN_USER_IDS +from src.api.synology import SynologyAPI + +logger = logging.getLogger(__name__) + +# Инициализация API Synology +synology_api = SynologyAPI() + +async def start_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /start""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + logger.warning(f"Unauthorized access attempt from user ID: {user_id}") + return + + await update.message.reply_text( + f"Привет, {update.effective_user.first_name}! 👋\n\n" + "Я бот для управления вашим Synology NAS.\n" + "Используйте следующие команды:\n\n" + "/status - Проверка статуса NAS\n" + "/power - Управление питанием NAS\n" + "/help - Помощь по использованию бота" + ) + +async def help_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /help""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + await update.message.reply_text( + "📖 Помощь по использованию бота\n\n" + "Доступные команды:\n\n" + "/start - Начало работы с ботом\n" + "/status - Проверка текущего статуса NAS\n" + "/power - Управление питанием NAS\n" + "/help - Вывод этой справки\n\n" + "Управление питанием:\n" + "• Включение NAS: Wake-on-LAN\n" + "• Выключение NAS: Безопасное завершение работы\n\n" + "Примечание: Для работы функции включения необходимо, " + "чтобы на NAS была настроена функция Wake-on-LAN.", + parse_mode="HTML" + ) + +async def status_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /status""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + message = await update.message.reply_text("⏳ Проверяю статус Synology NAS...") + + is_online = synology_api.is_online() + + if is_online: + # Если NAS включен, попробуем получить дополнительную информацию + system_info = synology_api.get_system_status() + + if system_info: + model = system_info.get("model", "Неизвестная модель") + version = system_info.get("version", "Неизвестная версия") + uptime_seconds = system_info.get("uptime", 0) + + # Преобразование времени работы в удобочитаемый формат + days, remainder = divmod(uptime_seconds, 86400) + hours, remainder = divmod(remainder, 3600) + minutes, seconds = divmod(remainder, 60) + uptime_str = f"{days}д {hours}ч {minutes}м {seconds}с" + + await message.edit_text( + f"✅ Synology NAS онлайн\n\n" + f"Модель: {model}\n" + f"Версия DSM: {version}\n" + f"Время работы: {uptime_str}", + parse_mode="HTML" + ) + else: + await message.edit_text( + "✅ Synology NAS онлайн\n\n" + "Детальная информация недоступна. Возможно, необходимо авторизоваться.", + parse_mode="HTML" + ) + else: + await message.edit_text("❌ Synology NAS оффлайн", parse_mode="HTML") + +async def power_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /power""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + is_online = synology_api.is_online() + + keyboard = [ + [ + InlineKeyboardButton("🟢 Включить", callback_data="power_on") if not is_online else + InlineKeyboardButton("🟢 Включить", callback_data="power_on", disabled=True) + ], + [ + InlineKeyboardButton("🔴 Выключить", callback_data="power_off") if is_online else + InlineKeyboardButton("🔴 Выключить", callback_data="power_off", disabled=True) + ], + [ + InlineKeyboardButton("🔄 Перезагрузить", callback_data="reboot") if is_online else + InlineKeyboardButton("🔄 Перезагрузить", callback_data="reboot", disabled=True) + ], + [InlineKeyboardButton("❌ Отмена", callback_data="cancel")] + ] + + reply_markup = InlineKeyboardMarkup(keyboard) + + status_text = "✅ Онлайн" if is_online else "❌ Оффлайн" + + await update.message.reply_text( + f"Управление питанием Synology NAS\n\n" + f"Текущий статус: {status_text}\n\n" + f"Выберите действие:", + reply_markup=reply_markup, + parse_mode="HTML" + ) + +async def power_callback(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик callback-запросов для кнопок управления питанием""" + query = update.callback_query + await query.answer() + + user_id = query.from_user.id + if user_id not in ADMIN_USER_IDS: + return + + action = query.data + + if action == "cancel": + await query.edit_message_text("❌ Действие отменено") + return + + if action == "power_on": + await query.edit_message_text("⏳ Отправка сигнала включения Synology NAS...") + + if await context.application.create_task( + handle_power_on(query.message.chat_id, context) + ): + # Функция вернула True, успешное включение + pass + else: + # Функция вернула False, ошибка включения + await context.bot.send_message( + chat_id=query.message.chat_id, + text="❌ Ошибка при включении Synology NAS. Проверьте настройки Wake-on-LAN." + ) + + elif action == "power_off": + await query.edit_message_text("⏳ Отправка команды выключения Synology NAS...") + + if await context.application.create_task( + handle_power_off(query.message.chat_id, context) + ): + # Функция вернула True, успешное выключение + pass + else: + # Функция вернула False, ошибка выключения + await context.bot.send_message( + chat_id=query.message.chat_id, + text="❌ Ошибка при выключении Synology NAS. Проверьте соединение и учетные данные." + ) + + elif action == "reboot": + await query.edit_message_text("⏳ Отправка команды перезагрузки Synology NAS...") + + if await context.application.create_task( + handle_reboot(query.message.chat_id, context) + ): + # Функция вернула True, успешная перезагрузка + pass + else: + # Функция вернула False, ошибка перезагрузки + await context.bot.send_message( + chat_id=query.message.chat_id, + text="❌ Ошибка при перезагрузке Synology NAS. Проверьте соединение и учетные данные." + ) + +async def handle_power_on(chat_id: int, context: ContextTypes.DEFAULT_TYPE) -> bool: + """Асинхронная функция для включения NAS""" + try: + # Отправка запроса на включение + success = synology_api.power_on() + + if success: + await context.bot.send_message( + chat_id=chat_id, + text="✅ Synology NAS успешно включен и доступен" + ) + return True + else: + return False + except Exception as e: + logger.error(f"Error during power on: {str(e)}") + return False + +async def handle_power_off(chat_id: int, context: ContextTypes.DEFAULT_TYPE) -> bool: + """Асинхронная функция для выключения NAS""" + try: + # Отправка запроса на выключение + success = synology_api.power_off() + + if success: + await context.bot.send_message( + chat_id=chat_id, + text="✅ Команда выключения успешно отправлена. Synology NAS выключается..." + ) + return True + else: + return False + except Exception as e: + logger.error(f"Error during power off: {str(e)}") + return False + +async def handle_reboot(chat_id: int, context: ContextTypes.DEFAULT_TYPE) -> bool: + """Асинхронная функция для перезагрузки NAS""" + try: + # Отправка запроса на перезагрузку + success = synology_api.reboot_system() + + if success: + await context.bot.send_message( + chat_id=chat_id, + text="✅ Команда перезагрузки успешно отправлена. Synology NAS перезагружается..." + ) + + # Ждем некоторое время перед проверкой статуса + await context.bot.send_message( + chat_id=chat_id, + text="⏳ Ожидание перезагрузки системы..." + ) + + # Создаем задачу для ожидания загрузки + wait_successful = synology_api.wait_for_boot() + + if wait_successful: + await context.bot.send_message( + chat_id=chat_id, + text="✅ Synology NAS успешно перезагружен и снова онлайн" + ) + else: + await context.bot.send_message( + chat_id=chat_id, + text="⚠️ Превышено время ожидания перезагрузки. Проверьте статус вручную." + ) + + return True + else: + return False + except Exception as e: + logger.error(f"Error during reboot: {str(e)}") + return False diff --git a/.history/src/handlers/command_handlers_20250830063839.py b/.history/src/handlers/command_handlers_20250830063839.py new file mode 100644 index 0000000..05c2c7d --- /dev/null +++ b/.history/src/handlers/command_handlers_20250830063839.py @@ -0,0 +1,275 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Обработчики команд для телеграм-бота +""" + +import logging +from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup +from telegram.ext import ContextTypes + +from src.config.config import ADMIN_USER_IDS +from src.api.synology import SynologyAPI + +logger = logging.getLogger(__name__) + +# Инициализация API Synology +synology_api = SynologyAPI() + +async def start_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /start""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + logger.warning(f"Unauthorized access attempt from user ID: {user_id}") + return + + await update.message.reply_text( + f"Привет, {update.effective_user.first_name}! 👋\n\n" + "Я бот для управления вашим Synology NAS.\n" + "Используйте следующие команды:\n\n" + "/status - Проверка статуса NAS\n" + "/power - Управление питанием NAS\n" + "/help - Помощь по использованию бота" + ) + +async def help_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /help""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + await update.message.reply_text( + "📖 Помощь по использованию бота\n\n" + "Доступные команды:\n\n" + "/start - Начало работы с ботом\n" + "/status - Проверка текущего статуса NAS\n" + "/power - Управление питанием NAS\n" + "/help - Вывод этой справки\n\n" + "Управление питанием:\n" + "• Включение NAS: Wake-on-LAN\n" + "• Выключение NAS: Безопасное завершение работы\n\n" + "Примечание: Для работы функции включения необходимо, " + "чтобы на NAS была настроена функция Wake-on-LAN.", + parse_mode="HTML" + ) + +async def status_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /status""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + message = await update.message.reply_text("⏳ Проверяю статус Synology NAS...") + + is_online = synology_api.is_online() + + if is_online: + # Если NAS включен, попробуем получить дополнительную информацию + system_info = synology_api.get_system_status() + + if system_info: + model = system_info.get("model", "Неизвестная модель") + version = system_info.get("version", "Неизвестная версия") + uptime_seconds = system_info.get("uptime", 0) + + # Преобразование времени работы в удобочитаемый формат + days, remainder = divmod(uptime_seconds, 86400) + hours, remainder = divmod(remainder, 3600) + minutes, seconds = divmod(remainder, 60) + uptime_str = f"{days}д {hours}ч {minutes}м {seconds}с" + + await message.edit_text( + f"✅ Synology NAS онлайн\n\n" + f"Модель: {model}\n" + f"Версия DSM: {version}\n" + f"Время работы: {uptime_str}", + parse_mode="HTML" + ) + else: + await message.edit_text( + "✅ Synology NAS онлайн\n\n" + "Детальная информация недоступна. Возможно, необходимо авторизоваться.", + parse_mode="HTML" + ) + else: + await message.edit_text("❌ Synology NAS оффлайн", parse_mode="HTML") + +async def power_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /power""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + is_online = synology_api.is_online() + + keyboard = [ + [ + InlineKeyboardButton("🟢 Включить", callback_data="power_on") if not is_online else + InlineKeyboardButton("🟢 Включить", callback_data="power_on", disabled=True) + ], + [ + InlineKeyboardButton("🔴 Выключить", callback_data="power_off") if is_online else + InlineKeyboardButton("🔴 Выключить", callback_data="power_off", disabled=True) + ], + [ + InlineKeyboardButton("🔄 Перезагрузить", callback_data="reboot") if is_online else + InlineKeyboardButton("🔄 Перезагрузить", callback_data="reboot", disabled=True) + ], + [InlineKeyboardButton("❌ Отмена", callback_data="cancel")] + ] + + reply_markup = InlineKeyboardMarkup(keyboard) + + status_text = "✅ Онлайн" if is_online else "❌ Оффлайн" + + await update.message.reply_text( + f"Управление питанием Synology NAS\n\n" + f"Текущий статус: {status_text}\n\n" + f"Выберите действие:", + reply_markup=reply_markup, + parse_mode="HTML" + ) + +async def power_callback(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик callback-запросов для кнопок управления питанием""" + query = update.callback_query + await query.answer() + + user_id = query.from_user.id + if user_id not in ADMIN_USER_IDS: + return + + action = query.data + + if action == "cancel": + await query.edit_message_text("❌ Действие отменено") + return + + if action == "power_on": + await query.edit_message_text("⏳ Отправка сигнала включения Synology NAS...") + + if await context.application.create_task( + handle_power_on(query.message.chat_id, context) + ): + # Функция вернула True, успешное включение + pass + else: + # Функция вернула False, ошибка включения + await context.bot.send_message( + chat_id=query.message.chat_id, + text="❌ Ошибка при включении Synology NAS. Проверьте настройки Wake-on-LAN." + ) + + elif action == "power_off": + await query.edit_message_text("⏳ Отправка команды выключения Synology NAS...") + + if await context.application.create_task( + handle_power_off(query.message.chat_id, context) + ): + # Функция вернула True, успешное выключение + pass + else: + # Функция вернула False, ошибка выключения + await context.bot.send_message( + chat_id=query.message.chat_id, + text="❌ Ошибка при выключении Synology NAS. Проверьте соединение и учетные данные." + ) + + elif action == "reboot": + await query.edit_message_text("⏳ Отправка команды перезагрузки Synology NAS...") + + if await context.application.create_task( + handle_reboot(query.message.chat_id, context) + ): + # Функция вернула True, успешная перезагрузка + pass + else: + # Функция вернула False, ошибка перезагрузки + await context.bot.send_message( + chat_id=query.message.chat_id, + text="❌ Ошибка при перезагрузке Synology NAS. Проверьте соединение и учетные данные." + ) + +async def handle_power_on(chat_id: int, context: ContextTypes.DEFAULT_TYPE) -> bool: + """Асинхронная функция для включения NAS""" + try: + # Отправка запроса на включение + success = synology_api.power_on() + + if success: + await context.bot.send_message( + chat_id=chat_id, + text="✅ Synology NAS успешно включен и доступен" + ) + return True + else: + return False + except Exception as e: + logger.error(f"Error during power on: {str(e)}") + return False + +async def handle_power_off(chat_id: int, context: ContextTypes.DEFAULT_TYPE) -> bool: + """Асинхронная функция для выключения NAS""" + try: + # Отправка запроса на выключение + success = synology_api.power_off() + + if success: + await context.bot.send_message( + chat_id=chat_id, + text="✅ Команда выключения успешно отправлена. Synology NAS выключается..." + ) + return True + else: + return False + except Exception as e: + logger.error(f"Error during power off: {str(e)}") + return False + +async def handle_reboot(chat_id: int, context: ContextTypes.DEFAULT_TYPE) -> bool: + """Асинхронная функция для перезагрузки NAS""" + try: + # Отправка запроса на перезагрузку + success = synology_api.reboot_system() + + if success: + await context.bot.send_message( + chat_id=chat_id, + text="✅ Команда перезагрузки успешно отправлена. Synology NAS перезагружается..." + ) + + # Ждем некоторое время перед проверкой статуса + await context.bot.send_message( + chat_id=chat_id, + text="⏳ Ожидание перезагрузки системы..." + ) + + # Создаем задачу для ожидания загрузки + wait_successful = synology_api.wait_for_boot() + + if wait_successful: + await context.bot.send_message( + chat_id=chat_id, + text="✅ Synology NAS успешно перезагружен и снова онлайн" + ) + else: + await context.bot.send_message( + chat_id=chat_id, + text="⚠️ Превышено время ожидания перезагрузки. Проверьте статус вручную." + ) + + return True + else: + return False + except Exception as e: + logger.error(f"Error during reboot: {str(e)}") + return False diff --git a/.history/src/handlers/command_handlers_20250830065335.py b/.history/src/handlers/command_handlers_20250830065335.py new file mode 100644 index 0000000..d3bedbe --- /dev/null +++ b/.history/src/handlers/command_handlers_20250830065335.py @@ -0,0 +1,282 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Обработчики команд для телеграм-бота +""" + +import logging +from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup +from telegram.ext import ContextTypes + +from src.config.config import ADMIN_USER_IDS +from src.api.synology import SynologyAPI + +logger = logging.getLogger(__name__) + +# Инициализация API Synology +synology_api = SynologyAPI() + +async def start_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /start""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + logger.warning(f"Unauthorized access attempt from user ID: {user_id}") + return + + await update.message.reply_text( + f"Привет, {update.effective_user.first_name}! 👋\n\n" + "Я бот для управления вашим Synology NAS.\n" + "Используйте следующие команды:\n\n" + "/status - Проверка статуса NAS\n" + "/power - Управление питанием NAS\n" + "/help - Помощь по использованию бота" + ) + +async def help_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /help""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + await update.message.reply_text( + "📖 Помощь по использованию бота\n\n" + "Основные команды:\n" + "/start - Начало работы с ботом\n" + "/status - Проверка текущего статуса NAS\n" + "/power - Управление питанием NAS\n" + "/help - Вывод этой справки\n\n" + "Расширенные команды:\n" + "/system - Подробная информация о системе\n" + "/storage - Информация о хранилище и дисках\n" + "/shares - Список общих папок\n" + "/load - Текущая нагрузка на систему\n" + "/security - Статус безопасности системы\n\n" + "Управление питанием:\n" + "• Включение NAS: Wake-on-LAN\n" + "• Выключение NAS: Безопасное завершение работы\n" + "• Перезагрузка: Безопасная перезагрузка\n\n" + "Примечание: Для работы функции включения необходимо, " + "чтобы на NAS была настроена функция Wake-on-LAN.", + parse_mode="HTML" + ) + +async def status_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /status""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + message = await update.message.reply_text("⏳ Проверяю статус Synology NAS...") + + is_online = synology_api.is_online() + + if is_online: + # Если NAS включен, попробуем получить дополнительную информацию + system_info = synology_api.get_system_status() + + if system_info: + model = system_info.get("model", "Неизвестная модель") + version = system_info.get("version", "Неизвестная версия") + uptime_seconds = system_info.get("uptime", 0) + + # Преобразование времени работы в удобочитаемый формат + days, remainder = divmod(uptime_seconds, 86400) + hours, remainder = divmod(remainder, 3600) + minutes, seconds = divmod(remainder, 60) + uptime_str = f"{days}д {hours}ч {minutes}м {seconds}с" + + await message.edit_text( + f"✅ Synology NAS онлайн\n\n" + f"Модель: {model}\n" + f"Версия DSM: {version}\n" + f"Время работы: {uptime_str}", + parse_mode="HTML" + ) + else: + await message.edit_text( + "✅ Synology NAS онлайн\n\n" + "Детальная информация недоступна. Возможно, необходимо авторизоваться.", + parse_mode="HTML" + ) + else: + await message.edit_text("❌ Synology NAS оффлайн", parse_mode="HTML") + +async def power_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /power""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + is_online = synology_api.is_online() + + keyboard = [ + [ + InlineKeyboardButton("🟢 Включить", callback_data="power_on") if not is_online else + InlineKeyboardButton("🟢 Включить", callback_data="power_on", disabled=True) + ], + [ + InlineKeyboardButton("🔴 Выключить", callback_data="power_off") if is_online else + InlineKeyboardButton("🔴 Выключить", callback_data="power_off", disabled=True) + ], + [ + InlineKeyboardButton("🔄 Перезагрузить", callback_data="reboot") if is_online else + InlineKeyboardButton("🔄 Перезагрузить", callback_data="reboot", disabled=True) + ], + [InlineKeyboardButton("❌ Отмена", callback_data="cancel")] + ] + + reply_markup = InlineKeyboardMarkup(keyboard) + + status_text = "✅ Онлайн" if is_online else "❌ Оффлайн" + + await update.message.reply_text( + f"Управление питанием Synology NAS\n\n" + f"Текущий статус: {status_text}\n\n" + f"Выберите действие:", + reply_markup=reply_markup, + parse_mode="HTML" + ) + +async def power_callback(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик callback-запросов для кнопок управления питанием""" + query = update.callback_query + await query.answer() + + user_id = query.from_user.id + if user_id not in ADMIN_USER_IDS: + return + + action = query.data + + if action == "cancel": + await query.edit_message_text("❌ Действие отменено") + return + + if action == "power_on": + await query.edit_message_text("⏳ Отправка сигнала включения Synology NAS...") + + if await context.application.create_task( + handle_power_on(query.message.chat_id, context) + ): + # Функция вернула True, успешное включение + pass + else: + # Функция вернула False, ошибка включения + await context.bot.send_message( + chat_id=query.message.chat_id, + text="❌ Ошибка при включении Synology NAS. Проверьте настройки Wake-on-LAN." + ) + + elif action == "power_off": + await query.edit_message_text("⏳ Отправка команды выключения Synology NAS...") + + if await context.application.create_task( + handle_power_off(query.message.chat_id, context) + ): + # Функция вернула True, успешное выключение + pass + else: + # Функция вернула False, ошибка выключения + await context.bot.send_message( + chat_id=query.message.chat_id, + text="❌ Ошибка при выключении Synology NAS. Проверьте соединение и учетные данные." + ) + + elif action == "reboot": + await query.edit_message_text("⏳ Отправка команды перезагрузки Synology NAS...") + + if await context.application.create_task( + handle_reboot(query.message.chat_id, context) + ): + # Функция вернула True, успешная перезагрузка + pass + else: + # Функция вернула False, ошибка перезагрузки + await context.bot.send_message( + chat_id=query.message.chat_id, + text="❌ Ошибка при перезагрузке Synology NAS. Проверьте соединение и учетные данные." + ) + +async def handle_power_on(chat_id: int, context: ContextTypes.DEFAULT_TYPE) -> bool: + """Асинхронная функция для включения NAS""" + try: + # Отправка запроса на включение + success = synology_api.power_on() + + if success: + await context.bot.send_message( + chat_id=chat_id, + text="✅ Synology NAS успешно включен и доступен" + ) + return True + else: + return False + except Exception as e: + logger.error(f"Error during power on: {str(e)}") + return False + +async def handle_power_off(chat_id: int, context: ContextTypes.DEFAULT_TYPE) -> bool: + """Асинхронная функция для выключения NAS""" + try: + # Отправка запроса на выключение + success = synology_api.power_off() + + if success: + await context.bot.send_message( + chat_id=chat_id, + text="✅ Команда выключения успешно отправлена. Synology NAS выключается..." + ) + return True + else: + return False + except Exception as e: + logger.error(f"Error during power off: {str(e)}") + return False + +async def handle_reboot(chat_id: int, context: ContextTypes.DEFAULT_TYPE) -> bool: + """Асинхронная функция для перезагрузки NAS""" + try: + # Отправка запроса на перезагрузку + success = synology_api.reboot_system() + + if success: + await context.bot.send_message( + chat_id=chat_id, + text="✅ Команда перезагрузки успешно отправлена. Synology NAS перезагружается..." + ) + + # Ждем некоторое время перед проверкой статуса + await context.bot.send_message( + chat_id=chat_id, + text="⏳ Ожидание перезагрузки системы..." + ) + + # Создаем задачу для ожидания загрузки + wait_successful = synology_api.wait_for_boot() + + if wait_successful: + await context.bot.send_message( + chat_id=chat_id, + text="✅ Synology NAS успешно перезагружен и снова онлайн" + ) + else: + await context.bot.send_message( + chat_id=chat_id, + text="⚠️ Превышено время ожидания перезагрузки. Проверьте статус вручную." + ) + + return True + else: + return False + except Exception as e: + logger.error(f"Error during reboot: {str(e)}") + return False diff --git a/.history/src/handlers/command_handlers_20250830065348.py b/.history/src/handlers/command_handlers_20250830065348.py new file mode 100644 index 0000000..35c3a90 --- /dev/null +++ b/.history/src/handlers/command_handlers_20250830065348.py @@ -0,0 +1,286 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Обработчики команд для телеграм-бота +""" + +import logging +from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup +from telegram.ext import ContextTypes + +from src.config.config import ADMIN_USER_IDS +from src.api.synology import SynologyAPI + +logger = logging.getLogger(__name__) + +# Инициализация API Synology +synology_api = SynologyAPI() + +async def start_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /start""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + logger.warning(f"Unauthorized access attempt from user ID: {user_id}") + return + + await update.message.reply_text( + f"Привет, {update.effective_user.first_name}! 👋\n\n" + "Я бот для управления вашим Synology NAS.\n" + "Используйте следующие команды:\n\n" + "Основные команды:\n" + "/status - Проверка статуса NAS\n" + "/power - Управление питанием NAS\n" + "/system - Информация о системе\n" + "/storage - Информация о хранилище\n\n" + "Используйте /help для получения полного списка команд", + parse_mode="HTML" + ) + +async def help_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /help""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + await update.message.reply_text( + "📖 Помощь по использованию бота\n\n" + "Основные команды:\n" + "/start - Начало работы с ботом\n" + "/status - Проверка текущего статуса NAS\n" + "/power - Управление питанием NAS\n" + "/help - Вывод этой справки\n\n" + "Расширенные команды:\n" + "/system - Подробная информация о системе\n" + "/storage - Информация о хранилище и дисках\n" + "/shares - Список общих папок\n" + "/load - Текущая нагрузка на систему\n" + "/security - Статус безопасности системы\n\n" + "Управление питанием:\n" + "• Включение NAS: Wake-on-LAN\n" + "• Выключение NAS: Безопасное завершение работы\n" + "• Перезагрузка: Безопасная перезагрузка\n\n" + "Примечание: Для работы функции включения необходимо, " + "чтобы на NAS была настроена функция Wake-on-LAN.", + parse_mode="HTML" + ) + +async def status_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /status""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + message = await update.message.reply_text("⏳ Проверяю статус Synology NAS...") + + is_online = synology_api.is_online() + + if is_online: + # Если NAS включен, попробуем получить дополнительную информацию + system_info = synology_api.get_system_status() + + if system_info: + model = system_info.get("model", "Неизвестная модель") + version = system_info.get("version", "Неизвестная версия") + uptime_seconds = system_info.get("uptime", 0) + + # Преобразование времени работы в удобочитаемый формат + days, remainder = divmod(uptime_seconds, 86400) + hours, remainder = divmod(remainder, 3600) + minutes, seconds = divmod(remainder, 60) + uptime_str = f"{days}д {hours}ч {minutes}м {seconds}с" + + await message.edit_text( + f"✅ Synology NAS онлайн\n\n" + f"Модель: {model}\n" + f"Версия DSM: {version}\n" + f"Время работы: {uptime_str}", + parse_mode="HTML" + ) + else: + await message.edit_text( + "✅ Synology NAS онлайн\n\n" + "Детальная информация недоступна. Возможно, необходимо авторизоваться.", + parse_mode="HTML" + ) + else: + await message.edit_text("❌ Synology NAS оффлайн", parse_mode="HTML") + +async def power_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /power""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + is_online = synology_api.is_online() + + keyboard = [ + [ + InlineKeyboardButton("🟢 Включить", callback_data="power_on") if not is_online else + InlineKeyboardButton("🟢 Включить", callback_data="power_on", disabled=True) + ], + [ + InlineKeyboardButton("🔴 Выключить", callback_data="power_off") if is_online else + InlineKeyboardButton("🔴 Выключить", callback_data="power_off", disabled=True) + ], + [ + InlineKeyboardButton("🔄 Перезагрузить", callback_data="reboot") if is_online else + InlineKeyboardButton("🔄 Перезагрузить", callback_data="reboot", disabled=True) + ], + [InlineKeyboardButton("❌ Отмена", callback_data="cancel")] + ] + + reply_markup = InlineKeyboardMarkup(keyboard) + + status_text = "✅ Онлайн" if is_online else "❌ Оффлайн" + + await update.message.reply_text( + f"Управление питанием Synology NAS\n\n" + f"Текущий статус: {status_text}\n\n" + f"Выберите действие:", + reply_markup=reply_markup, + parse_mode="HTML" + ) + +async def power_callback(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик callback-запросов для кнопок управления питанием""" + query = update.callback_query + await query.answer() + + user_id = query.from_user.id + if user_id not in ADMIN_USER_IDS: + return + + action = query.data + + if action == "cancel": + await query.edit_message_text("❌ Действие отменено") + return + + if action == "power_on": + await query.edit_message_text("⏳ Отправка сигнала включения Synology NAS...") + + if await context.application.create_task( + handle_power_on(query.message.chat_id, context) + ): + # Функция вернула True, успешное включение + pass + else: + # Функция вернула False, ошибка включения + await context.bot.send_message( + chat_id=query.message.chat_id, + text="❌ Ошибка при включении Synology NAS. Проверьте настройки Wake-on-LAN." + ) + + elif action == "power_off": + await query.edit_message_text("⏳ Отправка команды выключения Synology NAS...") + + if await context.application.create_task( + handle_power_off(query.message.chat_id, context) + ): + # Функция вернула True, успешное выключение + pass + else: + # Функция вернула False, ошибка выключения + await context.bot.send_message( + chat_id=query.message.chat_id, + text="❌ Ошибка при выключении Synology NAS. Проверьте соединение и учетные данные." + ) + + elif action == "reboot": + await query.edit_message_text("⏳ Отправка команды перезагрузки Synology NAS...") + + if await context.application.create_task( + handle_reboot(query.message.chat_id, context) + ): + # Функция вернула True, успешная перезагрузка + pass + else: + # Функция вернула False, ошибка перезагрузки + await context.bot.send_message( + chat_id=query.message.chat_id, + text="❌ Ошибка при перезагрузке Synology NAS. Проверьте соединение и учетные данные." + ) + +async def handle_power_on(chat_id: int, context: ContextTypes.DEFAULT_TYPE) -> bool: + """Асинхронная функция для включения NAS""" + try: + # Отправка запроса на включение + success = synology_api.power_on() + + if success: + await context.bot.send_message( + chat_id=chat_id, + text="✅ Synology NAS успешно включен и доступен" + ) + return True + else: + return False + except Exception as e: + logger.error(f"Error during power on: {str(e)}") + return False + +async def handle_power_off(chat_id: int, context: ContextTypes.DEFAULT_TYPE) -> bool: + """Асинхронная функция для выключения NAS""" + try: + # Отправка запроса на выключение + success = synology_api.power_off() + + if success: + await context.bot.send_message( + chat_id=chat_id, + text="✅ Команда выключения успешно отправлена. Synology NAS выключается..." + ) + return True + else: + return False + except Exception as e: + logger.error(f"Error during power off: {str(e)}") + return False + +async def handle_reboot(chat_id: int, context: ContextTypes.DEFAULT_TYPE) -> bool: + """Асинхронная функция для перезагрузки NAS""" + try: + # Отправка запроса на перезагрузку + success = synology_api.reboot_system() + + if success: + await context.bot.send_message( + chat_id=chat_id, + text="✅ Команда перезагрузки успешно отправлена. Synology NAS перезагружается..." + ) + + # Ждем некоторое время перед проверкой статуса + await context.bot.send_message( + chat_id=chat_id, + text="⏳ Ожидание перезагрузки системы..." + ) + + # Создаем задачу для ожидания загрузки + wait_successful = synology_api.wait_for_boot() + + if wait_successful: + await context.bot.send_message( + chat_id=chat_id, + text="✅ Synology NAS успешно перезагружен и снова онлайн" + ) + else: + await context.bot.send_message( + chat_id=chat_id, + text="⚠️ Превышено время ожидания перезагрузки. Проверьте статус вручную." + ) + + return True + else: + return False + except Exception as e: + logger.error(f"Error during reboot: {str(e)}") + return False diff --git a/.history/src/handlers/command_handlers_20250830065454.py b/.history/src/handlers/command_handlers_20250830065454.py new file mode 100644 index 0000000..35c3a90 --- /dev/null +++ b/.history/src/handlers/command_handlers_20250830065454.py @@ -0,0 +1,286 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Обработчики команд для телеграм-бота +""" + +import logging +from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup +from telegram.ext import ContextTypes + +from src.config.config import ADMIN_USER_IDS +from src.api.synology import SynologyAPI + +logger = logging.getLogger(__name__) + +# Инициализация API Synology +synology_api = SynologyAPI() + +async def start_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /start""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + logger.warning(f"Unauthorized access attempt from user ID: {user_id}") + return + + await update.message.reply_text( + f"Привет, {update.effective_user.first_name}! 👋\n\n" + "Я бот для управления вашим Synology NAS.\n" + "Используйте следующие команды:\n\n" + "Основные команды:\n" + "/status - Проверка статуса NAS\n" + "/power - Управление питанием NAS\n" + "/system - Информация о системе\n" + "/storage - Информация о хранилище\n\n" + "Используйте /help для получения полного списка команд", + parse_mode="HTML" + ) + +async def help_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /help""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + await update.message.reply_text( + "📖 Помощь по использованию бота\n\n" + "Основные команды:\n" + "/start - Начало работы с ботом\n" + "/status - Проверка текущего статуса NAS\n" + "/power - Управление питанием NAS\n" + "/help - Вывод этой справки\n\n" + "Расширенные команды:\n" + "/system - Подробная информация о системе\n" + "/storage - Информация о хранилище и дисках\n" + "/shares - Список общих папок\n" + "/load - Текущая нагрузка на систему\n" + "/security - Статус безопасности системы\n\n" + "Управление питанием:\n" + "• Включение NAS: Wake-on-LAN\n" + "• Выключение NAS: Безопасное завершение работы\n" + "• Перезагрузка: Безопасная перезагрузка\n\n" + "Примечание: Для работы функции включения необходимо, " + "чтобы на NAS была настроена функция Wake-on-LAN.", + parse_mode="HTML" + ) + +async def status_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /status""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + message = await update.message.reply_text("⏳ Проверяю статус Synology NAS...") + + is_online = synology_api.is_online() + + if is_online: + # Если NAS включен, попробуем получить дополнительную информацию + system_info = synology_api.get_system_status() + + if system_info: + model = system_info.get("model", "Неизвестная модель") + version = system_info.get("version", "Неизвестная версия") + uptime_seconds = system_info.get("uptime", 0) + + # Преобразование времени работы в удобочитаемый формат + days, remainder = divmod(uptime_seconds, 86400) + hours, remainder = divmod(remainder, 3600) + minutes, seconds = divmod(remainder, 60) + uptime_str = f"{days}д {hours}ч {minutes}м {seconds}с" + + await message.edit_text( + f"✅ Synology NAS онлайн\n\n" + f"Модель: {model}\n" + f"Версия DSM: {version}\n" + f"Время работы: {uptime_str}", + parse_mode="HTML" + ) + else: + await message.edit_text( + "✅ Synology NAS онлайн\n\n" + "Детальная информация недоступна. Возможно, необходимо авторизоваться.", + parse_mode="HTML" + ) + else: + await message.edit_text("❌ Synology NAS оффлайн", parse_mode="HTML") + +async def power_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /power""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + is_online = synology_api.is_online() + + keyboard = [ + [ + InlineKeyboardButton("🟢 Включить", callback_data="power_on") if not is_online else + InlineKeyboardButton("🟢 Включить", callback_data="power_on", disabled=True) + ], + [ + InlineKeyboardButton("🔴 Выключить", callback_data="power_off") if is_online else + InlineKeyboardButton("🔴 Выключить", callback_data="power_off", disabled=True) + ], + [ + InlineKeyboardButton("🔄 Перезагрузить", callback_data="reboot") if is_online else + InlineKeyboardButton("🔄 Перезагрузить", callback_data="reboot", disabled=True) + ], + [InlineKeyboardButton("❌ Отмена", callback_data="cancel")] + ] + + reply_markup = InlineKeyboardMarkup(keyboard) + + status_text = "✅ Онлайн" if is_online else "❌ Оффлайн" + + await update.message.reply_text( + f"Управление питанием Synology NAS\n\n" + f"Текущий статус: {status_text}\n\n" + f"Выберите действие:", + reply_markup=reply_markup, + parse_mode="HTML" + ) + +async def power_callback(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик callback-запросов для кнопок управления питанием""" + query = update.callback_query + await query.answer() + + user_id = query.from_user.id + if user_id not in ADMIN_USER_IDS: + return + + action = query.data + + if action == "cancel": + await query.edit_message_text("❌ Действие отменено") + return + + if action == "power_on": + await query.edit_message_text("⏳ Отправка сигнала включения Synology NAS...") + + if await context.application.create_task( + handle_power_on(query.message.chat_id, context) + ): + # Функция вернула True, успешное включение + pass + else: + # Функция вернула False, ошибка включения + await context.bot.send_message( + chat_id=query.message.chat_id, + text="❌ Ошибка при включении Synology NAS. Проверьте настройки Wake-on-LAN." + ) + + elif action == "power_off": + await query.edit_message_text("⏳ Отправка команды выключения Synology NAS...") + + if await context.application.create_task( + handle_power_off(query.message.chat_id, context) + ): + # Функция вернула True, успешное выключение + pass + else: + # Функция вернула False, ошибка выключения + await context.bot.send_message( + chat_id=query.message.chat_id, + text="❌ Ошибка при выключении Synology NAS. Проверьте соединение и учетные данные." + ) + + elif action == "reboot": + await query.edit_message_text("⏳ Отправка команды перезагрузки Synology NAS...") + + if await context.application.create_task( + handle_reboot(query.message.chat_id, context) + ): + # Функция вернула True, успешная перезагрузка + pass + else: + # Функция вернула False, ошибка перезагрузки + await context.bot.send_message( + chat_id=query.message.chat_id, + text="❌ Ошибка при перезагрузке Synology NAS. Проверьте соединение и учетные данные." + ) + +async def handle_power_on(chat_id: int, context: ContextTypes.DEFAULT_TYPE) -> bool: + """Асинхронная функция для включения NAS""" + try: + # Отправка запроса на включение + success = synology_api.power_on() + + if success: + await context.bot.send_message( + chat_id=chat_id, + text="✅ Synology NAS успешно включен и доступен" + ) + return True + else: + return False + except Exception as e: + logger.error(f"Error during power on: {str(e)}") + return False + +async def handle_power_off(chat_id: int, context: ContextTypes.DEFAULT_TYPE) -> bool: + """Асинхронная функция для выключения NAS""" + try: + # Отправка запроса на выключение + success = synology_api.power_off() + + if success: + await context.bot.send_message( + chat_id=chat_id, + text="✅ Команда выключения успешно отправлена. Synology NAS выключается..." + ) + return True + else: + return False + except Exception as e: + logger.error(f"Error during power off: {str(e)}") + return False + +async def handle_reboot(chat_id: int, context: ContextTypes.DEFAULT_TYPE) -> bool: + """Асинхронная функция для перезагрузки NAS""" + try: + # Отправка запроса на перезагрузку + success = synology_api.reboot_system() + + if success: + await context.bot.send_message( + chat_id=chat_id, + text="✅ Команда перезагрузки успешно отправлена. Synology NAS перезагружается..." + ) + + # Ждем некоторое время перед проверкой статуса + await context.bot.send_message( + chat_id=chat_id, + text="⏳ Ожидание перезагрузки системы..." + ) + + # Создаем задачу для ожидания загрузки + wait_successful = synology_api.wait_for_boot() + + if wait_successful: + await context.bot.send_message( + chat_id=chat_id, + text="✅ Synology NAS успешно перезагружен и снова онлайн" + ) + else: + await context.bot.send_message( + chat_id=chat_id, + text="⚠️ Превышено время ожидания перезагрузки. Проверьте статус вручную." + ) + + return True + else: + return False + except Exception as e: + logger.error(f"Error during reboot: {str(e)}") + return False diff --git a/.history/src/handlers/command_handlers_20250830073032.py b/.history/src/handlers/command_handlers_20250830073032.py new file mode 100644 index 0000000..82e77fe --- /dev/null +++ b/.history/src/handlers/command_handlers_20250830073032.py @@ -0,0 +1,293 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Обработчики команд для телеграм-бота +""" + +import logging +from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup +from telegram.ext import ContextTypes + +from src.config.config import ADMIN_USER_IDS +from src.api.synology import SynologyAPI + +logger = logging.getLogger(__name__) + +# Инициализация API Synology +synology_api = SynologyAPI() + +async def start_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /start""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + logger.warning(f"Unauthorized access attempt from user ID: {user_id}") + return + + await update.message.reply_text( + f"Привет, {update.effective_user.first_name}! 👋\n\n" + "Я бот для управления вашим Synology NAS.\n" + "Используйте следующие команды:\n\n" + "Основные команды:\n" + "/status - Проверка статуса NAS\n" + "/power - Управление питанием NAS\n" + "/system - Информация о системе\n" + "/storage - Информация о хранилище\n\n" + "Используйте /help для получения полного списка команд", + parse_mode="HTML" + ) + +async def help_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /help""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + await update.message.reply_text( + "📖 Помощь по использованию бота\n\n" + "Основные команды:\n" + "/start - Начало работы с ботом\n" + "/status - Проверка текущего статуса NAS\n" + "/power - Управление питанием NAS\n" + "/help - Вывод этой справки\n\n" + "Расширенные команды:\n" + "/system - Подробная информация о системе\n" + "/storage - Информация о хранилище и дисках\n" + "/shares - Список общих папок\n" + "/load - Текущая нагрузка на систему\n" + "/security - Статус безопасности системы\n\n" + "Управление питанием:\n" + "• Включение NAS: Wake-on-LAN\n" + "• Выключение NAS: Безопасное завершение работы\n" + "• Перезагрузка: Безопасная перезагрузка\n\n" + "Примечание: Для работы функции включения необходимо, " + "чтобы на NAS была настроена функция Wake-on-LAN.", + parse_mode="HTML" + ) + +async def status_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /status""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + message = await update.message.reply_text("⏳ Проверяю статус Synology NAS...") + + is_online = synology_api.is_online() + + if is_online: + # Если NAS включен, попробуем получить дополнительную информацию + system_info = synology_api.get_system_status() + + if system_info and system_info.get("status") != "error": + model = system_info.get("model", "Неизвестная модель") + version = system_info.get("version_string", system_info.get("version", "Неизвестная версия")) + uptime_seconds = system_info.get("up_time", system_info.get("uptime", 0)) + + # Преобразование времени работы в удобочитаемый формат + days, remainder = divmod(int(uptime_seconds), 86400) + hours, remainder = divmod(remainder, 3600) + minutes, seconds = divmod(remainder, 60) + uptime_str = f"{days}д {hours}ч {minutes}м {seconds}с" + + await message.edit_text( + f"✅ Synology NAS онлайн\n\n" + f"Модель: {model}\n" + f"Версия DSM: {version}\n" + f"Время работы: {uptime_str}", + parse_mode="HTML" + ) + else: + # Обработка возможной ошибки API + error_info = "" + if system_info and system_info.get("status") == "error": + error_code = system_info.get("error_code", "неизвестно") + error_info = f"\nКод ошибки API: {error_code}" + + await message.edit_text( + f"✅ Synology NAS онлайн\n\n" + f"Детальная информация недоступна. Возможно, необходимо авторизоваться." + f"{error_info}", + parse_mode="HTML" + ) + else: + await message.edit_text("❌ Synology NAS оффлайн", parse_mode="HTML") + +async def power_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /power""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + is_online = synology_api.is_online() + + keyboard = [ + [ + InlineKeyboardButton("🟢 Включить", callback_data="power_on") if not is_online else + InlineKeyboardButton("🟢 Включить", callback_data="power_on", disabled=True) + ], + [ + InlineKeyboardButton("🔴 Выключить", callback_data="power_off") if is_online else + InlineKeyboardButton("🔴 Выключить", callback_data="power_off", disabled=True) + ], + [ + InlineKeyboardButton("🔄 Перезагрузить", callback_data="reboot") if is_online else + InlineKeyboardButton("🔄 Перезагрузить", callback_data="reboot", disabled=True) + ], + [InlineKeyboardButton("❌ Отмена", callback_data="cancel")] + ] + + reply_markup = InlineKeyboardMarkup(keyboard) + + status_text = "✅ Онлайн" if is_online else "❌ Оффлайн" + + await update.message.reply_text( + f"Управление питанием Synology NAS\n\n" + f"Текущий статус: {status_text}\n\n" + f"Выберите действие:", + reply_markup=reply_markup, + parse_mode="HTML" + ) + +async def power_callback(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик callback-запросов для кнопок управления питанием""" + query = update.callback_query + await query.answer() + + user_id = query.from_user.id + if user_id not in ADMIN_USER_IDS: + return + + action = query.data + + if action == "cancel": + await query.edit_message_text("❌ Действие отменено") + return + + if action == "power_on": + await query.edit_message_text("⏳ Отправка сигнала включения Synology NAS...") + + if await context.application.create_task( + handle_power_on(query.message.chat_id, context) + ): + # Функция вернула True, успешное включение + pass + else: + # Функция вернула False, ошибка включения + await context.bot.send_message( + chat_id=query.message.chat_id, + text="❌ Ошибка при включении Synology NAS. Проверьте настройки Wake-on-LAN." + ) + + elif action == "power_off": + await query.edit_message_text("⏳ Отправка команды выключения Synology NAS...") + + if await context.application.create_task( + handle_power_off(query.message.chat_id, context) + ): + # Функция вернула True, успешное выключение + pass + else: + # Функция вернула False, ошибка выключения + await context.bot.send_message( + chat_id=query.message.chat_id, + text="❌ Ошибка при выключении Synology NAS. Проверьте соединение и учетные данные." + ) + + elif action == "reboot": + await query.edit_message_text("⏳ Отправка команды перезагрузки Synology NAS...") + + if await context.application.create_task( + handle_reboot(query.message.chat_id, context) + ): + # Функция вернула True, успешная перезагрузка + pass + else: + # Функция вернула False, ошибка перезагрузки + await context.bot.send_message( + chat_id=query.message.chat_id, + text="❌ Ошибка при перезагрузке Synology NAS. Проверьте соединение и учетные данные." + ) + +async def handle_power_on(chat_id: int, context: ContextTypes.DEFAULT_TYPE) -> bool: + """Асинхронная функция для включения NAS""" + try: + # Отправка запроса на включение + success = synology_api.power_on() + + if success: + await context.bot.send_message( + chat_id=chat_id, + text="✅ Synology NAS успешно включен и доступен" + ) + return True + else: + return False + except Exception as e: + logger.error(f"Error during power on: {str(e)}") + return False + +async def handle_power_off(chat_id: int, context: ContextTypes.DEFAULT_TYPE) -> bool: + """Асинхронная функция для выключения NAS""" + try: + # Отправка запроса на выключение + success = synology_api.power_off() + + if success: + await context.bot.send_message( + chat_id=chat_id, + text="✅ Команда выключения успешно отправлена. Synology NAS выключается..." + ) + return True + else: + return False + except Exception as e: + logger.error(f"Error during power off: {str(e)}") + return False + +async def handle_reboot(chat_id: int, context: ContextTypes.DEFAULT_TYPE) -> bool: + """Асинхронная функция для перезагрузки NAS""" + try: + # Отправка запроса на перезагрузку + success = synology_api.reboot_system() + + if success: + await context.bot.send_message( + chat_id=chat_id, + text="✅ Команда перезагрузки успешно отправлена. Synology NAS перезагружается..." + ) + + # Ждем некоторое время перед проверкой статуса + await context.bot.send_message( + chat_id=chat_id, + text="⏳ Ожидание перезагрузки системы..." + ) + + # Создаем задачу для ожидания загрузки + wait_successful = synology_api.wait_for_boot() + + if wait_successful: + await context.bot.send_message( + chat_id=chat_id, + text="✅ Synology NAS успешно перезагружен и снова онлайн" + ) + else: + await context.bot.send_message( + chat_id=chat_id, + text="⚠️ Превышено время ожидания перезагрузки. Проверьте статус вручную." + ) + + return True + else: + return False + except Exception as e: + logger.error(f"Error during reboot: {str(e)}") + return False diff --git a/.history/src/handlers/command_handlers_20250830073043.py b/.history/src/handlers/command_handlers_20250830073043.py new file mode 100644 index 0000000..82e77fe --- /dev/null +++ b/.history/src/handlers/command_handlers_20250830073043.py @@ -0,0 +1,293 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Обработчики команд для телеграм-бота +""" + +import logging +from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup +from telegram.ext import ContextTypes + +from src.config.config import ADMIN_USER_IDS +from src.api.synology import SynologyAPI + +logger = logging.getLogger(__name__) + +# Инициализация API Synology +synology_api = SynologyAPI() + +async def start_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /start""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + logger.warning(f"Unauthorized access attempt from user ID: {user_id}") + return + + await update.message.reply_text( + f"Привет, {update.effective_user.first_name}! 👋\n\n" + "Я бот для управления вашим Synology NAS.\n" + "Используйте следующие команды:\n\n" + "Основные команды:\n" + "/status - Проверка статуса NAS\n" + "/power - Управление питанием NAS\n" + "/system - Информация о системе\n" + "/storage - Информация о хранилище\n\n" + "Используйте /help для получения полного списка команд", + parse_mode="HTML" + ) + +async def help_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /help""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + await update.message.reply_text( + "📖 Помощь по использованию бота\n\n" + "Основные команды:\n" + "/start - Начало работы с ботом\n" + "/status - Проверка текущего статуса NAS\n" + "/power - Управление питанием NAS\n" + "/help - Вывод этой справки\n\n" + "Расширенные команды:\n" + "/system - Подробная информация о системе\n" + "/storage - Информация о хранилище и дисках\n" + "/shares - Список общих папок\n" + "/load - Текущая нагрузка на систему\n" + "/security - Статус безопасности системы\n\n" + "Управление питанием:\n" + "• Включение NAS: Wake-on-LAN\n" + "• Выключение NAS: Безопасное завершение работы\n" + "• Перезагрузка: Безопасная перезагрузка\n\n" + "Примечание: Для работы функции включения необходимо, " + "чтобы на NAS была настроена функция Wake-on-LAN.", + parse_mode="HTML" + ) + +async def status_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /status""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + message = await update.message.reply_text("⏳ Проверяю статус Synology NAS...") + + is_online = synology_api.is_online() + + if is_online: + # Если NAS включен, попробуем получить дополнительную информацию + system_info = synology_api.get_system_status() + + if system_info and system_info.get("status") != "error": + model = system_info.get("model", "Неизвестная модель") + version = system_info.get("version_string", system_info.get("version", "Неизвестная версия")) + uptime_seconds = system_info.get("up_time", system_info.get("uptime", 0)) + + # Преобразование времени работы в удобочитаемый формат + days, remainder = divmod(int(uptime_seconds), 86400) + hours, remainder = divmod(remainder, 3600) + minutes, seconds = divmod(remainder, 60) + uptime_str = f"{days}д {hours}ч {minutes}м {seconds}с" + + await message.edit_text( + f"✅ Synology NAS онлайн\n\n" + f"Модель: {model}\n" + f"Версия DSM: {version}\n" + f"Время работы: {uptime_str}", + parse_mode="HTML" + ) + else: + # Обработка возможной ошибки API + error_info = "" + if system_info and system_info.get("status") == "error": + error_code = system_info.get("error_code", "неизвестно") + error_info = f"\nКод ошибки API: {error_code}" + + await message.edit_text( + f"✅ Synology NAS онлайн\n\n" + f"Детальная информация недоступна. Возможно, необходимо авторизоваться." + f"{error_info}", + parse_mode="HTML" + ) + else: + await message.edit_text("❌ Synology NAS оффлайн", parse_mode="HTML") + +async def power_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /power""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + is_online = synology_api.is_online() + + keyboard = [ + [ + InlineKeyboardButton("🟢 Включить", callback_data="power_on") if not is_online else + InlineKeyboardButton("🟢 Включить", callback_data="power_on", disabled=True) + ], + [ + InlineKeyboardButton("🔴 Выключить", callback_data="power_off") if is_online else + InlineKeyboardButton("🔴 Выключить", callback_data="power_off", disabled=True) + ], + [ + InlineKeyboardButton("🔄 Перезагрузить", callback_data="reboot") if is_online else + InlineKeyboardButton("🔄 Перезагрузить", callback_data="reboot", disabled=True) + ], + [InlineKeyboardButton("❌ Отмена", callback_data="cancel")] + ] + + reply_markup = InlineKeyboardMarkup(keyboard) + + status_text = "✅ Онлайн" if is_online else "❌ Оффлайн" + + await update.message.reply_text( + f"Управление питанием Synology NAS\n\n" + f"Текущий статус: {status_text}\n\n" + f"Выберите действие:", + reply_markup=reply_markup, + parse_mode="HTML" + ) + +async def power_callback(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик callback-запросов для кнопок управления питанием""" + query = update.callback_query + await query.answer() + + user_id = query.from_user.id + if user_id not in ADMIN_USER_IDS: + return + + action = query.data + + if action == "cancel": + await query.edit_message_text("❌ Действие отменено") + return + + if action == "power_on": + await query.edit_message_text("⏳ Отправка сигнала включения Synology NAS...") + + if await context.application.create_task( + handle_power_on(query.message.chat_id, context) + ): + # Функция вернула True, успешное включение + pass + else: + # Функция вернула False, ошибка включения + await context.bot.send_message( + chat_id=query.message.chat_id, + text="❌ Ошибка при включении Synology NAS. Проверьте настройки Wake-on-LAN." + ) + + elif action == "power_off": + await query.edit_message_text("⏳ Отправка команды выключения Synology NAS...") + + if await context.application.create_task( + handle_power_off(query.message.chat_id, context) + ): + # Функция вернула True, успешное выключение + pass + else: + # Функция вернула False, ошибка выключения + await context.bot.send_message( + chat_id=query.message.chat_id, + text="❌ Ошибка при выключении Synology NAS. Проверьте соединение и учетные данные." + ) + + elif action == "reboot": + await query.edit_message_text("⏳ Отправка команды перезагрузки Synology NAS...") + + if await context.application.create_task( + handle_reboot(query.message.chat_id, context) + ): + # Функция вернула True, успешная перезагрузка + pass + else: + # Функция вернула False, ошибка перезагрузки + await context.bot.send_message( + chat_id=query.message.chat_id, + text="❌ Ошибка при перезагрузке Synology NAS. Проверьте соединение и учетные данные." + ) + +async def handle_power_on(chat_id: int, context: ContextTypes.DEFAULT_TYPE) -> bool: + """Асинхронная функция для включения NAS""" + try: + # Отправка запроса на включение + success = synology_api.power_on() + + if success: + await context.bot.send_message( + chat_id=chat_id, + text="✅ Synology NAS успешно включен и доступен" + ) + return True + else: + return False + except Exception as e: + logger.error(f"Error during power on: {str(e)}") + return False + +async def handle_power_off(chat_id: int, context: ContextTypes.DEFAULT_TYPE) -> bool: + """Асинхронная функция для выключения NAS""" + try: + # Отправка запроса на выключение + success = synology_api.power_off() + + if success: + await context.bot.send_message( + chat_id=chat_id, + text="✅ Команда выключения успешно отправлена. Synology NAS выключается..." + ) + return True + else: + return False + except Exception as e: + logger.error(f"Error during power off: {str(e)}") + return False + +async def handle_reboot(chat_id: int, context: ContextTypes.DEFAULT_TYPE) -> bool: + """Асинхронная функция для перезагрузки NAS""" + try: + # Отправка запроса на перезагрузку + success = synology_api.reboot_system() + + if success: + await context.bot.send_message( + chat_id=chat_id, + text="✅ Команда перезагрузки успешно отправлена. Synology NAS перезагружается..." + ) + + # Ждем некоторое время перед проверкой статуса + await context.bot.send_message( + chat_id=chat_id, + text="⏳ Ожидание перезагрузки системы..." + ) + + # Создаем задачу для ожидания загрузки + wait_successful = synology_api.wait_for_boot() + + if wait_successful: + await context.bot.send_message( + chat_id=chat_id, + text="✅ Synology NAS успешно перезагружен и снова онлайн" + ) + else: + await context.bot.send_message( + chat_id=chat_id, + text="⚠️ Превышено время ожидания перезагрузки. Проверьте статус вручную." + ) + + return True + else: + return False + except Exception as e: + logger.error(f"Error during reboot: {str(e)}") + return False diff --git a/.history/src/handlers/command_handlers_20250830073339.py b/.history/src/handlers/command_handlers_20250830073339.py new file mode 100644 index 0000000..4cf628a --- /dev/null +++ b/.history/src/handlers/command_handlers_20250830073339.py @@ -0,0 +1,300 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Обработчики команд для телеграм-бота +""" + +import logging +from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup +from telegram.ext import ContextTypes + +from src.config.config import ADMIN_USER_IDS +from src.api.synology import SynologyAPI + +logger = logging.getLogger(__name__) + +# Инициализация API Synology +synology_api = SynologyAPI() + +async def start_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /start""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + logger.warning(f"Unauthorized access attempt from user ID: {user_id}") + return + + await update.message.reply_text( + f"Привет, {update.effective_user.first_name}! 👋\n\n" + "Я бот для управления вашим Synology NAS.\n" + "Используйте следующие команды:\n\n" + "Основные команды:\n" + "/status - Проверка статуса NAS\n" + "/power - Управление питанием NAS\n" + "/system - Информация о системе\n" + "/storage - Информация о хранилище\n\n" + "Используйте /help для получения полного списка команд", + parse_mode="HTML" + ) + +async def help_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /help""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + await update.message.reply_text( + "📖 Помощь по использованию бота\n\n" + "Основные команды:\n" + "/start - Начало работы с ботом\n" + "/status - Проверка текущего статуса NAS\n" + "/power - Управление питанием NAS\n" + "/help - Вывод этой справки\n\n" + "Расширенные команды:\n" + "/system - Подробная информация о системе\n" + "/storage - Информация о хранилище и дисках\n" + "/shares - Список общих папок\n" + "/load - Текущая нагрузка на систему\n" + "/security - Статус безопасности системы\n\n" + "Управление питанием:\n" + "• Включение NAS: Wake-on-LAN\n" + "• Выключение NAS: Безопасное завершение работы\n" + "• Перезагрузка: Безопасная перезагрузка\n\n" + "Примечание: Для работы функции включения необходимо, " + "чтобы на NAS была настроена функция Wake-on-LAN.", + parse_mode="HTML" + ) + +async def status_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /status""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + message = await update.message.reply_text("⏳ Проверяю статус Synology NAS...") + + is_online = synology_api.is_online() + + if is_online: + # Если NAS включен, попробуем получить дополнительную информацию + system_info = synology_api.get_system_status() + + if system_info and system_info.get("status") != "error": + model = system_info.get("model", "Неизвестная модель") + version = system_info.get("version_string", system_info.get("version", "Неизвестная версия")) + uptime_seconds = system_info.get("up_time", system_info.get("uptime", 0)) + + # Преобразование времени работы в удобочитаемый формат + days, remainder = divmod(int(uptime_seconds), 86400) + hours, remainder = divmod(remainder, 3600) + minutes, seconds = divmod(remainder, 60) + uptime_str = f"{days}д {hours}ч {minutes}м {seconds}с" + + await message.edit_text( + f"✅ Synology NAS онлайн\n\n" + f"Модель: {model}\n" + f"Версия DSM: {version}\n" + f"Время работы: {uptime_str}", + parse_mode="HTML" + ) + else: + # Обработка возможной ошибки API + error_info = "" + if system_info and system_info.get("status") == "error": + error_code = system_info.get("error_code", "неизвестно") + error_info = f"\nКод ошибки API: {error_code}" + + await message.edit_text( + f"✅ Synology NAS онлайн\n\n" + f"Детальная информация недоступна. Возможно, необходимо авторизоваться." + f"{error_info}", + parse_mode="HTML" + ) + else: + await message.edit_text("❌ Synology NAS оффлайн", parse_mode="HTML") + +async def power_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /power""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + is_online = synology_api.is_online() + + keyboard = [] + + # Кнопка включения + if not is_online: + keyboard.append([InlineKeyboardButton("🟢 Включить", callback_data="power_on")]) + else: + keyboard.append([InlineKeyboardButton("🟢 Включить (уже включено)", callback_data="power_on_no_op")]) + + # Кнопка выключения + if is_online: + keyboard.append([InlineKeyboardButton("🔴 Выключить", callback_data="power_off")]) + else: + keyboard.append([InlineKeyboardButton("🔴 Выключить (уже выключено)", callback_data="power_off_no_op")]) + + # Кнопка перезагрузки + if is_online: + keyboard.append([InlineKeyboardButton("🔄 Перезагрузить", callback_data="reboot")]) + else: + keyboard.append([InlineKeyboardButton("🔄 Перезагрузить (недоступно)", callback_data="reboot_no_op")]) + + # Кнопка отмены + keyboard.append([InlineKeyboardButton("❌ Отмена", callback_data="cancel")]) + + reply_markup = InlineKeyboardMarkup(keyboard) + + status_text = "✅ Онлайн" if is_online else "❌ Оффлайн" + + await update.message.reply_text( + f"Управление питанием Synology NAS\n\n" + f"Текущий статус: {status_text}\n\n" + f"Выберите действие:", + reply_markup=reply_markup, + parse_mode="HTML" + ) + +async def power_callback(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик callback-запросов для кнопок управления питанием""" + query = update.callback_query + await query.answer() + + user_id = query.from_user.id + if user_id not in ADMIN_USER_IDS: + return + + action = query.data + + if action == "cancel": + await query.edit_message_text("❌ Действие отменено") + return + + if action == "power_on": + await query.edit_message_text("⏳ Отправка сигнала включения Synology NAS...") + + if await context.application.create_task( + handle_power_on(query.message.chat_id, context) + ): + # Функция вернула True, успешное включение + pass + else: + # Функция вернула False, ошибка включения + await context.bot.send_message( + chat_id=query.message.chat_id, + text="❌ Ошибка при включении Synology NAS. Проверьте настройки Wake-on-LAN." + ) + + elif action == "power_off": + await query.edit_message_text("⏳ Отправка команды выключения Synology NAS...") + + if await context.application.create_task( + handle_power_off(query.message.chat_id, context) + ): + # Функция вернула True, успешное выключение + pass + else: + # Функция вернула False, ошибка выключения + await context.bot.send_message( + chat_id=query.message.chat_id, + text="❌ Ошибка при выключении Synology NAS. Проверьте соединение и учетные данные." + ) + + elif action == "reboot": + await query.edit_message_text("⏳ Отправка команды перезагрузки Synology NAS...") + + if await context.application.create_task( + handle_reboot(query.message.chat_id, context) + ): + # Функция вернула True, успешная перезагрузка + pass + else: + # Функция вернула False, ошибка перезагрузки + await context.bot.send_message( + chat_id=query.message.chat_id, + text="❌ Ошибка при перезагрузке Synology NAS. Проверьте соединение и учетные данные." + ) + +async def handle_power_on(chat_id: int, context: ContextTypes.DEFAULT_TYPE) -> bool: + """Асинхронная функция для включения NAS""" + try: + # Отправка запроса на включение + success = synology_api.power_on() + + if success: + await context.bot.send_message( + chat_id=chat_id, + text="✅ Synology NAS успешно включен и доступен" + ) + return True + else: + return False + except Exception as e: + logger.error(f"Error during power on: {str(e)}") + return False + +async def handle_power_off(chat_id: int, context: ContextTypes.DEFAULT_TYPE) -> bool: + """Асинхронная функция для выключения NAS""" + try: + # Отправка запроса на выключение + success = synology_api.power_off() + + if success: + await context.bot.send_message( + chat_id=chat_id, + text="✅ Команда выключения успешно отправлена. Synology NAS выключается..." + ) + return True + else: + return False + except Exception as e: + logger.error(f"Error during power off: {str(e)}") + return False + +async def handle_reboot(chat_id: int, context: ContextTypes.DEFAULT_TYPE) -> bool: + """Асинхронная функция для перезагрузки NAS""" + try: + # Отправка запроса на перезагрузку + success = synology_api.reboot_system() + + if success: + await context.bot.send_message( + chat_id=chat_id, + text="✅ Команда перезагрузки успешно отправлена. Synology NAS перезагружается..." + ) + + # Ждем некоторое время перед проверкой статуса + await context.bot.send_message( + chat_id=chat_id, + text="⏳ Ожидание перезагрузки системы..." + ) + + # Создаем задачу для ожидания загрузки + wait_successful = synology_api.wait_for_boot() + + if wait_successful: + await context.bot.send_message( + chat_id=chat_id, + text="✅ Synology NAS успешно перезагружен и снова онлайн" + ) + else: + await context.bot.send_message( + chat_id=chat_id, + text="⚠️ Превышено время ожидания перезагрузки. Проверьте статус вручную." + ) + + return True + else: + return False + except Exception as e: + logger.error(f"Error during reboot: {str(e)}") + return False diff --git a/.history/src/handlers/command_handlers_20250830073407.py b/.history/src/handlers/command_handlers_20250830073407.py new file mode 100644 index 0000000..ecb77fb --- /dev/null +++ b/.history/src/handlers/command_handlers_20250830073407.py @@ -0,0 +1,311 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Обработчики команд для телеграм-бота +""" + +import logging +from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup +from telegram.ext import ContextTypes + +from src.config.config import ADMIN_USER_IDS +from src.api.synology import SynologyAPI + +logger = logging.getLogger(__name__) + +# Инициализация API Synology +synology_api = SynologyAPI() + +async def start_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /start""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + logger.warning(f"Unauthorized access attempt from user ID: {user_id}") + return + + await update.message.reply_text( + f"Привет, {update.effective_user.first_name}! 👋\n\n" + "Я бот для управления вашим Synology NAS.\n" + "Используйте следующие команды:\n\n" + "Основные команды:\n" + "/status - Проверка статуса NAS\n" + "/power - Управление питанием NAS\n" + "/system - Информация о системе\n" + "/storage - Информация о хранилище\n\n" + "Используйте /help для получения полного списка команд", + parse_mode="HTML" + ) + +async def help_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /help""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + await update.message.reply_text( + "📖 Помощь по использованию бота\n\n" + "Основные команды:\n" + "/start - Начало работы с ботом\n" + "/status - Проверка текущего статуса NAS\n" + "/power - Управление питанием NAS\n" + "/help - Вывод этой справки\n\n" + "Расширенные команды:\n" + "/system - Подробная информация о системе\n" + "/storage - Информация о хранилище и дисках\n" + "/shares - Список общих папок\n" + "/load - Текущая нагрузка на систему\n" + "/security - Статус безопасности системы\n\n" + "Управление питанием:\n" + "• Включение NAS: Wake-on-LAN\n" + "• Выключение NAS: Безопасное завершение работы\n" + "• Перезагрузка: Безопасная перезагрузка\n\n" + "Примечание: Для работы функции включения необходимо, " + "чтобы на NAS была настроена функция Wake-on-LAN.", + parse_mode="HTML" + ) + +async def status_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /status""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + message = await update.message.reply_text("⏳ Проверяю статус Synology NAS...") + + is_online = synology_api.is_online() + + if is_online: + # Если NAS включен, попробуем получить дополнительную информацию + system_info = synology_api.get_system_status() + + if system_info and system_info.get("status") != "error": + model = system_info.get("model", "Неизвестная модель") + version = system_info.get("version_string", system_info.get("version", "Неизвестная версия")) + uptime_seconds = system_info.get("up_time", system_info.get("uptime", 0)) + + # Преобразование времени работы в удобочитаемый формат + days, remainder = divmod(int(uptime_seconds), 86400) + hours, remainder = divmod(remainder, 3600) + minutes, seconds = divmod(remainder, 60) + uptime_str = f"{days}д {hours}ч {minutes}м {seconds}с" + + await message.edit_text( + f"✅ Synology NAS онлайн\n\n" + f"Модель: {model}\n" + f"Версия DSM: {version}\n" + f"Время работы: {uptime_str}", + parse_mode="HTML" + ) + else: + # Обработка возможной ошибки API + error_info = "" + if system_info and system_info.get("status") == "error": + error_code = system_info.get("error_code", "неизвестно") + error_info = f"\nКод ошибки API: {error_code}" + + await message.edit_text( + f"✅ Synology NAS онлайн\n\n" + f"Детальная информация недоступна. Возможно, необходимо авторизоваться." + f"{error_info}", + parse_mode="HTML" + ) + else: + await message.edit_text("❌ Synology NAS оффлайн", parse_mode="HTML") + +async def power_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /power""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + is_online = synology_api.is_online() + + keyboard = [] + + # Кнопка включения + if not is_online: + keyboard.append([InlineKeyboardButton("🟢 Включить", callback_data="power_on")]) + else: + keyboard.append([InlineKeyboardButton("🟢 Включить (уже включено)", callback_data="power_on_no_op")]) + + # Кнопка выключения + if is_online: + keyboard.append([InlineKeyboardButton("🔴 Выключить", callback_data="power_off")]) + else: + keyboard.append([InlineKeyboardButton("🔴 Выключить (уже выключено)", callback_data="power_off_no_op")]) + + # Кнопка перезагрузки + if is_online: + keyboard.append([InlineKeyboardButton("🔄 Перезагрузить", callback_data="reboot")]) + else: + keyboard.append([InlineKeyboardButton("🔄 Перезагрузить (недоступно)", callback_data="reboot_no_op")]) + + # Кнопка отмены + keyboard.append([InlineKeyboardButton("❌ Отмена", callback_data="cancel")]) + + reply_markup = InlineKeyboardMarkup(keyboard) + + status_text = "✅ Онлайн" if is_online else "❌ Оффлайн" + + await update.message.reply_text( + f"Управление питанием Synology NAS\n\n" + f"Текущий статус: {status_text}\n\n" + f"Выберите действие:", + reply_markup=reply_markup, + parse_mode="HTML" + ) + +async def power_callback(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик callback-запросов для кнопок управления питанием""" + query = update.callback_query + await query.answer() + + user_id = query.from_user.id + if user_id not in ADMIN_USER_IDS: + return + + action = query.data + + if action == "cancel": + await query.edit_message_text("❌ Действие отменено") + return + + # Обработка неактивных кнопок + if action in ["power_on_no_op", "power_off_no_op", "reboot_no_op"]: + if action == "power_on_no_op": + await query.edit_message_text("ℹ️ Synology NAS уже включен") + elif action == "power_off_no_op": + await query.edit_message_text("ℹ️ Synology NAS уже выключен") + else: + await query.edit_message_text("ℹ️ Невозможно перезагрузить выключенное устройство") + return + + # Обработка основных действий + if action == "power_on": + await query.edit_message_text("⏳ Отправка сигнала включения Synology NAS...") + + if await context.application.create_task( + handle_power_on(query.message.chat_id, context) + ): + # Функция вернула True, успешное включение + pass + else: + # Функция вернула False, ошибка включения + await context.bot.send_message( + chat_id=query.message.chat_id, + text="❌ Ошибка при включении Synology NAS. Проверьте настройки Wake-on-LAN." + ) + + elif action == "power_off": + await query.edit_message_text("⏳ Отправка команды выключения Synology NAS...") + + if await context.application.create_task( + handle_power_off(query.message.chat_id, context) + ): + # Функция вернула True, успешное выключение + pass + else: + # Функция вернула False, ошибка выключения + await context.bot.send_message( + chat_id=query.message.chat_id, + text="❌ Ошибка при выключении Synology NAS. Проверьте соединение и учетные данные." + ) + + elif action == "reboot": + await query.edit_message_text("⏳ Отправка команды перезагрузки Synology NAS...") + + if await context.application.create_task( + handle_reboot(query.message.chat_id, context) + ): + # Функция вернула True, успешная перезагрузка + pass + else: + # Функция вернула False, ошибка перезагрузки + await context.bot.send_message( + chat_id=query.message.chat_id, + text="❌ Ошибка при перезагрузке Synology NAS. Проверьте соединение и учетные данные." + ) + +async def handle_power_on(chat_id: int, context: ContextTypes.DEFAULT_TYPE) -> bool: + """Асинхронная функция для включения NAS""" + try: + # Отправка запроса на включение + success = synology_api.power_on() + + if success: + await context.bot.send_message( + chat_id=chat_id, + text="✅ Synology NAS успешно включен и доступен" + ) + return True + else: + return False + except Exception as e: + logger.error(f"Error during power on: {str(e)}") + return False + +async def handle_power_off(chat_id: int, context: ContextTypes.DEFAULT_TYPE) -> bool: + """Асинхронная функция для выключения NAS""" + try: + # Отправка запроса на выключение + success = synology_api.power_off() + + if success: + await context.bot.send_message( + chat_id=chat_id, + text="✅ Команда выключения успешно отправлена. Synology NAS выключается..." + ) + return True + else: + return False + except Exception as e: + logger.error(f"Error during power off: {str(e)}") + return False + +async def handle_reboot(chat_id: int, context: ContextTypes.DEFAULT_TYPE) -> bool: + """Асинхронная функция для перезагрузки NAS""" + try: + # Отправка запроса на перезагрузку + success = synology_api.reboot_system() + + if success: + await context.bot.send_message( + chat_id=chat_id, + text="✅ Команда перезагрузки успешно отправлена. Synology NAS перезагружается..." + ) + + # Ждем некоторое время перед проверкой статуса + await context.bot.send_message( + chat_id=chat_id, + text="⏳ Ожидание перезагрузки системы..." + ) + + # Создаем задачу для ожидания загрузки + wait_successful = synology_api.wait_for_boot() + + if wait_successful: + await context.bot.send_message( + chat_id=chat_id, + text="✅ Synology NAS успешно перезагружен и снова онлайн" + ) + else: + await context.bot.send_message( + chat_id=chat_id, + text="⚠️ Превышено время ожидания перезагрузки. Проверьте статус вручную." + ) + + return True + else: + return False + except Exception as e: + logger.error(f"Error during reboot: {str(e)}") + return False diff --git a/.history/src/handlers/command_handlers_20250830073425.py b/.history/src/handlers/command_handlers_20250830073425.py new file mode 100644 index 0000000..ecb77fb --- /dev/null +++ b/.history/src/handlers/command_handlers_20250830073425.py @@ -0,0 +1,311 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Обработчики команд для телеграм-бота +""" + +import logging +from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup +from telegram.ext import ContextTypes + +from src.config.config import ADMIN_USER_IDS +from src.api.synology import SynologyAPI + +logger = logging.getLogger(__name__) + +# Инициализация API Synology +synology_api = SynologyAPI() + +async def start_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /start""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + logger.warning(f"Unauthorized access attempt from user ID: {user_id}") + return + + await update.message.reply_text( + f"Привет, {update.effective_user.first_name}! 👋\n\n" + "Я бот для управления вашим Synology NAS.\n" + "Используйте следующие команды:\n\n" + "Основные команды:\n" + "/status - Проверка статуса NAS\n" + "/power - Управление питанием NAS\n" + "/system - Информация о системе\n" + "/storage - Информация о хранилище\n\n" + "Используйте /help для получения полного списка команд", + parse_mode="HTML" + ) + +async def help_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /help""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + await update.message.reply_text( + "📖 Помощь по использованию бота\n\n" + "Основные команды:\n" + "/start - Начало работы с ботом\n" + "/status - Проверка текущего статуса NAS\n" + "/power - Управление питанием NAS\n" + "/help - Вывод этой справки\n\n" + "Расширенные команды:\n" + "/system - Подробная информация о системе\n" + "/storage - Информация о хранилище и дисках\n" + "/shares - Список общих папок\n" + "/load - Текущая нагрузка на систему\n" + "/security - Статус безопасности системы\n\n" + "Управление питанием:\n" + "• Включение NAS: Wake-on-LAN\n" + "• Выключение NAS: Безопасное завершение работы\n" + "• Перезагрузка: Безопасная перезагрузка\n\n" + "Примечание: Для работы функции включения необходимо, " + "чтобы на NAS была настроена функция Wake-on-LAN.", + parse_mode="HTML" + ) + +async def status_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /status""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + message = await update.message.reply_text("⏳ Проверяю статус Synology NAS...") + + is_online = synology_api.is_online() + + if is_online: + # Если NAS включен, попробуем получить дополнительную информацию + system_info = synology_api.get_system_status() + + if system_info and system_info.get("status") != "error": + model = system_info.get("model", "Неизвестная модель") + version = system_info.get("version_string", system_info.get("version", "Неизвестная версия")) + uptime_seconds = system_info.get("up_time", system_info.get("uptime", 0)) + + # Преобразование времени работы в удобочитаемый формат + days, remainder = divmod(int(uptime_seconds), 86400) + hours, remainder = divmod(remainder, 3600) + minutes, seconds = divmod(remainder, 60) + uptime_str = f"{days}д {hours}ч {minutes}м {seconds}с" + + await message.edit_text( + f"✅ Synology NAS онлайн\n\n" + f"Модель: {model}\n" + f"Версия DSM: {version}\n" + f"Время работы: {uptime_str}", + parse_mode="HTML" + ) + else: + # Обработка возможной ошибки API + error_info = "" + if system_info and system_info.get("status") == "error": + error_code = system_info.get("error_code", "неизвестно") + error_info = f"\nКод ошибки API: {error_code}" + + await message.edit_text( + f"✅ Synology NAS онлайн\n\n" + f"Детальная информация недоступна. Возможно, необходимо авторизоваться." + f"{error_info}", + parse_mode="HTML" + ) + else: + await message.edit_text("❌ Synology NAS оффлайн", parse_mode="HTML") + +async def power_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /power""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + is_online = synology_api.is_online() + + keyboard = [] + + # Кнопка включения + if not is_online: + keyboard.append([InlineKeyboardButton("🟢 Включить", callback_data="power_on")]) + else: + keyboard.append([InlineKeyboardButton("🟢 Включить (уже включено)", callback_data="power_on_no_op")]) + + # Кнопка выключения + if is_online: + keyboard.append([InlineKeyboardButton("🔴 Выключить", callback_data="power_off")]) + else: + keyboard.append([InlineKeyboardButton("🔴 Выключить (уже выключено)", callback_data="power_off_no_op")]) + + # Кнопка перезагрузки + if is_online: + keyboard.append([InlineKeyboardButton("🔄 Перезагрузить", callback_data="reboot")]) + else: + keyboard.append([InlineKeyboardButton("🔄 Перезагрузить (недоступно)", callback_data="reboot_no_op")]) + + # Кнопка отмены + keyboard.append([InlineKeyboardButton("❌ Отмена", callback_data="cancel")]) + + reply_markup = InlineKeyboardMarkup(keyboard) + + status_text = "✅ Онлайн" if is_online else "❌ Оффлайн" + + await update.message.reply_text( + f"Управление питанием Synology NAS\n\n" + f"Текущий статус: {status_text}\n\n" + f"Выберите действие:", + reply_markup=reply_markup, + parse_mode="HTML" + ) + +async def power_callback(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик callback-запросов для кнопок управления питанием""" + query = update.callback_query + await query.answer() + + user_id = query.from_user.id + if user_id not in ADMIN_USER_IDS: + return + + action = query.data + + if action == "cancel": + await query.edit_message_text("❌ Действие отменено") + return + + # Обработка неактивных кнопок + if action in ["power_on_no_op", "power_off_no_op", "reboot_no_op"]: + if action == "power_on_no_op": + await query.edit_message_text("ℹ️ Synology NAS уже включен") + elif action == "power_off_no_op": + await query.edit_message_text("ℹ️ Synology NAS уже выключен") + else: + await query.edit_message_text("ℹ️ Невозможно перезагрузить выключенное устройство") + return + + # Обработка основных действий + if action == "power_on": + await query.edit_message_text("⏳ Отправка сигнала включения Synology NAS...") + + if await context.application.create_task( + handle_power_on(query.message.chat_id, context) + ): + # Функция вернула True, успешное включение + pass + else: + # Функция вернула False, ошибка включения + await context.bot.send_message( + chat_id=query.message.chat_id, + text="❌ Ошибка при включении Synology NAS. Проверьте настройки Wake-on-LAN." + ) + + elif action == "power_off": + await query.edit_message_text("⏳ Отправка команды выключения Synology NAS...") + + if await context.application.create_task( + handle_power_off(query.message.chat_id, context) + ): + # Функция вернула True, успешное выключение + pass + else: + # Функция вернула False, ошибка выключения + await context.bot.send_message( + chat_id=query.message.chat_id, + text="❌ Ошибка при выключении Synology NAS. Проверьте соединение и учетные данные." + ) + + elif action == "reboot": + await query.edit_message_text("⏳ Отправка команды перезагрузки Synology NAS...") + + if await context.application.create_task( + handle_reboot(query.message.chat_id, context) + ): + # Функция вернула True, успешная перезагрузка + pass + else: + # Функция вернула False, ошибка перезагрузки + await context.bot.send_message( + chat_id=query.message.chat_id, + text="❌ Ошибка при перезагрузке Synology NAS. Проверьте соединение и учетные данные." + ) + +async def handle_power_on(chat_id: int, context: ContextTypes.DEFAULT_TYPE) -> bool: + """Асинхронная функция для включения NAS""" + try: + # Отправка запроса на включение + success = synology_api.power_on() + + if success: + await context.bot.send_message( + chat_id=chat_id, + text="✅ Synology NAS успешно включен и доступен" + ) + return True + else: + return False + except Exception as e: + logger.error(f"Error during power on: {str(e)}") + return False + +async def handle_power_off(chat_id: int, context: ContextTypes.DEFAULT_TYPE) -> bool: + """Асинхронная функция для выключения NAS""" + try: + # Отправка запроса на выключение + success = synology_api.power_off() + + if success: + await context.bot.send_message( + chat_id=chat_id, + text="✅ Команда выключения успешно отправлена. Synology NAS выключается..." + ) + return True + else: + return False + except Exception as e: + logger.error(f"Error during power off: {str(e)}") + return False + +async def handle_reboot(chat_id: int, context: ContextTypes.DEFAULT_TYPE) -> bool: + """Асинхронная функция для перезагрузки NAS""" + try: + # Отправка запроса на перезагрузку + success = synology_api.reboot_system() + + if success: + await context.bot.send_message( + chat_id=chat_id, + text="✅ Команда перезагрузки успешно отправлена. Synology NAS перезагружается..." + ) + + # Ждем некоторое время перед проверкой статуса + await context.bot.send_message( + chat_id=chat_id, + text="⏳ Ожидание перезагрузки системы..." + ) + + # Создаем задачу для ожидания загрузки + wait_successful = synology_api.wait_for_boot() + + if wait_successful: + await context.bot.send_message( + chat_id=chat_id, + text="✅ Synology NAS успешно перезагружен и снова онлайн" + ) + else: + await context.bot.send_message( + chat_id=chat_id, + text="⚠️ Превышено время ожидания перезагрузки. Проверьте статус вручную." + ) + + return True + else: + return False + except Exception as e: + logger.error(f"Error during reboot: {str(e)}") + return False diff --git a/.history/src/handlers/command_handlers_20250830073858.py b/.history/src/handlers/command_handlers_20250830073858.py new file mode 100644 index 0000000..b2108a8 --- /dev/null +++ b/.history/src/handlers/command_handlers_20250830073858.py @@ -0,0 +1,328 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Обработчики команд для телеграм-бота +""" + +import logging +from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup +from telegram.ext import ContextTypes + +from src.config.config import ADMIN_USER_IDS +from src.api.synology import SynologyAPI + +logger = logging.getLogger(__name__) + +# Инициализация API Synology +synology_api = SynologyAPI() + +async def start_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /start""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + logger.warning(f"Unauthorized access attempt from user ID: {user_id}") + return + + await update.message.reply_text( + f"Привет, {update.effective_user.first_name}! 👋\n\n" + "Я бот для управления вашим Synology NAS.\n" + "Используйте следующие команды:\n\n" + "Основные команды:\n" + "/status - Проверка статуса NAS\n" + "/power - Управление питанием NAS\n" + "/system - Информация о системе\n" + "/storage - Информация о хранилище\n\n" + "Используйте /help для получения полного списка команд", + parse_mode="HTML" + ) + +async def help_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /help""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + await update.message.reply_text( + "📖 Помощь по использованию бота\n\n" + "Основные команды:\n" + "/start - Начало работы с ботом\n" + "/status - Проверка текущего статуса NAS\n" + "/power - Управление питанием NAS\n" + "/help - Вывод этой справки\n\n" + "Расширенные команды:\n" + "/system - Подробная информация о системе\n" + "/storage - Информация о хранилище и дисках\n" + "/shares - Список общих папок\n" + "/load - Текущая нагрузка на систему\n" + "/security - Статус безопасности системы\n\n" + "Управление питанием:\n" + "• Включение NAS: Wake-on-LAN\n" + "• Выключение NAS: Безопасное завершение работы\n" + "• Перезагрузка: Безопасная перезагрузка\n\n" + "Примечание: Для работы функции включения необходимо, " + "чтобы на NAS была настроена функция Wake-on-LAN.", + parse_mode="HTML" + ) + +async def status_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /status""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + message = await update.message.reply_text("⏳ Проверяю статус Synology NAS...") + + is_online = synology_api.is_online() + + if is_online: + # Если NAS включен, попробуем получить дополнительную информацию + system_info = synology_api.get_system_status() + + if system_info and system_info.get("status") != "error": + model = system_info.get("model", "Неизвестная модель") + version = system_info.get("version_string", system_info.get("version", "Неизвестная версия")) + uptime_seconds = system_info.get("up_time", system_info.get("uptime", 0)) + + # Преобразование времени работы в удобочитаемый формат + days, remainder = divmod(int(uptime_seconds), 86400) + hours, remainder = divmod(remainder, 3600) + minutes, seconds = divmod(remainder, 60) + uptime_str = f"{days}д {hours}ч {minutes}м {seconds}с" + + await message.edit_text( + f"✅ Synology NAS онлайн\n\n" + f"Модель: {model}\n" + f"Версия DSM: {version}\n" + f"Время работы: {uptime_str}", + parse_mode="HTML" + ) + else: + # Обработка возможной ошибки API + error_info = "" + if system_info and system_info.get("status") == "error": + error_code = system_info.get("error_code", "неизвестно") + error_info = f"\nКод ошибки API: {error_code}" + + await message.edit_text( + f"✅ Synology NAS онлайн\n\n" + f"Детальная информация недоступна. Возможно, необходимо авторизоваться." + f"{error_info}", + parse_mode="HTML" + ) + else: + await message.edit_text("❌ Synology NAS оффлайн", parse_mode="HTML") + +async def power_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /power""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + is_online = synology_api.is_online() + + keyboard = [] + + # Кнопка включения + if not is_online: + keyboard.append([InlineKeyboardButton("🟢 Включить", callback_data="power_on")]) + else: + keyboard.append([InlineKeyboardButton("🟢 Включить (уже включено)", callback_data="power_on_no_op")]) + + # Кнопка выключения + if is_online: + keyboard.append([InlineKeyboardButton("🔴 Выключить", callback_data="power_off")]) + else: + keyboard.append([InlineKeyboardButton("🔴 Выключить (уже выключено)", callback_data="power_off_no_op")]) + + # Кнопка перезагрузки + if is_online: + keyboard.append([InlineKeyboardButton("🔄 Перезагрузить", callback_data="reboot")]) + else: + keyboard.append([InlineKeyboardButton("🔄 Перезагрузить (недоступно)", callback_data="reboot_no_op")]) + + # Кнопка отмены + keyboard.append([InlineKeyboardButton("❌ Отмена", callback_data="cancel")]) + + reply_markup = InlineKeyboardMarkup(keyboard) + + status_text = "✅ Онлайн" if is_online else "❌ Оффлайн" + + await update.message.reply_text( + f"Управление питанием Synology NAS\n\n" + f"Текущий статус: {status_text}\n\n" + f"Выберите действие:", + reply_markup=reply_markup, + parse_mode="HTML" + ) + +async def power_callback(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик callback-запросов для кнопок управления питанием""" + query = update.callback_query + await query.answer() + + user_id = query.from_user.id + if user_id not in ADMIN_USER_IDS: + return + + action = query.data + + if action == "cancel": + await query.edit_message_text("❌ Действие отменено") + return + + # Обработка неактивных кнопок + if action in ["power_on_no_op", "power_off_no_op", "reboot_no_op"]: + if action == "power_on_no_op": + await query.edit_message_text("ℹ️ Synology NAS уже включен") + elif action == "power_off_no_op": + await query.edit_message_text("ℹ️ Synology NAS уже выключен") + else: + await query.edit_message_text("ℹ️ Невозможно перезагрузить выключенное устройство") + return + + # Обработка основных действий + if action == "power_on": + await query.edit_message_text("⏳ Отправка сигнала включения Synology NAS...") + + if await context.application.create_task( + handle_power_on(query.message.chat_id, context) + ): + # Функция вернула True, успешное включение + pass + else: + # Функция вернула False, ошибка включения + await context.bot.send_message( + chat_id=query.message.chat_id, + text="❌ Ошибка при включении Synology NAS. Проверьте настройки Wake-on-LAN." + ) + + elif action == "power_off": + await query.edit_message_text("⏳ Отправка команды выключения Synology NAS...") + + if await context.application.create_task( + handle_power_off(query.message.chat_id, context) + ): + # Функция вернула True, успешное выключение + pass + else: + # Функция вернула False, ошибка выключения + await context.bot.send_message( + chat_id=query.message.chat_id, + text="❌ Ошибка при выключении Synology NAS. Проверьте соединение и учетные данные." + ) + + elif action == "reboot": + await query.edit_message_text("⏳ Отправка команды перезагрузки Synology NAS...") + + if await context.application.create_task( + handle_reboot(query.message.chat_id, context) + ): + # Функция вернула True, успешная перезагрузка + pass + else: + # Функция вернула False, ошибка перезагрузки + await context.bot.send_message( + chat_id=query.message.chat_id, + text="❌ Ошибка при перезагрузке Synology NAS. Проверьте соединение и учетные данные." + ) + +async def handle_power_on(chat_id: int, context: ContextTypes.DEFAULT_TYPE) -> bool: + """Асинхронная функция для включения NAS""" + try: + # Отправка запроса на включение + success = synology_api.power_on() + + if success: + await context.bot.send_message( + chat_id=chat_id, + text="✅ Synology NAS успешно включен и доступен" + ) + return True + else: + return False + except Exception as e: + logger.error(f"Error during power on: {str(e)}") + return False + +async def handle_power_off(chat_id: int, context: ContextTypes.DEFAULT_TYPE) -> bool: + """Асинхронная функция для выключения NAS""" + try: + # Проверка доступности NAS + if not synology_api.is_online(): + await context.bot.send_message( + chat_id=chat_id, + text="⚠️ Synology NAS не в сети. Невозможно отправить команду выключения." + ) + return False + + # Отправка запроса на выключение + success = synology_api.power_off() + + if success: + await context.bot.send_message( + chat_id=chat_id, + text="✅ Команда выключения успешно отправлена. Synology NAS выключается..." + ) + return True + else: + await context.bot.send_message( + chat_id=chat_id, + text="⚠️ Ошибка при отправке команды выключения. Пожалуйста, проверьте журналы для получения дополнительной информации." + ) + return False + except Exception as e: + error_msg = str(e) + logger.error(f"Error during power off: {error_msg}") + await context.bot.send_message( + chat_id=chat_id, + text=f"❌ Ошибка при выключении: {error_msg[:100]}..." + ) + return False + +async def handle_reboot(chat_id: int, context: ContextTypes.DEFAULT_TYPE) -> bool: + """Асинхронная функция для перезагрузки NAS""" + try: + # Отправка запроса на перезагрузку + success = synology_api.reboot_system() + + if success: + await context.bot.send_message( + chat_id=chat_id, + text="✅ Команда перезагрузки успешно отправлена. Synology NAS перезагружается..." + ) + + # Ждем некоторое время перед проверкой статуса + await context.bot.send_message( + chat_id=chat_id, + text="⏳ Ожидание перезагрузки системы..." + ) + + # Создаем задачу для ожидания загрузки + wait_successful = synology_api.wait_for_boot() + + if wait_successful: + await context.bot.send_message( + chat_id=chat_id, + text="✅ Synology NAS успешно перезагружен и снова онлайн" + ) + else: + await context.bot.send_message( + chat_id=chat_id, + text="⚠️ Превышено время ожидания перезагрузки. Проверьте статус вручную." + ) + + return True + else: + return False + except Exception as e: + logger.error(f"Error during reboot: {str(e)}") + return False diff --git a/.history/src/handlers/command_handlers_20250830073916.py b/.history/src/handlers/command_handlers_20250830073916.py new file mode 100644 index 0000000..6af8fa1 --- /dev/null +++ b/.history/src/handlers/command_handlers_20250830073916.py @@ -0,0 +1,327 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Обработчики команд для телеграм-бота +""" + +import logging +from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup +from telegram.ext import ContextTypes + +from src.config.config import ADMIN_USER_IDS +from src.api.synology import SynologyAPI + +logger = logging.getLogger(__name__) + +# Инициализация API Synology +synology_api = SynologyAPI() + +async def start_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /start""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + logger.warning(f"Unauthorized access attempt from user ID: {user_id}") + return + + await update.message.reply_text( + f"Привет, {update.effective_user.first_name}! 👋\n\n" + "Я бот для управления вашим Synology NAS.\n" + "Используйте следующие команды:\n\n" + "Основные команды:\n" + "/status - Проверка статуса NAS\n" + "/power - Управление питанием NAS\n" + "/system - Информация о системе\n" + "/storage - Информация о хранилище\n\n" + "Используйте /help для получения полного списка команд", + parse_mode="HTML" + ) + +async def help_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /help""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + await update.message.reply_text( + "📖 Помощь по использованию бота\n\n" + "Основные команды:\n" + "/start - Начало работы с ботом\n" + "/status - Проверка текущего статуса NAS\n" + "/power - Управление питанием NAS\n" + "/help - Вывод этой справки\n\n" + "Расширенные команды:\n" + "/system - Подробная информация о системе\n" + "/storage - Информация о хранилище и дисках\n" + "/shares - Список общих папок\n" + "/load - Текущая нагрузка на систему\n" + "/security - Статус безопасности системы\n\n" + "Управление питанием:\n" + "• Включение NAS: Wake-on-LAN\n" + "• Выключение NAS: Безопасное завершение работы\n" + "• Перезагрузка: Безопасная перезагрузка\n\n" + "Примечание: Для работы функции включения необходимо, " + "чтобы на NAS была настроена функция Wake-on-LAN.", + parse_mode="HTML" + ) + +async def status_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /status""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + message = await update.message.reply_text("⏳ Проверяю статус Synology NAS...") + + is_online = synology_api.is_online() + + if is_online: + # Если NAS включен, попробуем получить дополнительную информацию + system_info = synology_api.get_system_status() + + if system_info and system_info.get("status") != "error": + model = system_info.get("model", "Неизвестная модель") + version = system_info.get("version_string", system_info.get("version", "Неизвестная версия")) + uptime_seconds = system_info.get("up_time", system_info.get("uptime", 0)) + + # Преобразование времени работы в удобочитаемый формат + days, remainder = divmod(int(uptime_seconds), 86400) + hours, remainder = divmod(remainder, 3600) + minutes, seconds = divmod(remainder, 60) + uptime_str = f"{days}д {hours}ч {minutes}м {seconds}с" + + await message.edit_text( + f"✅ Synology NAS онлайн\n\n" + f"Модель: {model}\n" + f"Версия DSM: {version}\n" + f"Время работы: {uptime_str}", + parse_mode="HTML" + ) + else: + # Обработка возможной ошибки API + error_info = "" + if system_info and system_info.get("status") == "error": + error_code = system_info.get("error_code", "неизвестно") + error_info = f"\nКод ошибки API: {error_code}" + + await message.edit_text( + f"✅ Synology NAS онлайн\n\n" + f"Детальная информация недоступна. Возможно, необходимо авторизоваться." + f"{error_info}", + parse_mode="HTML" + ) + else: + await message.edit_text("❌ Synology NAS оффлайн", parse_mode="HTML") + +async def power_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /power""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + is_online = synology_api.is_online() + + keyboard = [] + + # Кнопка включения + if not is_online: + keyboard.append([InlineKeyboardButton("🟢 Включить", callback_data="power_on")]) + else: + keyboard.append([InlineKeyboardButton("🟢 Включить (уже включено)", callback_data="power_on_no_op")]) + + # Кнопка выключения + if is_online: + keyboard.append([InlineKeyboardButton("🔴 Выключить", callback_data="power_off")]) + else: + keyboard.append([InlineKeyboardButton("🔴 Выключить (уже выключено)", callback_data="power_off_no_op")]) + + # Кнопка перезагрузки + if is_online: + keyboard.append([InlineKeyboardButton("🔄 Перезагрузить", callback_data="reboot")]) + else: + keyboard.append([InlineKeyboardButton("🔄 Перезагрузить (недоступно)", callback_data="reboot_no_op")]) + + # Кнопка отмены + keyboard.append([InlineKeyboardButton("❌ Отмена", callback_data="cancel")]) + + reply_markup = InlineKeyboardMarkup(keyboard) + + status_text = "✅ Онлайн" if is_online else "❌ Оффлайн" + + await update.message.reply_text( + f"Управление питанием Synology NAS\n\n" + f"Текущий статус: {status_text}\n\n" + f"Выберите действие:", + reply_markup=reply_markup, + parse_mode="HTML" + ) + +async def power_callback(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик callback-запросов для кнопок управления питанием""" + query = update.callback_query + await query.answer() + + user_id = query.from_user.id + if user_id not in ADMIN_USER_IDS: + return + + action = query.data + + if action == "cancel": + await query.edit_message_text("❌ Действие отменено") + return + + # Обработка неактивных кнопок + if action in ["power_on_no_op", "power_off_no_op", "reboot_no_op"]: + if action == "power_on_no_op": + await query.edit_message_text("ℹ️ Synology NAS уже включен") + elif action == "power_off_no_op": + await query.edit_message_text("ℹ️ Synology NAS уже выключен") + else: + await query.edit_message_text("ℹ️ Невозможно перезагрузить выключенное устройство") + return + + # Обработка основных действий + if action == "power_on": + await query.edit_message_text("⏳ Отправка сигнала включения Synology NAS...") + + if await context.application.create_task( + handle_power_on(query.message.chat_id, context) + ): + # Функция вернула True, успешное включение + pass + else: + # Функция вернула False, ошибка включения + await context.bot.send_message( + chat_id=query.message.chat_id, + text="❌ Ошибка при включении Synology NAS. Проверьте настройки Wake-on-LAN." + ) + + elif action == "power_off": + await query.edit_message_text("⏳ Отправка команды выключения Synology NAS...") + + try: + success = await handle_power_off(query.message.chat_id, context) + # Если handle_power_off уже отправил сообщение об успехе или ошибке, + # дополнительных сообщений не требуется + except Exception as e: + logger.error(f"Exception in power_off callback: {str(e)}") + await context.bot.send_message( + chat_id=query.message.chat_id, + text="❌ Ошибка при выключении Synology NAS. Проверьте соединение и учетные данные." + ) + + elif action == "reboot": + await query.edit_message_text("⏳ Отправка команды перезагрузки Synology NAS...") + + if await context.application.create_task( + handle_reboot(query.message.chat_id, context) + ): + # Функция вернула True, успешная перезагрузка + pass + else: + # Функция вернула False, ошибка перезагрузки + await context.bot.send_message( + chat_id=query.message.chat_id, + text="❌ Ошибка при перезагрузке Synology NAS. Проверьте соединение и учетные данные." + ) + +async def handle_power_on(chat_id: int, context: ContextTypes.DEFAULT_TYPE) -> bool: + """Асинхронная функция для включения NAS""" + try: + # Отправка запроса на включение + success = synology_api.power_on() + + if success: + await context.bot.send_message( + chat_id=chat_id, + text="✅ Synology NAS успешно включен и доступен" + ) + return True + else: + return False + except Exception as e: + logger.error(f"Error during power on: {str(e)}") + return False + +async def handle_power_off(chat_id: int, context: ContextTypes.DEFAULT_TYPE) -> bool: + """Асинхронная функция для выключения NAS""" + try: + # Проверка доступности NAS + if not synology_api.is_online(): + await context.bot.send_message( + chat_id=chat_id, + text="⚠️ Synology NAS не в сети. Невозможно отправить команду выключения." + ) + return False + + # Отправка запроса на выключение + success = synology_api.power_off() + + if success: + await context.bot.send_message( + chat_id=chat_id, + text="✅ Команда выключения успешно отправлена. Synology NAS выключается..." + ) + return True + else: + await context.bot.send_message( + chat_id=chat_id, + text="⚠️ Ошибка при отправке команды выключения. Пожалуйста, проверьте журналы для получения дополнительной информации." + ) + return False + except Exception as e: + error_msg = str(e) + logger.error(f"Error during power off: {error_msg}") + await context.bot.send_message( + chat_id=chat_id, + text=f"❌ Ошибка при выключении: {error_msg[:100]}..." + ) + return False + +async def handle_reboot(chat_id: int, context: ContextTypes.DEFAULT_TYPE) -> bool: + """Асинхронная функция для перезагрузки NAS""" + try: + # Отправка запроса на перезагрузку + success = synology_api.reboot_system() + + if success: + await context.bot.send_message( + chat_id=chat_id, + text="✅ Команда перезагрузки успешно отправлена. Synology NAS перезагружается..." + ) + + # Ждем некоторое время перед проверкой статуса + await context.bot.send_message( + chat_id=chat_id, + text="⏳ Ожидание перезагрузки системы..." + ) + + # Создаем задачу для ожидания загрузки + wait_successful = synology_api.wait_for_boot() + + if wait_successful: + await context.bot.send_message( + chat_id=chat_id, + text="✅ Synology NAS успешно перезагружен и снова онлайн" + ) + else: + await context.bot.send_message( + chat_id=chat_id, + text="⚠️ Превышено время ожидания перезагрузки. Проверьте статус вручную." + ) + + return True + else: + return False + except Exception as e: + logger.error(f"Error during reboot: {str(e)}") + return False diff --git a/.history/src/handlers/command_handlers_20250830074106.py b/.history/src/handlers/command_handlers_20250830074106.py new file mode 100644 index 0000000..b9c0c32 --- /dev/null +++ b/.history/src/handlers/command_handlers_20250830074106.py @@ -0,0 +1,380 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Обработчики команд для телеграм-бота +""" + +import logging +from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup +from telegram.ext import ContextTypes + +from src.config.config import ADMIN_USER_IDS +from src.api.synology import SynologyAPI + +logger = logging.getLogger(__name__) + +# Инициализация API Synology +synology_api = SynologyAPI() + +async def start_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /start""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + logger.warning(f"Unauthorized access attempt from user ID: {user_id}") + return + + await update.message.reply_text( + f"Привет, {update.effective_user.first_name}! 👋\n\n" + "Я бот для управления вашим Synology NAS.\n" + "Используйте следующие команды:\n\n" + "Основные команды:\n" + "/status - Проверка статуса NAS\n" + "/power - Управление питанием NAS\n" + "/system - Информация о системе\n" + "/storage - Информация о хранилище\n\n" + "Используйте /help для получения полного списка команд", + parse_mode="HTML" + ) + +async def help_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /help""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + await update.message.reply_text( + "📖 Помощь по использованию бота\n\n" + "Основные команды:\n" + "/start - Начало работы с ботом\n" + "/status - Проверка текущего статуса NAS\n" + "/power - Управление питанием NAS\n" + "/help - Вывод этой справки\n\n" + "Расширенные команды:\n" + "/system - Подробная информация о системе\n" + "/storage - Информация о хранилище и дисках\n" + "/shares - Список общих папок\n" + "/load - Текущая нагрузка на систему\n" + "/security - Статус безопасности системы\n\n" + "Управление питанием:\n" + "• Включение NAS: Wake-on-LAN\n" + "• Выключение NAS: Безопасное завершение работы\n" + "• Перезагрузка: Безопасная перезагрузка\n\n" + "Примечание: Для работы функции включения необходимо, " + "чтобы на NAS была настроена функция Wake-on-LAN.", + parse_mode="HTML" + ) + +async def status_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /status""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + message = await update.message.reply_text("⏳ Проверяю статус Synology NAS...") + + is_online = synology_api.is_online() + + if is_online: + try: + # Если NAS включен, попробуем получить дополнительную информацию + system_info = synology_api.get_system_status() + + if system_info and system_info.get("status") != "error": + model = system_info.get("model", "Неизвестная модель") + version = system_info.get("version_string", system_info.get("version", "Неизвестная версия")) + uptime_seconds = system_info.get("up_time", system_info.get("uptime", 0)) + + # Преобразование времени работы в удобочитаемый формат + days, remainder = divmod(int(uptime_seconds), 86400) + hours, remainder = divmod(remainder, 3600) + minutes, seconds = divmod(remainder, 60) + uptime_str = f"{days}д {hours}ч {minutes}м {seconds}с" + + await message.edit_text( + f"✅ Synology NAS онлайн\n\n" + f"Модель: {model}\n" + f"Версия DSM: {version}\n" + f"Время работы: {uptime_str}", + parse_mode="HTML" + ) + else: + # Обработка возможной ошибки API + error_info = "" + if system_info and system_info.get("status") == "error": + error_code = system_info.get("error_code", "неизвестно") + error_info = f"\nКод ошибки API: {error_code}" + + # Проверяем порт и сеть + network_info = "" + try: + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + s.settimeout(1) + result = s.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT)) + s.close() + if result == 0: + network_info = f"\n\nСетевая информация:\nIP: {SYNOLOGY_HOST}\nПорт {SYNOLOGY_PORT}: открыт" + else: + network_info = f"\n\nСетевая информация:\nIP: {SYNOLOGY_HOST}\nПорт {SYNOLOGY_PORT}: закрыт (код {result})" + except Exception as e: + network_info = f"\n\nСетевая информация:\nIP: {SYNOLOGY_HOST}\nОшибка проверки порта: {str(e)}" + + await message.edit_text( + f"✅ Synology NAS онлайн\n\n" + f"Устройство доступно по сети, но детальная информация через API недоступна. " + f"Возможно, необходимо проверить учетные данные или права доступа." + f"{error_info}" + f"{network_info}", + parse_mode="HTML" + ) + except Exception as e: + await message.edit_text( + f"✅ Synology NAS онлайн\n\n" + f"Ошибка при получении информации: {str(e)[:100]}...\n\n" + f"Сетевая информация:\nIP: {SYNOLOGY_HOST}\nПорт: {SYNOLOGY_PORT}", + parse_mode="HTML" + ) + else: + # Устройство не в сети, проверим соседние порты для диагностики + port_scan_info = "" + try: + for test_port in [SYNOLOGY_PORT-1, SYNOLOGY_PORT, SYNOLOGY_PORT+1, 5000, 5001]: + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + s.settimeout(1) + result = s.connect_ex((SYNOLOGY_HOST, test_port)) + s.close() + status = "открыт" if result == 0 else "закрыт" + port_scan_info += f"Порт {test_port}: {status}\n" + + # Добавим информацию о MAC-адресе для WoL + mac_info = f"MAC: {SYNOLOGY_MAC}" if SYNOLOGY_MAC else "MAC-адрес не настроен" + + await message.edit_text( + f"❌ Synology NAS оффлайн\n\n" + f"Информация о сети:\n" + f"IP: {SYNOLOGY_HOST}\n" + f"{port_scan_info}\n" + f"{mac_info}\n\n" + f"Используйте /power для отправки Wake-on-LAN пакета", + parse_mode="HTML" + ) + except Exception as e: + await message.edit_text( + f"❌ Synology NAS оффлайн\n\n" + f"Ошибка при сканировании портов: {str(e)[:100]}...\n\n" + f"Используйте /power для отправки Wake-on-LAN пакета", + parse_mode="HTML" + ) + +async def power_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /power""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + is_online = synology_api.is_online() + + keyboard = [] + + # Кнопка включения + if not is_online: + keyboard.append([InlineKeyboardButton("🟢 Включить", callback_data="power_on")]) + else: + keyboard.append([InlineKeyboardButton("🟢 Включить (уже включено)", callback_data="power_on_no_op")]) + + # Кнопка выключения + if is_online: + keyboard.append([InlineKeyboardButton("🔴 Выключить", callback_data="power_off")]) + else: + keyboard.append([InlineKeyboardButton("🔴 Выключить (уже выключено)", callback_data="power_off_no_op")]) + + # Кнопка перезагрузки + if is_online: + keyboard.append([InlineKeyboardButton("🔄 Перезагрузить", callback_data="reboot")]) + else: + keyboard.append([InlineKeyboardButton("🔄 Перезагрузить (недоступно)", callback_data="reboot_no_op")]) + + # Кнопка отмены + keyboard.append([InlineKeyboardButton("❌ Отмена", callback_data="cancel")]) + + reply_markup = InlineKeyboardMarkup(keyboard) + + status_text = "✅ Онлайн" if is_online else "❌ Оффлайн" + + await update.message.reply_text( + f"Управление питанием Synology NAS\n\n" + f"Текущий статус: {status_text}\n\n" + f"Выберите действие:", + reply_markup=reply_markup, + parse_mode="HTML" + ) + +async def power_callback(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик callback-запросов для кнопок управления питанием""" + query = update.callback_query + await query.answer() + + user_id = query.from_user.id + if user_id not in ADMIN_USER_IDS: + return + + action = query.data + + if action == "cancel": + await query.edit_message_text("❌ Действие отменено") + return + + # Обработка неактивных кнопок + if action in ["power_on_no_op", "power_off_no_op", "reboot_no_op"]: + if action == "power_on_no_op": + await query.edit_message_text("ℹ️ Synology NAS уже включен") + elif action == "power_off_no_op": + await query.edit_message_text("ℹ️ Synology NAS уже выключен") + else: + await query.edit_message_text("ℹ️ Невозможно перезагрузить выключенное устройство") + return + + # Обработка основных действий + if action == "power_on": + await query.edit_message_text("⏳ Отправка сигнала включения Synology NAS...") + + if await context.application.create_task( + handle_power_on(query.message.chat_id, context) + ): + # Функция вернула True, успешное включение + pass + else: + # Функция вернула False, ошибка включения + await context.bot.send_message( + chat_id=query.message.chat_id, + text="❌ Ошибка при включении Synology NAS. Проверьте настройки Wake-on-LAN." + ) + + elif action == "power_off": + await query.edit_message_text("⏳ Отправка команды выключения Synology NAS...") + + try: + success = await handle_power_off(query.message.chat_id, context) + # Если handle_power_off уже отправил сообщение об успехе или ошибке, + # дополнительных сообщений не требуется + except Exception as e: + logger.error(f"Exception in power_off callback: {str(e)}") + await context.bot.send_message( + chat_id=query.message.chat_id, + text="❌ Ошибка при выключении Synology NAS. Проверьте соединение и учетные данные." + ) + + elif action == "reboot": + await query.edit_message_text("⏳ Отправка команды перезагрузки Synology NAS...") + + if await context.application.create_task( + handle_reboot(query.message.chat_id, context) + ): + # Функция вернула True, успешная перезагрузка + pass + else: + # Функция вернула False, ошибка перезагрузки + await context.bot.send_message( + chat_id=query.message.chat_id, + text="❌ Ошибка при перезагрузке Synology NAS. Проверьте соединение и учетные данные." + ) + +async def handle_power_on(chat_id: int, context: ContextTypes.DEFAULT_TYPE) -> bool: + """Асинхронная функция для включения NAS""" + try: + # Отправка запроса на включение + success = synology_api.power_on() + + if success: + await context.bot.send_message( + chat_id=chat_id, + text="✅ Synology NAS успешно включен и доступен" + ) + return True + else: + return False + except Exception as e: + logger.error(f"Error during power on: {str(e)}") + return False + +async def handle_power_off(chat_id: int, context: ContextTypes.DEFAULT_TYPE) -> bool: + """Асинхронная функция для выключения NAS""" + try: + # Проверка доступности NAS + if not synology_api.is_online(): + await context.bot.send_message( + chat_id=chat_id, + text="⚠️ Synology NAS не в сети. Невозможно отправить команду выключения." + ) + return False + + # Отправка запроса на выключение + success = synology_api.power_off() + + if success: + await context.bot.send_message( + chat_id=chat_id, + text="✅ Команда выключения успешно отправлена. Synology NAS выключается..." + ) + return True + else: + await context.bot.send_message( + chat_id=chat_id, + text="⚠️ Ошибка при отправке команды выключения. Пожалуйста, проверьте журналы для получения дополнительной информации." + ) + return False + except Exception as e: + error_msg = str(e) + logger.error(f"Error during power off: {error_msg}") + await context.bot.send_message( + chat_id=chat_id, + text=f"❌ Ошибка при выключении: {error_msg[:100]}..." + ) + return False + +async def handle_reboot(chat_id: int, context: ContextTypes.DEFAULT_TYPE) -> bool: + """Асинхронная функция для перезагрузки NAS""" + try: + # Отправка запроса на перезагрузку + success = synology_api.reboot_system() + + if success: + await context.bot.send_message( + chat_id=chat_id, + text="✅ Команда перезагрузки успешно отправлена. Synology NAS перезагружается..." + ) + + # Ждем некоторое время перед проверкой статуса + await context.bot.send_message( + chat_id=chat_id, + text="⏳ Ожидание перезагрузки системы..." + ) + + # Создаем задачу для ожидания загрузки + wait_successful = synology_api.wait_for_boot() + + if wait_successful: + await context.bot.send_message( + chat_id=chat_id, + text="✅ Synology NAS успешно перезагружен и снова онлайн" + ) + else: + await context.bot.send_message( + chat_id=chat_id, + text="⚠️ Превышено время ожидания перезагрузки. Проверьте статус вручную." + ) + + return True + else: + return False + except Exception as e: + logger.error(f"Error during reboot: {str(e)}") + return False diff --git a/.history/src/handlers/command_handlers_20250830074122.py b/.history/src/handlers/command_handlers_20250830074122.py new file mode 100644 index 0000000..688dfc7 --- /dev/null +++ b/.history/src/handlers/command_handlers_20250830074122.py @@ -0,0 +1,383 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Обработчики команд для телеграм-бота +""" + +import logging +import socket +from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup +from telegram.ext import ContextTypes + +from src.config.config import ( + ADMIN_USER_IDS, SYNOLOGY_HOST, SYNOLOGY_PORT, SYNOLOGY_MAC +) +from src.api.synology import SynologyAPI + +logger = logging.getLogger(__name__) + +# Инициализация API Synology +synology_api = SynologyAPI() + +async def start_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /start""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + logger.warning(f"Unauthorized access attempt from user ID: {user_id}") + return + + await update.message.reply_text( + f"Привет, {update.effective_user.first_name}! 👋\n\n" + "Я бот для управления вашим Synology NAS.\n" + "Используйте следующие команды:\n\n" + "Основные команды:\n" + "/status - Проверка статуса NAS\n" + "/power - Управление питанием NAS\n" + "/system - Информация о системе\n" + "/storage - Информация о хранилище\n\n" + "Используйте /help для получения полного списка команд", + parse_mode="HTML" + ) + +async def help_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /help""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + await update.message.reply_text( + "📖 Помощь по использованию бота\n\n" + "Основные команды:\n" + "/start - Начало работы с ботом\n" + "/status - Проверка текущего статуса NAS\n" + "/power - Управление питанием NAS\n" + "/help - Вывод этой справки\n\n" + "Расширенные команды:\n" + "/system - Подробная информация о системе\n" + "/storage - Информация о хранилище и дисках\n" + "/shares - Список общих папок\n" + "/load - Текущая нагрузка на систему\n" + "/security - Статус безопасности системы\n\n" + "Управление питанием:\n" + "• Включение NAS: Wake-on-LAN\n" + "• Выключение NAS: Безопасное завершение работы\n" + "• Перезагрузка: Безопасная перезагрузка\n\n" + "Примечание: Для работы функции включения необходимо, " + "чтобы на NAS была настроена функция Wake-on-LAN.", + parse_mode="HTML" + ) + +async def status_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /status""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + message = await update.message.reply_text("⏳ Проверяю статус Synology NAS...") + + is_online = synology_api.is_online() + + if is_online: + try: + # Если NAS включен, попробуем получить дополнительную информацию + system_info = synology_api.get_system_status() + + if system_info and system_info.get("status") != "error": + model = system_info.get("model", "Неизвестная модель") + version = system_info.get("version_string", system_info.get("version", "Неизвестная версия")) + uptime_seconds = system_info.get("up_time", system_info.get("uptime", 0)) + + # Преобразование времени работы в удобочитаемый формат + days, remainder = divmod(int(uptime_seconds), 86400) + hours, remainder = divmod(remainder, 3600) + minutes, seconds = divmod(remainder, 60) + uptime_str = f"{days}д {hours}ч {minutes}м {seconds}с" + + await message.edit_text( + f"✅ Synology NAS онлайн\n\n" + f"Модель: {model}\n" + f"Версия DSM: {version}\n" + f"Время работы: {uptime_str}", + parse_mode="HTML" + ) + else: + # Обработка возможной ошибки API + error_info = "" + if system_info and system_info.get("status") == "error": + error_code = system_info.get("error_code", "неизвестно") + error_info = f"\nКод ошибки API: {error_code}" + + # Проверяем порт и сеть + network_info = "" + try: + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + s.settimeout(1) + result = s.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT)) + s.close() + if result == 0: + network_info = f"\n\nСетевая информация:\nIP: {SYNOLOGY_HOST}\nПорт {SYNOLOGY_PORT}: открыт" + else: + network_info = f"\n\nСетевая информация:\nIP: {SYNOLOGY_HOST}\nПорт {SYNOLOGY_PORT}: закрыт (код {result})" + except Exception as e: + network_info = f"\n\nСетевая информация:\nIP: {SYNOLOGY_HOST}\nОшибка проверки порта: {str(e)}" + + await message.edit_text( + f"✅ Synology NAS онлайн\n\n" + f"Устройство доступно по сети, но детальная информация через API недоступна. " + f"Возможно, необходимо проверить учетные данные или права доступа." + f"{error_info}" + f"{network_info}", + parse_mode="HTML" + ) + except Exception as e: + await message.edit_text( + f"✅ Synology NAS онлайн\n\n" + f"Ошибка при получении информации: {str(e)[:100]}...\n\n" + f"Сетевая информация:\nIP: {SYNOLOGY_HOST}\nПорт: {SYNOLOGY_PORT}", + parse_mode="HTML" + ) + else: + # Устройство не в сети, проверим соседние порты для диагностики + port_scan_info = "" + try: + for test_port in [SYNOLOGY_PORT-1, SYNOLOGY_PORT, SYNOLOGY_PORT+1, 5000, 5001]: + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + s.settimeout(1) + result = s.connect_ex((SYNOLOGY_HOST, test_port)) + s.close() + status = "открыт" if result == 0 else "закрыт" + port_scan_info += f"Порт {test_port}: {status}\n" + + # Добавим информацию о MAC-адресе для WoL + mac_info = f"MAC: {SYNOLOGY_MAC}" if SYNOLOGY_MAC else "MAC-адрес не настроен" + + await message.edit_text( + f"❌ Synology NAS оффлайн\n\n" + f"Информация о сети:\n" + f"IP: {SYNOLOGY_HOST}\n" + f"{port_scan_info}\n" + f"{mac_info}\n\n" + f"Используйте /power для отправки Wake-on-LAN пакета", + parse_mode="HTML" + ) + except Exception as e: + await message.edit_text( + f"❌ Synology NAS оффлайн\n\n" + f"Ошибка при сканировании портов: {str(e)[:100]}...\n\n" + f"Используйте /power для отправки Wake-on-LAN пакета", + parse_mode="HTML" + ) + +async def power_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /power""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + is_online = synology_api.is_online() + + keyboard = [] + + # Кнопка включения + if not is_online: + keyboard.append([InlineKeyboardButton("🟢 Включить", callback_data="power_on")]) + else: + keyboard.append([InlineKeyboardButton("🟢 Включить (уже включено)", callback_data="power_on_no_op")]) + + # Кнопка выключения + if is_online: + keyboard.append([InlineKeyboardButton("🔴 Выключить", callback_data="power_off")]) + else: + keyboard.append([InlineKeyboardButton("🔴 Выключить (уже выключено)", callback_data="power_off_no_op")]) + + # Кнопка перезагрузки + if is_online: + keyboard.append([InlineKeyboardButton("🔄 Перезагрузить", callback_data="reboot")]) + else: + keyboard.append([InlineKeyboardButton("🔄 Перезагрузить (недоступно)", callback_data="reboot_no_op")]) + + # Кнопка отмены + keyboard.append([InlineKeyboardButton("❌ Отмена", callback_data="cancel")]) + + reply_markup = InlineKeyboardMarkup(keyboard) + + status_text = "✅ Онлайн" if is_online else "❌ Оффлайн" + + await update.message.reply_text( + f"Управление питанием Synology NAS\n\n" + f"Текущий статус: {status_text}\n\n" + f"Выберите действие:", + reply_markup=reply_markup, + parse_mode="HTML" + ) + +async def power_callback(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик callback-запросов для кнопок управления питанием""" + query = update.callback_query + await query.answer() + + user_id = query.from_user.id + if user_id not in ADMIN_USER_IDS: + return + + action = query.data + + if action == "cancel": + await query.edit_message_text("❌ Действие отменено") + return + + # Обработка неактивных кнопок + if action in ["power_on_no_op", "power_off_no_op", "reboot_no_op"]: + if action == "power_on_no_op": + await query.edit_message_text("ℹ️ Synology NAS уже включен") + elif action == "power_off_no_op": + await query.edit_message_text("ℹ️ Synology NAS уже выключен") + else: + await query.edit_message_text("ℹ️ Невозможно перезагрузить выключенное устройство") + return + + # Обработка основных действий + if action == "power_on": + await query.edit_message_text("⏳ Отправка сигнала включения Synology NAS...") + + if await context.application.create_task( + handle_power_on(query.message.chat_id, context) + ): + # Функция вернула True, успешное включение + pass + else: + # Функция вернула False, ошибка включения + await context.bot.send_message( + chat_id=query.message.chat_id, + text="❌ Ошибка при включении Synology NAS. Проверьте настройки Wake-on-LAN." + ) + + elif action == "power_off": + await query.edit_message_text("⏳ Отправка команды выключения Synology NAS...") + + try: + success = await handle_power_off(query.message.chat_id, context) + # Если handle_power_off уже отправил сообщение об успехе или ошибке, + # дополнительных сообщений не требуется + except Exception as e: + logger.error(f"Exception in power_off callback: {str(e)}") + await context.bot.send_message( + chat_id=query.message.chat_id, + text="❌ Ошибка при выключении Synology NAS. Проверьте соединение и учетные данные." + ) + + elif action == "reboot": + await query.edit_message_text("⏳ Отправка команды перезагрузки Synology NAS...") + + if await context.application.create_task( + handle_reboot(query.message.chat_id, context) + ): + # Функция вернула True, успешная перезагрузка + pass + else: + # Функция вернула False, ошибка перезагрузки + await context.bot.send_message( + chat_id=query.message.chat_id, + text="❌ Ошибка при перезагрузке Synology NAS. Проверьте соединение и учетные данные." + ) + +async def handle_power_on(chat_id: int, context: ContextTypes.DEFAULT_TYPE) -> bool: + """Асинхронная функция для включения NAS""" + try: + # Отправка запроса на включение + success = synology_api.power_on() + + if success: + await context.bot.send_message( + chat_id=chat_id, + text="✅ Synology NAS успешно включен и доступен" + ) + return True + else: + return False + except Exception as e: + logger.error(f"Error during power on: {str(e)}") + return False + +async def handle_power_off(chat_id: int, context: ContextTypes.DEFAULT_TYPE) -> bool: + """Асинхронная функция для выключения NAS""" + try: + # Проверка доступности NAS + if not synology_api.is_online(): + await context.bot.send_message( + chat_id=chat_id, + text="⚠️ Synology NAS не в сети. Невозможно отправить команду выключения." + ) + return False + + # Отправка запроса на выключение + success = synology_api.power_off() + + if success: + await context.bot.send_message( + chat_id=chat_id, + text="✅ Команда выключения успешно отправлена. Synology NAS выключается..." + ) + return True + else: + await context.bot.send_message( + chat_id=chat_id, + text="⚠️ Ошибка при отправке команды выключения. Пожалуйста, проверьте журналы для получения дополнительной информации." + ) + return False + except Exception as e: + error_msg = str(e) + logger.error(f"Error during power off: {error_msg}") + await context.bot.send_message( + chat_id=chat_id, + text=f"❌ Ошибка при выключении: {error_msg[:100]}..." + ) + return False + +async def handle_reboot(chat_id: int, context: ContextTypes.DEFAULT_TYPE) -> bool: + """Асинхронная функция для перезагрузки NAS""" + try: + # Отправка запроса на перезагрузку + success = synology_api.reboot_system() + + if success: + await context.bot.send_message( + chat_id=chat_id, + text="✅ Команда перезагрузки успешно отправлена. Synology NAS перезагружается..." + ) + + # Ждем некоторое время перед проверкой статуса + await context.bot.send_message( + chat_id=chat_id, + text="⏳ Ожидание перезагрузки системы..." + ) + + # Создаем задачу для ожидания загрузки + wait_successful = synology_api.wait_for_boot() + + if wait_successful: + await context.bot.send_message( + chat_id=chat_id, + text="✅ Synology NAS успешно перезагружен и снова онлайн" + ) + else: + await context.bot.send_message( + chat_id=chat_id, + text="⚠️ Превышено время ожидания перезагрузки. Проверьте статус вручную." + ) + + return True + else: + return False + except Exception as e: + logger.error(f"Error during reboot: {str(e)}") + return False diff --git a/.history/src/handlers/command_handlers_20250830074140.py b/.history/src/handlers/command_handlers_20250830074140.py new file mode 100644 index 0000000..688dfc7 --- /dev/null +++ b/.history/src/handlers/command_handlers_20250830074140.py @@ -0,0 +1,383 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Обработчики команд для телеграм-бота +""" + +import logging +import socket +from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup +from telegram.ext import ContextTypes + +from src.config.config import ( + ADMIN_USER_IDS, SYNOLOGY_HOST, SYNOLOGY_PORT, SYNOLOGY_MAC +) +from src.api.synology import SynologyAPI + +logger = logging.getLogger(__name__) + +# Инициализация API Synology +synology_api = SynologyAPI() + +async def start_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /start""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + logger.warning(f"Unauthorized access attempt from user ID: {user_id}") + return + + await update.message.reply_text( + f"Привет, {update.effective_user.first_name}! 👋\n\n" + "Я бот для управления вашим Synology NAS.\n" + "Используйте следующие команды:\n\n" + "Основные команды:\n" + "/status - Проверка статуса NAS\n" + "/power - Управление питанием NAS\n" + "/system - Информация о системе\n" + "/storage - Информация о хранилище\n\n" + "Используйте /help для получения полного списка команд", + parse_mode="HTML" + ) + +async def help_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /help""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + await update.message.reply_text( + "📖 Помощь по использованию бота\n\n" + "Основные команды:\n" + "/start - Начало работы с ботом\n" + "/status - Проверка текущего статуса NAS\n" + "/power - Управление питанием NAS\n" + "/help - Вывод этой справки\n\n" + "Расширенные команды:\n" + "/system - Подробная информация о системе\n" + "/storage - Информация о хранилище и дисках\n" + "/shares - Список общих папок\n" + "/load - Текущая нагрузка на систему\n" + "/security - Статус безопасности системы\n\n" + "Управление питанием:\n" + "• Включение NAS: Wake-on-LAN\n" + "• Выключение NAS: Безопасное завершение работы\n" + "• Перезагрузка: Безопасная перезагрузка\n\n" + "Примечание: Для работы функции включения необходимо, " + "чтобы на NAS была настроена функция Wake-on-LAN.", + parse_mode="HTML" + ) + +async def status_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /status""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + message = await update.message.reply_text("⏳ Проверяю статус Synology NAS...") + + is_online = synology_api.is_online() + + if is_online: + try: + # Если NAS включен, попробуем получить дополнительную информацию + system_info = synology_api.get_system_status() + + if system_info and system_info.get("status") != "error": + model = system_info.get("model", "Неизвестная модель") + version = system_info.get("version_string", system_info.get("version", "Неизвестная версия")) + uptime_seconds = system_info.get("up_time", system_info.get("uptime", 0)) + + # Преобразование времени работы в удобочитаемый формат + days, remainder = divmod(int(uptime_seconds), 86400) + hours, remainder = divmod(remainder, 3600) + minutes, seconds = divmod(remainder, 60) + uptime_str = f"{days}д {hours}ч {minutes}м {seconds}с" + + await message.edit_text( + f"✅ Synology NAS онлайн\n\n" + f"Модель: {model}\n" + f"Версия DSM: {version}\n" + f"Время работы: {uptime_str}", + parse_mode="HTML" + ) + else: + # Обработка возможной ошибки API + error_info = "" + if system_info and system_info.get("status") == "error": + error_code = system_info.get("error_code", "неизвестно") + error_info = f"\nКод ошибки API: {error_code}" + + # Проверяем порт и сеть + network_info = "" + try: + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + s.settimeout(1) + result = s.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT)) + s.close() + if result == 0: + network_info = f"\n\nСетевая информация:\nIP: {SYNOLOGY_HOST}\nПорт {SYNOLOGY_PORT}: открыт" + else: + network_info = f"\n\nСетевая информация:\nIP: {SYNOLOGY_HOST}\nПорт {SYNOLOGY_PORT}: закрыт (код {result})" + except Exception as e: + network_info = f"\n\nСетевая информация:\nIP: {SYNOLOGY_HOST}\nОшибка проверки порта: {str(e)}" + + await message.edit_text( + f"✅ Synology NAS онлайн\n\n" + f"Устройство доступно по сети, но детальная информация через API недоступна. " + f"Возможно, необходимо проверить учетные данные или права доступа." + f"{error_info}" + f"{network_info}", + parse_mode="HTML" + ) + except Exception as e: + await message.edit_text( + f"✅ Synology NAS онлайн\n\n" + f"Ошибка при получении информации: {str(e)[:100]}...\n\n" + f"Сетевая информация:\nIP: {SYNOLOGY_HOST}\nПорт: {SYNOLOGY_PORT}", + parse_mode="HTML" + ) + else: + # Устройство не в сети, проверим соседние порты для диагностики + port_scan_info = "" + try: + for test_port in [SYNOLOGY_PORT-1, SYNOLOGY_PORT, SYNOLOGY_PORT+1, 5000, 5001]: + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + s.settimeout(1) + result = s.connect_ex((SYNOLOGY_HOST, test_port)) + s.close() + status = "открыт" if result == 0 else "закрыт" + port_scan_info += f"Порт {test_port}: {status}\n" + + # Добавим информацию о MAC-адресе для WoL + mac_info = f"MAC: {SYNOLOGY_MAC}" if SYNOLOGY_MAC else "MAC-адрес не настроен" + + await message.edit_text( + f"❌ Synology NAS оффлайн\n\n" + f"Информация о сети:\n" + f"IP: {SYNOLOGY_HOST}\n" + f"{port_scan_info}\n" + f"{mac_info}\n\n" + f"Используйте /power для отправки Wake-on-LAN пакета", + parse_mode="HTML" + ) + except Exception as e: + await message.edit_text( + f"❌ Synology NAS оффлайн\n\n" + f"Ошибка при сканировании портов: {str(e)[:100]}...\n\n" + f"Используйте /power для отправки Wake-on-LAN пакета", + parse_mode="HTML" + ) + +async def power_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /power""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + is_online = synology_api.is_online() + + keyboard = [] + + # Кнопка включения + if not is_online: + keyboard.append([InlineKeyboardButton("🟢 Включить", callback_data="power_on")]) + else: + keyboard.append([InlineKeyboardButton("🟢 Включить (уже включено)", callback_data="power_on_no_op")]) + + # Кнопка выключения + if is_online: + keyboard.append([InlineKeyboardButton("🔴 Выключить", callback_data="power_off")]) + else: + keyboard.append([InlineKeyboardButton("🔴 Выключить (уже выключено)", callback_data="power_off_no_op")]) + + # Кнопка перезагрузки + if is_online: + keyboard.append([InlineKeyboardButton("🔄 Перезагрузить", callback_data="reboot")]) + else: + keyboard.append([InlineKeyboardButton("🔄 Перезагрузить (недоступно)", callback_data="reboot_no_op")]) + + # Кнопка отмены + keyboard.append([InlineKeyboardButton("❌ Отмена", callback_data="cancel")]) + + reply_markup = InlineKeyboardMarkup(keyboard) + + status_text = "✅ Онлайн" if is_online else "❌ Оффлайн" + + await update.message.reply_text( + f"Управление питанием Synology NAS\n\n" + f"Текущий статус: {status_text}\n\n" + f"Выберите действие:", + reply_markup=reply_markup, + parse_mode="HTML" + ) + +async def power_callback(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик callback-запросов для кнопок управления питанием""" + query = update.callback_query + await query.answer() + + user_id = query.from_user.id + if user_id not in ADMIN_USER_IDS: + return + + action = query.data + + if action == "cancel": + await query.edit_message_text("❌ Действие отменено") + return + + # Обработка неактивных кнопок + if action in ["power_on_no_op", "power_off_no_op", "reboot_no_op"]: + if action == "power_on_no_op": + await query.edit_message_text("ℹ️ Synology NAS уже включен") + elif action == "power_off_no_op": + await query.edit_message_text("ℹ️ Synology NAS уже выключен") + else: + await query.edit_message_text("ℹ️ Невозможно перезагрузить выключенное устройство") + return + + # Обработка основных действий + if action == "power_on": + await query.edit_message_text("⏳ Отправка сигнала включения Synology NAS...") + + if await context.application.create_task( + handle_power_on(query.message.chat_id, context) + ): + # Функция вернула True, успешное включение + pass + else: + # Функция вернула False, ошибка включения + await context.bot.send_message( + chat_id=query.message.chat_id, + text="❌ Ошибка при включении Synology NAS. Проверьте настройки Wake-on-LAN." + ) + + elif action == "power_off": + await query.edit_message_text("⏳ Отправка команды выключения Synology NAS...") + + try: + success = await handle_power_off(query.message.chat_id, context) + # Если handle_power_off уже отправил сообщение об успехе или ошибке, + # дополнительных сообщений не требуется + except Exception as e: + logger.error(f"Exception in power_off callback: {str(e)}") + await context.bot.send_message( + chat_id=query.message.chat_id, + text="❌ Ошибка при выключении Synology NAS. Проверьте соединение и учетные данные." + ) + + elif action == "reboot": + await query.edit_message_text("⏳ Отправка команды перезагрузки Synology NAS...") + + if await context.application.create_task( + handle_reboot(query.message.chat_id, context) + ): + # Функция вернула True, успешная перезагрузка + pass + else: + # Функция вернула False, ошибка перезагрузки + await context.bot.send_message( + chat_id=query.message.chat_id, + text="❌ Ошибка при перезагрузке Synology NAS. Проверьте соединение и учетные данные." + ) + +async def handle_power_on(chat_id: int, context: ContextTypes.DEFAULT_TYPE) -> bool: + """Асинхронная функция для включения NAS""" + try: + # Отправка запроса на включение + success = synology_api.power_on() + + if success: + await context.bot.send_message( + chat_id=chat_id, + text="✅ Synology NAS успешно включен и доступен" + ) + return True + else: + return False + except Exception as e: + logger.error(f"Error during power on: {str(e)}") + return False + +async def handle_power_off(chat_id: int, context: ContextTypes.DEFAULT_TYPE) -> bool: + """Асинхронная функция для выключения NAS""" + try: + # Проверка доступности NAS + if not synology_api.is_online(): + await context.bot.send_message( + chat_id=chat_id, + text="⚠️ Synology NAS не в сети. Невозможно отправить команду выключения." + ) + return False + + # Отправка запроса на выключение + success = synology_api.power_off() + + if success: + await context.bot.send_message( + chat_id=chat_id, + text="✅ Команда выключения успешно отправлена. Synology NAS выключается..." + ) + return True + else: + await context.bot.send_message( + chat_id=chat_id, + text="⚠️ Ошибка при отправке команды выключения. Пожалуйста, проверьте журналы для получения дополнительной информации." + ) + return False + except Exception as e: + error_msg = str(e) + logger.error(f"Error during power off: {error_msg}") + await context.bot.send_message( + chat_id=chat_id, + text=f"❌ Ошибка при выключении: {error_msg[:100]}..." + ) + return False + +async def handle_reboot(chat_id: int, context: ContextTypes.DEFAULT_TYPE) -> bool: + """Асинхронная функция для перезагрузки NAS""" + try: + # Отправка запроса на перезагрузку + success = synology_api.reboot_system() + + if success: + await context.bot.send_message( + chat_id=chat_id, + text="✅ Команда перезагрузки успешно отправлена. Synology NAS перезагружается..." + ) + + # Ждем некоторое время перед проверкой статуса + await context.bot.send_message( + chat_id=chat_id, + text="⏳ Ожидание перезагрузки системы..." + ) + + # Создаем задачу для ожидания загрузки + wait_successful = synology_api.wait_for_boot() + + if wait_successful: + await context.bot.send_message( + chat_id=chat_id, + text="✅ Synology NAS успешно перезагружен и снова онлайн" + ) + else: + await context.bot.send_message( + chat_id=chat_id, + text="⚠️ Превышено время ожидания перезагрузки. Проверьте статус вручную." + ) + + return True + else: + return False + except Exception as e: + logger.error(f"Error during reboot: {str(e)}") + return False diff --git a/.history/src/handlers/command_handlers_20250830083412.py b/.history/src/handlers/command_handlers_20250830083412.py new file mode 100644 index 0000000..3722e31 --- /dev/null +++ b/.history/src/handlers/command_handlers_20250830083412.py @@ -0,0 +1,385 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Обработчики команд для телеграм-бота +""" + +import logging +import socket +from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup +from telegram.ext import ContextTypes + +from src.config.config import ( + ADMIN_USER_IDS, SYNOLOGY_HOST, SYNOLOGY_PORT, SYNOLOGY_MAC +) +from src.api.synology import SynologyAPI + +logger = logging.getLogger(__name__) + +# Инициализация API Synology +synology_api = SynologyAPI() + +async def start_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /start""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + logger.warning(f"Unauthorized access attempt from user ID: {user_id}") + return + + await update.message.reply_text( + f"Привет, {update.effective_user.first_name}! 👋\n\n" + "Я бот для управления вашим Synology NAS.\n" + "Используйте следующие команды:\n\n" + "Основные команды:\n" + "/status - Проверка статуса NAS\n" + "/power - Управление питанием NAS\n" + "/system - Информация о системе\n" + "/storage - Информация о хранилище\n\n" + "Используйте /help для получения полного списка команд", + parse_mode="HTML" + ) + +async def help_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /help""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + await update.message.reply_text( + "📖 Помощь по использованию бота\n\n" + "Основные команды:\n" + "/start - Начало работы с ботом\n" + "/status - Проверка текущего статуса NAS\n" + "/power - Управление питанием NAS\n" + "/help - Вывод этой справки\n\n" + "Расширенные команды:\n" + "/system - Подробная информация о системе\n" + "/storage - Информация о хранилище и дисках\n" + "/shares - Список общих папок\n" + "/load - Текущая нагрузка на систему\n" + "/security - Статус безопасности системы\n\n" + "Диагностика:\n" + "/checkapi - Проверка доступных API Synology\n\n" + "Управление питанием:\n" + "• Включение NAS: Wake-on-LAN\n" + "• Выключение NAS: Безопасное завершение работы\n" + "• Перезагрузка: Безопасная перезагрузка\n\n" + "Примечание: Для работы функции включения необходимо, " + "чтобы на NAS была настроена функция Wake-on-LAN.", + parse_mode="HTML" + ) + +async def status_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /status""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + message = await update.message.reply_text("⏳ Проверяю статус Synology NAS...") + + is_online = synology_api.is_online() + + if is_online: + try: + # Если NAS включен, попробуем получить дополнительную информацию + system_info = synology_api.get_system_status() + + if system_info and system_info.get("status") != "error": + model = system_info.get("model", "Неизвестная модель") + version = system_info.get("version_string", system_info.get("version", "Неизвестная версия")) + uptime_seconds = system_info.get("up_time", system_info.get("uptime", 0)) + + # Преобразование времени работы в удобочитаемый формат + days, remainder = divmod(int(uptime_seconds), 86400) + hours, remainder = divmod(remainder, 3600) + minutes, seconds = divmod(remainder, 60) + uptime_str = f"{days}д {hours}ч {minutes}м {seconds}с" + + await message.edit_text( + f"✅ Synology NAS онлайн\n\n" + f"Модель: {model}\n" + f"Версия DSM: {version}\n" + f"Время работы: {uptime_str}", + parse_mode="HTML" + ) + else: + # Обработка возможной ошибки API + error_info = "" + if system_info and system_info.get("status") == "error": + error_code = system_info.get("error_code", "неизвестно") + error_info = f"\nКод ошибки API: {error_code}" + + # Проверяем порт и сеть + network_info = "" + try: + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + s.settimeout(1) + result = s.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT)) + s.close() + if result == 0: + network_info = f"\n\nСетевая информация:\nIP: {SYNOLOGY_HOST}\nПорт {SYNOLOGY_PORT}: открыт" + else: + network_info = f"\n\nСетевая информация:\nIP: {SYNOLOGY_HOST}\nПорт {SYNOLOGY_PORT}: закрыт (код {result})" + except Exception as e: + network_info = f"\n\nСетевая информация:\nIP: {SYNOLOGY_HOST}\nОшибка проверки порта: {str(e)}" + + await message.edit_text( + f"✅ Synology NAS онлайн\n\n" + f"Устройство доступно по сети, но детальная информация через API недоступна. " + f"Возможно, необходимо проверить учетные данные или права доступа." + f"{error_info}" + f"{network_info}", + parse_mode="HTML" + ) + except Exception as e: + await message.edit_text( + f"✅ Synology NAS онлайн\n\n" + f"Ошибка при получении информации: {str(e)[:100]}...\n\n" + f"Сетевая информация:\nIP: {SYNOLOGY_HOST}\nПорт: {SYNOLOGY_PORT}", + parse_mode="HTML" + ) + else: + # Устройство не в сети, проверим соседние порты для диагностики + port_scan_info = "" + try: + for test_port in [SYNOLOGY_PORT-1, SYNOLOGY_PORT, SYNOLOGY_PORT+1, 5000, 5001]: + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + s.settimeout(1) + result = s.connect_ex((SYNOLOGY_HOST, test_port)) + s.close() + status = "открыт" if result == 0 else "закрыт" + port_scan_info += f"Порт {test_port}: {status}\n" + + # Добавим информацию о MAC-адресе для WoL + mac_info = f"MAC: {SYNOLOGY_MAC}" if SYNOLOGY_MAC else "MAC-адрес не настроен" + + await message.edit_text( + f"❌ Synology NAS оффлайн\n\n" + f"Информация о сети:\n" + f"IP: {SYNOLOGY_HOST}\n" + f"{port_scan_info}\n" + f"{mac_info}\n\n" + f"Используйте /power для отправки Wake-on-LAN пакета", + parse_mode="HTML" + ) + except Exception as e: + await message.edit_text( + f"❌ Synology NAS оффлайн\n\n" + f"Ошибка при сканировании портов: {str(e)[:100]}...\n\n" + f"Используйте /power для отправки Wake-on-LAN пакета", + parse_mode="HTML" + ) + +async def power_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /power""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + is_online = synology_api.is_online() + + keyboard = [] + + # Кнопка включения + if not is_online: + keyboard.append([InlineKeyboardButton("🟢 Включить", callback_data="power_on")]) + else: + keyboard.append([InlineKeyboardButton("🟢 Включить (уже включено)", callback_data="power_on_no_op")]) + + # Кнопка выключения + if is_online: + keyboard.append([InlineKeyboardButton("🔴 Выключить", callback_data="power_off")]) + else: + keyboard.append([InlineKeyboardButton("🔴 Выключить (уже выключено)", callback_data="power_off_no_op")]) + + # Кнопка перезагрузки + if is_online: + keyboard.append([InlineKeyboardButton("🔄 Перезагрузить", callback_data="reboot")]) + else: + keyboard.append([InlineKeyboardButton("🔄 Перезагрузить (недоступно)", callback_data="reboot_no_op")]) + + # Кнопка отмены + keyboard.append([InlineKeyboardButton("❌ Отмена", callback_data="cancel")]) + + reply_markup = InlineKeyboardMarkup(keyboard) + + status_text = "✅ Онлайн" if is_online else "❌ Оффлайн" + + await update.message.reply_text( + f"Управление питанием Synology NAS\n\n" + f"Текущий статус: {status_text}\n\n" + f"Выберите действие:", + reply_markup=reply_markup, + parse_mode="HTML" + ) + +async def power_callback(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик callback-запросов для кнопок управления питанием""" + query = update.callback_query + await query.answer() + + user_id = query.from_user.id + if user_id not in ADMIN_USER_IDS: + return + + action = query.data + + if action == "cancel": + await query.edit_message_text("❌ Действие отменено") + return + + # Обработка неактивных кнопок + if action in ["power_on_no_op", "power_off_no_op", "reboot_no_op"]: + if action == "power_on_no_op": + await query.edit_message_text("ℹ️ Synology NAS уже включен") + elif action == "power_off_no_op": + await query.edit_message_text("ℹ️ Synology NAS уже выключен") + else: + await query.edit_message_text("ℹ️ Невозможно перезагрузить выключенное устройство") + return + + # Обработка основных действий + if action == "power_on": + await query.edit_message_text("⏳ Отправка сигнала включения Synology NAS...") + + if await context.application.create_task( + handle_power_on(query.message.chat_id, context) + ): + # Функция вернула True, успешное включение + pass + else: + # Функция вернула False, ошибка включения + await context.bot.send_message( + chat_id=query.message.chat_id, + text="❌ Ошибка при включении Synology NAS. Проверьте настройки Wake-on-LAN." + ) + + elif action == "power_off": + await query.edit_message_text("⏳ Отправка команды выключения Synology NAS...") + + try: + success = await handle_power_off(query.message.chat_id, context) + # Если handle_power_off уже отправил сообщение об успехе или ошибке, + # дополнительных сообщений не требуется + except Exception as e: + logger.error(f"Exception in power_off callback: {str(e)}") + await context.bot.send_message( + chat_id=query.message.chat_id, + text="❌ Ошибка при выключении Synology NAS. Проверьте соединение и учетные данные." + ) + + elif action == "reboot": + await query.edit_message_text("⏳ Отправка команды перезагрузки Synology NAS...") + + if await context.application.create_task( + handle_reboot(query.message.chat_id, context) + ): + # Функция вернула True, успешная перезагрузка + pass + else: + # Функция вернула False, ошибка перезагрузки + await context.bot.send_message( + chat_id=query.message.chat_id, + text="❌ Ошибка при перезагрузке Synology NAS. Проверьте соединение и учетные данные." + ) + +async def handle_power_on(chat_id: int, context: ContextTypes.DEFAULT_TYPE) -> bool: + """Асинхронная функция для включения NAS""" + try: + # Отправка запроса на включение + success = synology_api.power_on() + + if success: + await context.bot.send_message( + chat_id=chat_id, + text="✅ Synology NAS успешно включен и доступен" + ) + return True + else: + return False + except Exception as e: + logger.error(f"Error during power on: {str(e)}") + return False + +async def handle_power_off(chat_id: int, context: ContextTypes.DEFAULT_TYPE) -> bool: + """Асинхронная функция для выключения NAS""" + try: + # Проверка доступности NAS + if not synology_api.is_online(): + await context.bot.send_message( + chat_id=chat_id, + text="⚠️ Synology NAS не в сети. Невозможно отправить команду выключения." + ) + return False + + # Отправка запроса на выключение + success = synology_api.power_off() + + if success: + await context.bot.send_message( + chat_id=chat_id, + text="✅ Команда выключения успешно отправлена. Synology NAS выключается..." + ) + return True + else: + await context.bot.send_message( + chat_id=chat_id, + text="⚠️ Ошибка при отправке команды выключения. Пожалуйста, проверьте журналы для получения дополнительной информации." + ) + return False + except Exception as e: + error_msg = str(e) + logger.error(f"Error during power off: {error_msg}") + await context.bot.send_message( + chat_id=chat_id, + text=f"❌ Ошибка при выключении: {error_msg[:100]}..." + ) + return False + +async def handle_reboot(chat_id: int, context: ContextTypes.DEFAULT_TYPE) -> bool: + """Асинхронная функция для перезагрузки NAS""" + try: + # Отправка запроса на перезагрузку + success = synology_api.reboot_system() + + if success: + await context.bot.send_message( + chat_id=chat_id, + text="✅ Команда перезагрузки успешно отправлена. Synology NAS перезагружается..." + ) + + # Ждем некоторое время перед проверкой статуса + await context.bot.send_message( + chat_id=chat_id, + text="⏳ Ожидание перезагрузки системы..." + ) + + # Создаем задачу для ожидания загрузки + wait_successful = synology_api.wait_for_boot() + + if wait_successful: + await context.bot.send_message( + chat_id=chat_id, + text="✅ Synology NAS успешно перезагружен и снова онлайн" + ) + else: + await context.bot.send_message( + chat_id=chat_id, + text="⚠️ Превышено время ожидания перезагрузки. Проверьте статус вручную." + ) + + return True + else: + return False + except Exception as e: + logger.error(f"Error during reboot: {str(e)}") + return False diff --git a/.history/src/handlers/command_handlers_20250830083502.py b/.history/src/handlers/command_handlers_20250830083502.py new file mode 100644 index 0000000..3722e31 --- /dev/null +++ b/.history/src/handlers/command_handlers_20250830083502.py @@ -0,0 +1,385 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Обработчики команд для телеграм-бота +""" + +import logging +import socket +from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup +from telegram.ext import ContextTypes + +from src.config.config import ( + ADMIN_USER_IDS, SYNOLOGY_HOST, SYNOLOGY_PORT, SYNOLOGY_MAC +) +from src.api.synology import SynologyAPI + +logger = logging.getLogger(__name__) + +# Инициализация API Synology +synology_api = SynologyAPI() + +async def start_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /start""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + logger.warning(f"Unauthorized access attempt from user ID: {user_id}") + return + + await update.message.reply_text( + f"Привет, {update.effective_user.first_name}! 👋\n\n" + "Я бот для управления вашим Synology NAS.\n" + "Используйте следующие команды:\n\n" + "Основные команды:\n" + "/status - Проверка статуса NAS\n" + "/power - Управление питанием NAS\n" + "/system - Информация о системе\n" + "/storage - Информация о хранилище\n\n" + "Используйте /help для получения полного списка команд", + parse_mode="HTML" + ) + +async def help_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /help""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + await update.message.reply_text( + "📖 Помощь по использованию бота\n\n" + "Основные команды:\n" + "/start - Начало работы с ботом\n" + "/status - Проверка текущего статуса NAS\n" + "/power - Управление питанием NAS\n" + "/help - Вывод этой справки\n\n" + "Расширенные команды:\n" + "/system - Подробная информация о системе\n" + "/storage - Информация о хранилище и дисках\n" + "/shares - Список общих папок\n" + "/load - Текущая нагрузка на систему\n" + "/security - Статус безопасности системы\n\n" + "Диагностика:\n" + "/checkapi - Проверка доступных API Synology\n\n" + "Управление питанием:\n" + "• Включение NAS: Wake-on-LAN\n" + "• Выключение NAS: Безопасное завершение работы\n" + "• Перезагрузка: Безопасная перезагрузка\n\n" + "Примечание: Для работы функции включения необходимо, " + "чтобы на NAS была настроена функция Wake-on-LAN.", + parse_mode="HTML" + ) + +async def status_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /status""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + message = await update.message.reply_text("⏳ Проверяю статус Synology NAS...") + + is_online = synology_api.is_online() + + if is_online: + try: + # Если NAS включен, попробуем получить дополнительную информацию + system_info = synology_api.get_system_status() + + if system_info and system_info.get("status") != "error": + model = system_info.get("model", "Неизвестная модель") + version = system_info.get("version_string", system_info.get("version", "Неизвестная версия")) + uptime_seconds = system_info.get("up_time", system_info.get("uptime", 0)) + + # Преобразование времени работы в удобочитаемый формат + days, remainder = divmod(int(uptime_seconds), 86400) + hours, remainder = divmod(remainder, 3600) + minutes, seconds = divmod(remainder, 60) + uptime_str = f"{days}д {hours}ч {minutes}м {seconds}с" + + await message.edit_text( + f"✅ Synology NAS онлайн\n\n" + f"Модель: {model}\n" + f"Версия DSM: {version}\n" + f"Время работы: {uptime_str}", + parse_mode="HTML" + ) + else: + # Обработка возможной ошибки API + error_info = "" + if system_info and system_info.get("status") == "error": + error_code = system_info.get("error_code", "неизвестно") + error_info = f"\nКод ошибки API: {error_code}" + + # Проверяем порт и сеть + network_info = "" + try: + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + s.settimeout(1) + result = s.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT)) + s.close() + if result == 0: + network_info = f"\n\nСетевая информация:\nIP: {SYNOLOGY_HOST}\nПорт {SYNOLOGY_PORT}: открыт" + else: + network_info = f"\n\nСетевая информация:\nIP: {SYNOLOGY_HOST}\nПорт {SYNOLOGY_PORT}: закрыт (код {result})" + except Exception as e: + network_info = f"\n\nСетевая информация:\nIP: {SYNOLOGY_HOST}\nОшибка проверки порта: {str(e)}" + + await message.edit_text( + f"✅ Synology NAS онлайн\n\n" + f"Устройство доступно по сети, но детальная информация через API недоступна. " + f"Возможно, необходимо проверить учетные данные или права доступа." + f"{error_info}" + f"{network_info}", + parse_mode="HTML" + ) + except Exception as e: + await message.edit_text( + f"✅ Synology NAS онлайн\n\n" + f"Ошибка при получении информации: {str(e)[:100]}...\n\n" + f"Сетевая информация:\nIP: {SYNOLOGY_HOST}\nПорт: {SYNOLOGY_PORT}", + parse_mode="HTML" + ) + else: + # Устройство не в сети, проверим соседние порты для диагностики + port_scan_info = "" + try: + for test_port in [SYNOLOGY_PORT-1, SYNOLOGY_PORT, SYNOLOGY_PORT+1, 5000, 5001]: + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + s.settimeout(1) + result = s.connect_ex((SYNOLOGY_HOST, test_port)) + s.close() + status = "открыт" if result == 0 else "закрыт" + port_scan_info += f"Порт {test_port}: {status}\n" + + # Добавим информацию о MAC-адресе для WoL + mac_info = f"MAC: {SYNOLOGY_MAC}" if SYNOLOGY_MAC else "MAC-адрес не настроен" + + await message.edit_text( + f"❌ Synology NAS оффлайн\n\n" + f"Информация о сети:\n" + f"IP: {SYNOLOGY_HOST}\n" + f"{port_scan_info}\n" + f"{mac_info}\n\n" + f"Используйте /power для отправки Wake-on-LAN пакета", + parse_mode="HTML" + ) + except Exception as e: + await message.edit_text( + f"❌ Synology NAS оффлайн\n\n" + f"Ошибка при сканировании портов: {str(e)[:100]}...\n\n" + f"Используйте /power для отправки Wake-on-LAN пакета", + parse_mode="HTML" + ) + +async def power_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /power""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + is_online = synology_api.is_online() + + keyboard = [] + + # Кнопка включения + if not is_online: + keyboard.append([InlineKeyboardButton("🟢 Включить", callback_data="power_on")]) + else: + keyboard.append([InlineKeyboardButton("🟢 Включить (уже включено)", callback_data="power_on_no_op")]) + + # Кнопка выключения + if is_online: + keyboard.append([InlineKeyboardButton("🔴 Выключить", callback_data="power_off")]) + else: + keyboard.append([InlineKeyboardButton("🔴 Выключить (уже выключено)", callback_data="power_off_no_op")]) + + # Кнопка перезагрузки + if is_online: + keyboard.append([InlineKeyboardButton("🔄 Перезагрузить", callback_data="reboot")]) + else: + keyboard.append([InlineKeyboardButton("🔄 Перезагрузить (недоступно)", callback_data="reboot_no_op")]) + + # Кнопка отмены + keyboard.append([InlineKeyboardButton("❌ Отмена", callback_data="cancel")]) + + reply_markup = InlineKeyboardMarkup(keyboard) + + status_text = "✅ Онлайн" if is_online else "❌ Оффлайн" + + await update.message.reply_text( + f"Управление питанием Synology NAS\n\n" + f"Текущий статус: {status_text}\n\n" + f"Выберите действие:", + reply_markup=reply_markup, + parse_mode="HTML" + ) + +async def power_callback(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик callback-запросов для кнопок управления питанием""" + query = update.callback_query + await query.answer() + + user_id = query.from_user.id + if user_id not in ADMIN_USER_IDS: + return + + action = query.data + + if action == "cancel": + await query.edit_message_text("❌ Действие отменено") + return + + # Обработка неактивных кнопок + if action in ["power_on_no_op", "power_off_no_op", "reboot_no_op"]: + if action == "power_on_no_op": + await query.edit_message_text("ℹ️ Synology NAS уже включен") + elif action == "power_off_no_op": + await query.edit_message_text("ℹ️ Synology NAS уже выключен") + else: + await query.edit_message_text("ℹ️ Невозможно перезагрузить выключенное устройство") + return + + # Обработка основных действий + if action == "power_on": + await query.edit_message_text("⏳ Отправка сигнала включения Synology NAS...") + + if await context.application.create_task( + handle_power_on(query.message.chat_id, context) + ): + # Функция вернула True, успешное включение + pass + else: + # Функция вернула False, ошибка включения + await context.bot.send_message( + chat_id=query.message.chat_id, + text="❌ Ошибка при включении Synology NAS. Проверьте настройки Wake-on-LAN." + ) + + elif action == "power_off": + await query.edit_message_text("⏳ Отправка команды выключения Synology NAS...") + + try: + success = await handle_power_off(query.message.chat_id, context) + # Если handle_power_off уже отправил сообщение об успехе или ошибке, + # дополнительных сообщений не требуется + except Exception as e: + logger.error(f"Exception in power_off callback: {str(e)}") + await context.bot.send_message( + chat_id=query.message.chat_id, + text="❌ Ошибка при выключении Synology NAS. Проверьте соединение и учетные данные." + ) + + elif action == "reboot": + await query.edit_message_text("⏳ Отправка команды перезагрузки Synology NAS...") + + if await context.application.create_task( + handle_reboot(query.message.chat_id, context) + ): + # Функция вернула True, успешная перезагрузка + pass + else: + # Функция вернула False, ошибка перезагрузки + await context.bot.send_message( + chat_id=query.message.chat_id, + text="❌ Ошибка при перезагрузке Synology NAS. Проверьте соединение и учетные данные." + ) + +async def handle_power_on(chat_id: int, context: ContextTypes.DEFAULT_TYPE) -> bool: + """Асинхронная функция для включения NAS""" + try: + # Отправка запроса на включение + success = synology_api.power_on() + + if success: + await context.bot.send_message( + chat_id=chat_id, + text="✅ Synology NAS успешно включен и доступен" + ) + return True + else: + return False + except Exception as e: + logger.error(f"Error during power on: {str(e)}") + return False + +async def handle_power_off(chat_id: int, context: ContextTypes.DEFAULT_TYPE) -> bool: + """Асинхронная функция для выключения NAS""" + try: + # Проверка доступности NAS + if not synology_api.is_online(): + await context.bot.send_message( + chat_id=chat_id, + text="⚠️ Synology NAS не в сети. Невозможно отправить команду выключения." + ) + return False + + # Отправка запроса на выключение + success = synology_api.power_off() + + if success: + await context.bot.send_message( + chat_id=chat_id, + text="✅ Команда выключения успешно отправлена. Synology NAS выключается..." + ) + return True + else: + await context.bot.send_message( + chat_id=chat_id, + text="⚠️ Ошибка при отправке команды выключения. Пожалуйста, проверьте журналы для получения дополнительной информации." + ) + return False + except Exception as e: + error_msg = str(e) + logger.error(f"Error during power off: {error_msg}") + await context.bot.send_message( + chat_id=chat_id, + text=f"❌ Ошибка при выключении: {error_msg[:100]}..." + ) + return False + +async def handle_reboot(chat_id: int, context: ContextTypes.DEFAULT_TYPE) -> bool: + """Асинхронная функция для перезагрузки NAS""" + try: + # Отправка запроса на перезагрузку + success = synology_api.reboot_system() + + if success: + await context.bot.send_message( + chat_id=chat_id, + text="✅ Команда перезагрузки успешно отправлена. Synology NAS перезагружается..." + ) + + # Ждем некоторое время перед проверкой статуса + await context.bot.send_message( + chat_id=chat_id, + text="⏳ Ожидание перезагрузки системы..." + ) + + # Создаем задачу для ожидания загрузки + wait_successful = synology_api.wait_for_boot() + + if wait_successful: + await context.bot.send_message( + chat_id=chat_id, + text="✅ Synology NAS успешно перезагружен и снова онлайн" + ) + else: + await context.bot.send_message( + chat_id=chat_id, + text="⚠️ Превышено время ожидания перезагрузки. Проверьте статус вручную." + ) + + return True + else: + return False + except Exception as e: + logger.error(f"Error during reboot: {str(e)}") + return False diff --git a/.history/src/handlers/command_handlers_20250830092806.py b/.history/src/handlers/command_handlers_20250830092806.py new file mode 100644 index 0000000..1b50d8e --- /dev/null +++ b/.history/src/handlers/command_handlers_20250830092806.py @@ -0,0 +1,331 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Обработчики команд для телеграм-бота +""" + +import logging +import socket +from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup +from telegram.ext import ContextTypes + +from src.config.config import ( + ADMIN_USER_IDS, SYNOLOGY_HOST, SYNOLOGY_PORT, SYNOLOGY_MAC +) +from src.api.synology import SynologyAPI + +logger = logging.getLogger(__name__) + +# Инициализация API Synology +synology_api = SynologyAPI() + +async def status_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /status""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + message = await update.message.reply_text("⏳ Проверяю статус Synology NAS...") + + is_online = synology_api.is_online() + + if is_online: + try: + # Если NAS включен, попробуем получить дополнительную информацию + system_info = synology_api.get_system_status() + + if system_info and system_info.get("status") != "error": + model = system_info.get("model", "Неизвестная модель") + version = system_info.get("version_string", system_info.get("version", "Неизвестная версия")) + uptime_seconds = system_info.get("up_time", system_info.get("uptime", 0)) + + # Преобразование времени работы в удобочитаемый формат + days, remainder = divmod(int(uptime_seconds), 86400) + hours, remainder = divmod(remainder, 3600) + minutes, seconds = divmod(remainder, 60) + uptime_str = f"{days}д {hours}ч {minutes}м {seconds}с" + + await message.edit_text( + f"✅ Synology NAS онлайн\n\n" + f"Модель: {model}\n" + f"Версия DSM: {version}\n" + f"Время работы: {uptime_str}", + parse_mode="HTML" + ) + else: + # Обработка возможной ошибки API + error_info = "" + if system_info and system_info.get("status") == "error": + error_code = system_info.get("error_code", "неизвестно") + error_info = f"\nКод ошибки API: {error_code}" + + # Проверяем порт и сеть + network_info = "" + try: + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + s.settimeout(1) + result = s.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT)) + s.close() + if result == 0: + network_info = f"\n\nСетевая информация:\nIP: {SYNOLOGY_HOST}\nПорт {SYNOLOGY_PORT}: открыт" + else: + network_info = f"\n\nСетевая информация:\nIP: {SYNOLOGY_HOST}\nПорт {SYNOLOGY_PORT}: закрыт (код {result})" + except Exception as e: + network_info = f"\n\nСетевая информация:\nIP: {SYNOLOGY_HOST}\nОшибка проверки порта: {str(e)}" + + await message.edit_text( + f"✅ Synology NAS онлайн\n\n" + f"Устройство доступно по сети, но детальная информация через API недоступна. " + f"Возможно, необходимо проверить учетные данные или права доступа." + f"{error_info}" + f"{network_info}", + parse_mode="HTML" + ) + except Exception as e: + await message.edit_text( + f"✅ Synology NAS онлайн\n\n" + f"Ошибка при получении информации: {str(e)[:100]}...\n\n" + f"Сетевая информация:\nIP: {SYNOLOGY_HOST}\nПорт: {SYNOLOGY_PORT}", + parse_mode="HTML" + ) + else: + # Устройство не в сети, проверим соседние порты для диагностики + port_scan_info = "" + try: + for test_port in [SYNOLOGY_PORT-1, SYNOLOGY_PORT, SYNOLOGY_PORT+1, 5000, 5001]: + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + s.settimeout(1) + result = s.connect_ex((SYNOLOGY_HOST, test_port)) + s.close() + status = "открыт" if result == 0 else "закрыт" + port_scan_info += f"Порт {test_port}: {status}\n" + + # Добавим информацию о MAC-адресе для WoL + mac_info = f"MAC: {SYNOLOGY_MAC}" if SYNOLOGY_MAC else "MAC-адрес не настроен" + + await message.edit_text( + f"❌ Synology NAS оффлайн\n\n" + f"Информация о сети:\n" + f"IP: {SYNOLOGY_HOST}\n" + f"{port_scan_info}\n" + f"{mac_info}\n\n" + f"Используйте /power для отправки Wake-on-LAN пакета", + parse_mode="HTML" + ) + except Exception as e: + await message.edit_text( + f"❌ Synology NAS оффлайн\n\n" + f"Ошибка при сканировании портов: {str(e)[:100]}...\n\n" + f"Используйте /power для отправки Wake-on-LAN пакета", + parse_mode="HTML" + ) + +async def power_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /power""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + is_online = synology_api.is_online() + + keyboard = [] + + # Кнопка включения + if not is_online: + keyboard.append([InlineKeyboardButton("🟢 Включить", callback_data="power_on")]) + else: + keyboard.append([InlineKeyboardButton("🟢 Включить (уже включено)", callback_data="power_on_no_op")]) + + # Кнопка выключения + if is_online: + keyboard.append([InlineKeyboardButton("🔴 Выключить", callback_data="power_off")]) + else: + keyboard.append([InlineKeyboardButton("🔴 Выключить (уже выключено)", callback_data="power_off_no_op")]) + + # Кнопка перезагрузки + if is_online: + keyboard.append([InlineKeyboardButton("🔄 Перезагрузить", callback_data="reboot")]) + else: + keyboard.append([InlineKeyboardButton("🔄 Перезагрузить (недоступно)", callback_data="reboot_no_op")]) + + # Кнопка отмены + keyboard.append([InlineKeyboardButton("❌ Отмена", callback_data="cancel")]) + + reply_markup = InlineKeyboardMarkup(keyboard) + + status_text = "✅ Онлайн" if is_online else "❌ Оффлайн" + + await update.message.reply_text( + f"Управление питанием Synology NAS\n\n" + f"Текущий статус: {status_text}\n\n" + f"Выберите действие:", + reply_markup=reply_markup, + parse_mode="HTML" + ) + +async def power_callback(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик callback-запросов для кнопок управления питанием""" + query = update.callback_query + await query.answer() + + user_id = query.from_user.id + if user_id not in ADMIN_USER_IDS: + return + + action = query.data + + if action == "cancel": + await query.edit_message_text("❌ Действие отменено") + return + + # Обработка неактивных кнопок + if action in ["power_on_no_op", "power_off_no_op", "reboot_no_op"]: + if action == "power_on_no_op": + await query.edit_message_text("ℹ️ Synology NAS уже включен") + elif action == "power_off_no_op": + await query.edit_message_text("ℹ️ Synology NAS уже выключен") + else: + await query.edit_message_text("ℹ️ Невозможно перезагрузить выключенное устройство") + return + + # Обработка основных действий + if action == "power_on": + await query.edit_message_text("⏳ Отправка сигнала включения Synology NAS...") + + if await context.application.create_task( + handle_power_on(query.message.chat_id, context) + ): + # Функция вернула True, успешное включение + pass + else: + # Функция вернула False, ошибка включения + await context.bot.send_message( + chat_id=query.message.chat_id, + text="❌ Ошибка при включении Synology NAS. Проверьте настройки Wake-on-LAN." + ) + + elif action == "power_off": + await query.edit_message_text("⏳ Отправка команды выключения Synology NAS...") + + try: + success = await handle_power_off(query.message.chat_id, context) + # Если handle_power_off уже отправил сообщение об успехе или ошибке, + # дополнительных сообщений не требуется + except Exception as e: + logger.error(f"Exception in power_off callback: {str(e)}") + await context.bot.send_message( + chat_id=query.message.chat_id, + text="❌ Ошибка при выключении Synology NAS. Проверьте соединение и учетные данные." + ) + + elif action == "reboot": + await query.edit_message_text("⏳ Отправка команды перезагрузки Synology NAS...") + + if await context.application.create_task( + handle_reboot(query.message.chat_id, context) + ): + # Функция вернула True, успешная перезагрузка + pass + else: + # Функция вернула False, ошибка перезагрузки + await context.bot.send_message( + chat_id=query.message.chat_id, + text="❌ Ошибка при перезагрузке Synology NAS. Проверьте соединение и учетные данные." + ) + +async def handle_power_on(chat_id: int, context: ContextTypes.DEFAULT_TYPE) -> bool: + """Асинхронная функция для включения NAS""" + try: + # Отправка запроса на включение + success = synology_api.power_on() + + if success: + await context.bot.send_message( + chat_id=chat_id, + text="✅ Synology NAS успешно включен и доступен" + ) + return True + else: + return False + except Exception as e: + logger.error(f"Error during power on: {str(e)}") + return False + +async def handle_power_off(chat_id: int, context: ContextTypes.DEFAULT_TYPE) -> bool: + """Асинхронная функция для выключения NAS""" + try: + # Проверка доступности NAS + if not synology_api.is_online(): + await context.bot.send_message( + chat_id=chat_id, + text="⚠️ Synology NAS не в сети. Невозможно отправить команду выключения." + ) + return False + + # Отправка запроса на выключение + success = synology_api.power_off() + + if success: + await context.bot.send_message( + chat_id=chat_id, + text="✅ Команда выключения успешно отправлена. Synology NAS выключается..." + ) + return True + else: + await context.bot.send_message( + chat_id=chat_id, + text="⚠️ Ошибка при отправке команды выключения. Пожалуйста, проверьте журналы для получения дополнительной информации." + ) + return False + except Exception as e: + error_msg = str(e) + logger.error(f"Error during power off: {error_msg}") + await context.bot.send_message( + chat_id=chat_id, + text=f"❌ Ошибка при выключении: {error_msg[:100]}..." + ) + return False + +async def handle_reboot(chat_id: int, context: ContextTypes.DEFAULT_TYPE) -> bool: + """Асинхронная функция для перезагрузки NAS""" + try: + # Отправка запроса на перезагрузку + success = synology_api.reboot_system() + + if success: + await context.bot.send_message( + chat_id=chat_id, + text="✅ Команда перезагрузки успешно отправлена. Synology NAS перезагружается..." + ) + + # Ждем некоторое время перед проверкой статуса + await context.bot.send_message( + chat_id=chat_id, + text="⏳ Ожидание перезагрузки системы..." + ) + + # Создаем задачу для ожидания загрузки + wait_successful = synology_api.wait_for_boot() + + if wait_successful: + await context.bot.send_message( + chat_id=chat_id, + text="✅ Synology NAS успешно перезагружен и снова онлайн" + ) + else: + await context.bot.send_message( + chat_id=chat_id, + text="⚠️ Превышено время ожидания перезагрузки. Проверьте статус вручную." + ) + + return True + else: + return False + except Exception as e: + logger.error(f"Error during reboot: {str(e)}") + return False diff --git a/.history/src/handlers/command_handlers_20250830094738.py b/.history/src/handlers/command_handlers_20250830094738.py new file mode 100644 index 0000000..1b50d8e --- /dev/null +++ b/.history/src/handlers/command_handlers_20250830094738.py @@ -0,0 +1,331 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Обработчики команд для телеграм-бота +""" + +import logging +import socket +from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup +from telegram.ext import ContextTypes + +from src.config.config import ( + ADMIN_USER_IDS, SYNOLOGY_HOST, SYNOLOGY_PORT, SYNOLOGY_MAC +) +from src.api.synology import SynologyAPI + +logger = logging.getLogger(__name__) + +# Инициализация API Synology +synology_api = SynologyAPI() + +async def status_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /status""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + message = await update.message.reply_text("⏳ Проверяю статус Synology NAS...") + + is_online = synology_api.is_online() + + if is_online: + try: + # Если NAS включен, попробуем получить дополнительную информацию + system_info = synology_api.get_system_status() + + if system_info and system_info.get("status") != "error": + model = system_info.get("model", "Неизвестная модель") + version = system_info.get("version_string", system_info.get("version", "Неизвестная версия")) + uptime_seconds = system_info.get("up_time", system_info.get("uptime", 0)) + + # Преобразование времени работы в удобочитаемый формат + days, remainder = divmod(int(uptime_seconds), 86400) + hours, remainder = divmod(remainder, 3600) + minutes, seconds = divmod(remainder, 60) + uptime_str = f"{days}д {hours}ч {minutes}м {seconds}с" + + await message.edit_text( + f"✅ Synology NAS онлайн\n\n" + f"Модель: {model}\n" + f"Версия DSM: {version}\n" + f"Время работы: {uptime_str}", + parse_mode="HTML" + ) + else: + # Обработка возможной ошибки API + error_info = "" + if system_info and system_info.get("status") == "error": + error_code = system_info.get("error_code", "неизвестно") + error_info = f"\nКод ошибки API: {error_code}" + + # Проверяем порт и сеть + network_info = "" + try: + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + s.settimeout(1) + result = s.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT)) + s.close() + if result == 0: + network_info = f"\n\nСетевая информация:\nIP: {SYNOLOGY_HOST}\nПорт {SYNOLOGY_PORT}: открыт" + else: + network_info = f"\n\nСетевая информация:\nIP: {SYNOLOGY_HOST}\nПорт {SYNOLOGY_PORT}: закрыт (код {result})" + except Exception as e: + network_info = f"\n\nСетевая информация:\nIP: {SYNOLOGY_HOST}\nОшибка проверки порта: {str(e)}" + + await message.edit_text( + f"✅ Synology NAS онлайн\n\n" + f"Устройство доступно по сети, но детальная информация через API недоступна. " + f"Возможно, необходимо проверить учетные данные или права доступа." + f"{error_info}" + f"{network_info}", + parse_mode="HTML" + ) + except Exception as e: + await message.edit_text( + f"✅ Synology NAS онлайн\n\n" + f"Ошибка при получении информации: {str(e)[:100]}...\n\n" + f"Сетевая информация:\nIP: {SYNOLOGY_HOST}\nПорт: {SYNOLOGY_PORT}", + parse_mode="HTML" + ) + else: + # Устройство не в сети, проверим соседние порты для диагностики + port_scan_info = "" + try: + for test_port in [SYNOLOGY_PORT-1, SYNOLOGY_PORT, SYNOLOGY_PORT+1, 5000, 5001]: + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + s.settimeout(1) + result = s.connect_ex((SYNOLOGY_HOST, test_port)) + s.close() + status = "открыт" if result == 0 else "закрыт" + port_scan_info += f"Порт {test_port}: {status}\n" + + # Добавим информацию о MAC-адресе для WoL + mac_info = f"MAC: {SYNOLOGY_MAC}" if SYNOLOGY_MAC else "MAC-адрес не настроен" + + await message.edit_text( + f"❌ Synology NAS оффлайн\n\n" + f"Информация о сети:\n" + f"IP: {SYNOLOGY_HOST}\n" + f"{port_scan_info}\n" + f"{mac_info}\n\n" + f"Используйте /power для отправки Wake-on-LAN пакета", + parse_mode="HTML" + ) + except Exception as e: + await message.edit_text( + f"❌ Synology NAS оффлайн\n\n" + f"Ошибка при сканировании портов: {str(e)[:100]}...\n\n" + f"Используйте /power для отправки Wake-on-LAN пакета", + parse_mode="HTML" + ) + +async def power_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /power""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + is_online = synology_api.is_online() + + keyboard = [] + + # Кнопка включения + if not is_online: + keyboard.append([InlineKeyboardButton("🟢 Включить", callback_data="power_on")]) + else: + keyboard.append([InlineKeyboardButton("🟢 Включить (уже включено)", callback_data="power_on_no_op")]) + + # Кнопка выключения + if is_online: + keyboard.append([InlineKeyboardButton("🔴 Выключить", callback_data="power_off")]) + else: + keyboard.append([InlineKeyboardButton("🔴 Выключить (уже выключено)", callback_data="power_off_no_op")]) + + # Кнопка перезагрузки + if is_online: + keyboard.append([InlineKeyboardButton("🔄 Перезагрузить", callback_data="reboot")]) + else: + keyboard.append([InlineKeyboardButton("🔄 Перезагрузить (недоступно)", callback_data="reboot_no_op")]) + + # Кнопка отмены + keyboard.append([InlineKeyboardButton("❌ Отмена", callback_data="cancel")]) + + reply_markup = InlineKeyboardMarkup(keyboard) + + status_text = "✅ Онлайн" if is_online else "❌ Оффлайн" + + await update.message.reply_text( + f"Управление питанием Synology NAS\n\n" + f"Текущий статус: {status_text}\n\n" + f"Выберите действие:", + reply_markup=reply_markup, + parse_mode="HTML" + ) + +async def power_callback(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик callback-запросов для кнопок управления питанием""" + query = update.callback_query + await query.answer() + + user_id = query.from_user.id + if user_id not in ADMIN_USER_IDS: + return + + action = query.data + + if action == "cancel": + await query.edit_message_text("❌ Действие отменено") + return + + # Обработка неактивных кнопок + if action in ["power_on_no_op", "power_off_no_op", "reboot_no_op"]: + if action == "power_on_no_op": + await query.edit_message_text("ℹ️ Synology NAS уже включен") + elif action == "power_off_no_op": + await query.edit_message_text("ℹ️ Synology NAS уже выключен") + else: + await query.edit_message_text("ℹ️ Невозможно перезагрузить выключенное устройство") + return + + # Обработка основных действий + if action == "power_on": + await query.edit_message_text("⏳ Отправка сигнала включения Synology NAS...") + + if await context.application.create_task( + handle_power_on(query.message.chat_id, context) + ): + # Функция вернула True, успешное включение + pass + else: + # Функция вернула False, ошибка включения + await context.bot.send_message( + chat_id=query.message.chat_id, + text="❌ Ошибка при включении Synology NAS. Проверьте настройки Wake-on-LAN." + ) + + elif action == "power_off": + await query.edit_message_text("⏳ Отправка команды выключения Synology NAS...") + + try: + success = await handle_power_off(query.message.chat_id, context) + # Если handle_power_off уже отправил сообщение об успехе или ошибке, + # дополнительных сообщений не требуется + except Exception as e: + logger.error(f"Exception in power_off callback: {str(e)}") + await context.bot.send_message( + chat_id=query.message.chat_id, + text="❌ Ошибка при выключении Synology NAS. Проверьте соединение и учетные данные." + ) + + elif action == "reboot": + await query.edit_message_text("⏳ Отправка команды перезагрузки Synology NAS...") + + if await context.application.create_task( + handle_reboot(query.message.chat_id, context) + ): + # Функция вернула True, успешная перезагрузка + pass + else: + # Функция вернула False, ошибка перезагрузки + await context.bot.send_message( + chat_id=query.message.chat_id, + text="❌ Ошибка при перезагрузке Synology NAS. Проверьте соединение и учетные данные." + ) + +async def handle_power_on(chat_id: int, context: ContextTypes.DEFAULT_TYPE) -> bool: + """Асинхронная функция для включения NAS""" + try: + # Отправка запроса на включение + success = synology_api.power_on() + + if success: + await context.bot.send_message( + chat_id=chat_id, + text="✅ Synology NAS успешно включен и доступен" + ) + return True + else: + return False + except Exception as e: + logger.error(f"Error during power on: {str(e)}") + return False + +async def handle_power_off(chat_id: int, context: ContextTypes.DEFAULT_TYPE) -> bool: + """Асинхронная функция для выключения NAS""" + try: + # Проверка доступности NAS + if not synology_api.is_online(): + await context.bot.send_message( + chat_id=chat_id, + text="⚠️ Synology NAS не в сети. Невозможно отправить команду выключения." + ) + return False + + # Отправка запроса на выключение + success = synology_api.power_off() + + if success: + await context.bot.send_message( + chat_id=chat_id, + text="✅ Команда выключения успешно отправлена. Synology NAS выключается..." + ) + return True + else: + await context.bot.send_message( + chat_id=chat_id, + text="⚠️ Ошибка при отправке команды выключения. Пожалуйста, проверьте журналы для получения дополнительной информации." + ) + return False + except Exception as e: + error_msg = str(e) + logger.error(f"Error during power off: {error_msg}") + await context.bot.send_message( + chat_id=chat_id, + text=f"❌ Ошибка при выключении: {error_msg[:100]}..." + ) + return False + +async def handle_reboot(chat_id: int, context: ContextTypes.DEFAULT_TYPE) -> bool: + """Асинхронная функция для перезагрузки NAS""" + try: + # Отправка запроса на перезагрузку + success = synology_api.reboot_system() + + if success: + await context.bot.send_message( + chat_id=chat_id, + text="✅ Команда перезагрузки успешно отправлена. Synology NAS перезагружается..." + ) + + # Ждем некоторое время перед проверкой статуса + await context.bot.send_message( + chat_id=chat_id, + text="⏳ Ожидание перезагрузки системы..." + ) + + # Создаем задачу для ожидания загрузки + wait_successful = synology_api.wait_for_boot() + + if wait_successful: + await context.bot.send_message( + chat_id=chat_id, + text="✅ Synology NAS успешно перезагружен и снова онлайн" + ) + else: + await context.bot.send_message( + chat_id=chat_id, + text="⚠️ Превышено время ожидания перезагрузки. Проверьте статус вручную." + ) + + return True + else: + return False + except Exception as e: + logger.error(f"Error during reboot: {str(e)}") + return False diff --git a/.history/src/handlers/extended_handlers_20250830065246.py b/.history/src/handlers/extended_handlers_20250830065246.py new file mode 100644 index 0000000..e7d707f --- /dev/null +++ b/.history/src/handlers/extended_handlers_20250830065246.py @@ -0,0 +1,255 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Дополнительные обработчики команд для телеграм-бота +""" + +import logging +from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup +from telegram.ext import ContextTypes + +from src.config.config import ADMIN_USER_IDS +from src.api.synology import SynologyAPI + +logger = logging.getLogger(__name__) + +# Инициализация API Synology +synology_api = SynologyAPI() + +async def storage_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /storage""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + message = await update.message.reply_text("⏳ Получение информации о хранилище...") + + if not synology_api.is_online(): + await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о хранилище.", parse_mode="HTML") + return + + storage_info = synology_api.get_storage_status() + + if not storage_info: + await message.edit_text("❌ Ошибка получения информации о хранилище", parse_mode="HTML") + return + + # Формируем сообщение о состоянии хранилища + total_size_gb = storage_info.get("total_size", 0) / (1024**3) + total_used_gb = storage_info.get("total_used", 0) / (1024**3) + usage_percent = (total_used_gb / total_size_gb * 100) if total_size_gb > 0 else 0 + + reply_text = f"📊 Информация о хранилище Synology NAS\n\n" + reply_text += f"Общий размер: {total_size_gb:.2f} ГБ\n" + reply_text += f"Использовано: {total_used_gb:.2f} ГБ ({usage_percent:.1f}%)\n" + reply_text += f"Свободно: {total_size_gb - total_used_gb:.2f} ГБ\n\n" + + # Добавляем информацию о томах + volumes = storage_info.get("volumes", []) + if volumes: + reply_text += "Тома:\n" + for volume in volumes: + name = volume.get("name", "Неизвестно") + status = volume.get("status", "Неизвестно") + size_gb = volume.get("total_size", 0) / (1024**3) + used_gb = volume.get("used_size", 0) / (1024**3) + percent = volume.get("percent_used", 0) + + reply_text += f"• {name} ({status})\n" + reply_text += f" └ {used_gb:.1f} ГБ из {size_gb:.1f} ГБ ({percent}%)\n" + + # Добавляем информацию о дисках + disks = storage_info.get("disks", []) + if disks: + reply_text += "\nДиски:\n" + for disk in disks: + name = disk.get("name", "Неизвестно") + model = disk.get("model", "Неизвестно") + status = disk.get("status", "Неизвестно") + temp = disk.get("temp", "?") + + reply_text += f"• {name} - {model}\n" + reply_text += f" └ Статус: {status}, Температура: {temp}°C\n" + + await message.edit_text(reply_text, parse_mode="HTML") + +async def shares_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /shares""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + message = await update.message.reply_text("⏳ Получение информации об общих папках...") + + if not synology_api.is_online(): + await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию об общих папках.", parse_mode="HTML") + return + + shares = synology_api.get_shared_folders() + + if not shares: + await message.edit_text("❌ Ошибка получения информации об общих папках", parse_mode="HTML") + return + + # Формируем сообщение об общих папках + reply_text = f"📁 Общие папки Synology NAS\n\n" + + for share in shares: + name = share.get("name", "Неизвестно") + path = share.get("path", "Неизвестно") + desc = share.get("desc", "") + + reply_text += f"• {name}\n" + reply_text += f" └ Путь: {path}\n" + + if desc: + reply_text += f" └ Описание: {desc}\n" + + await message.edit_text(reply_text, parse_mode="HTML") + +async def system_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /system""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + message = await update.message.reply_text("⏳ Получение информации о системе...") + + if not synology_api.is_online(): + await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о системе.", parse_mode="HTML") + return + + system_status = synology_api.get_system_status() + + if not system_status: + await message.edit_text("❌ Ошибка получения информации о системе", parse_mode="HTML") + return + + # Формируем сообщение о состоянии системы + model = system_status.get("model", "Неизвестно") + version = system_status.get("version", "Неизвестно") + serial = system_status.get("serial", "Неизвестно") + uptime_seconds = system_status.get("uptime", 0) + temperature = system_status.get("temperature", "?") + + # Преобразование времени работы в удобочитаемый формат + days, remainder = divmod(uptime_seconds, 86400) + hours, remainder = divmod(remainder, 3600) + minutes, seconds = divmod(remainder, 60) + uptime_str = f"{days}д {hours}ч {minutes}м {seconds}с" + + reply_text = f"🖥️ Информация о системе Synology NAS\n\n" + reply_text += f"Модель: {model}\n" + reply_text += f"Серийный номер: {serial}\n" + reply_text += f"Версия DSM: {version}\n" + reply_text += f"Время работы: {uptime_str}\n" + reply_text += f"Температура: {temperature}°C\n\n" + + # Добавляем информацию о CPU и памяти + memory = system_status.get("memory", {}) + total_memory_gb = memory.get("total_mb", 0) / 1024 + available_memory_gb = memory.get("available_mb", 0) / 1024 + memory_usage = memory.get("usage_percent", 0) + cpu_usage = system_status.get("cpu_usage", 0) + + reply_text += f"Загрузка CPU: {cpu_usage}%\n" + reply_text += f"Память: {total_memory_gb:.1f} ГБ (используется {memory_usage}%)\n" + reply_text += f"Доступно памяти: {available_memory_gb:.1f} ГБ\n\n" + + # Добавляем информацию о сетевых интерфейсах + network_info = system_status.get("network", []) + if network_info: + reply_text += "Сетевые интерфейсы:\n" + for interface in network_info: + device = interface.get("device", "Неизвестно") + ip = interface.get("ip", "Неизвестно") + mac = interface.get("mac", "Неизвестно") + + reply_text += f"• {device}\n" + reply_text += f" └ IP: {ip}, MAC: {mac}\n" + + await message.edit_text(reply_text, parse_mode="HTML") + +async def load_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /load""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + message = await update.message.reply_text("⏳ Получение информации о нагрузке системы...") + + if not synology_api.is_online(): + await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о нагрузке системы.", parse_mode="HTML") + return + + system_load = synology_api.get_system_load() + + if not system_load: + await message.edit_text("❌ Ошибка получения информации о нагрузке системы", parse_mode="HTML") + return + + # Формируем сообщение о нагрузке системы + cpu_load = system_load.get("cpu_load", 0) + memory = system_load.get("memory", {}) + memory_usage = memory.get("usage_percent", 0) + + reply_text = f"📈 Текущая нагрузка Synology NAS\n\n" + reply_text += f"Загрузка CPU: {cpu_load}%\n" + reply_text += f"Загрузка памяти: {memory_usage}%\n\n" + + # Добавляем информацию о сетевой активности + network = system_load.get("network", {}) + if network: + reply_text += "Сетевая активность:\n" + for device, stats in network.items(): + rx = int(stats.get("rx_bytes", 0)) / (1024**2) # МБ + tx = int(stats.get("tx_bytes", 0)) / (1024**2) # МБ + + reply_text += f"• {device}\n" + reply_text += f" └ Получено: {rx:.1f} МБ, Отправлено: {tx:.1f} МБ\n" + + await message.edit_text(reply_text, parse_mode="HTML") + +async def security_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /security""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + message = await update.message.reply_text("⏳ Получение информации о безопасности...") + + if not synology_api.is_online(): + await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о безопасности.", parse_mode="HTML") + return + + security_info = synology_api.get_security_status() + + if not security_info.get("success", False): + await message.edit_text("❌ Ошибка получения информации о безопасности", parse_mode="HTML") + return + + # Формируем сообщение о безопасности + status = security_info.get("status", "unknown") + is_secure = security_info.get("is_secure", False) + last_check = security_info.get("last_check", "Неизвестно") + + status_emoji = "✅" if is_secure else "⚠️" + status_text = "Безопасно" if is_secure else "Требуется внимание" + + reply_text = f"🔐 Статус безопасности Synology NAS\n\n" + reply_text += f"Статус: {status_emoji} {status_text}\n" + reply_text += f"Подробности: {status}\n" + reply_text += f"Последняя проверка: {last_check}\n" + + await message.edit_text(reply_text, parse_mode="HTML") diff --git a/.history/src/handlers/extended_handlers_20250830065455.py b/.history/src/handlers/extended_handlers_20250830065455.py new file mode 100644 index 0000000..e7d707f --- /dev/null +++ b/.history/src/handlers/extended_handlers_20250830065455.py @@ -0,0 +1,255 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Дополнительные обработчики команд для телеграм-бота +""" + +import logging +from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup +from telegram.ext import ContextTypes + +from src.config.config import ADMIN_USER_IDS +from src.api.synology import SynologyAPI + +logger = logging.getLogger(__name__) + +# Инициализация API Synology +synology_api = SynologyAPI() + +async def storage_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /storage""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + message = await update.message.reply_text("⏳ Получение информации о хранилище...") + + if not synology_api.is_online(): + await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о хранилище.", parse_mode="HTML") + return + + storage_info = synology_api.get_storage_status() + + if not storage_info: + await message.edit_text("❌ Ошибка получения информации о хранилище", parse_mode="HTML") + return + + # Формируем сообщение о состоянии хранилища + total_size_gb = storage_info.get("total_size", 0) / (1024**3) + total_used_gb = storage_info.get("total_used", 0) / (1024**3) + usage_percent = (total_used_gb / total_size_gb * 100) if total_size_gb > 0 else 0 + + reply_text = f"📊 Информация о хранилище Synology NAS\n\n" + reply_text += f"Общий размер: {total_size_gb:.2f} ГБ\n" + reply_text += f"Использовано: {total_used_gb:.2f} ГБ ({usage_percent:.1f}%)\n" + reply_text += f"Свободно: {total_size_gb - total_used_gb:.2f} ГБ\n\n" + + # Добавляем информацию о томах + volumes = storage_info.get("volumes", []) + if volumes: + reply_text += "Тома:\n" + for volume in volumes: + name = volume.get("name", "Неизвестно") + status = volume.get("status", "Неизвестно") + size_gb = volume.get("total_size", 0) / (1024**3) + used_gb = volume.get("used_size", 0) / (1024**3) + percent = volume.get("percent_used", 0) + + reply_text += f"• {name} ({status})\n" + reply_text += f" └ {used_gb:.1f} ГБ из {size_gb:.1f} ГБ ({percent}%)\n" + + # Добавляем информацию о дисках + disks = storage_info.get("disks", []) + if disks: + reply_text += "\nДиски:\n" + for disk in disks: + name = disk.get("name", "Неизвестно") + model = disk.get("model", "Неизвестно") + status = disk.get("status", "Неизвестно") + temp = disk.get("temp", "?") + + reply_text += f"• {name} - {model}\n" + reply_text += f" └ Статус: {status}, Температура: {temp}°C\n" + + await message.edit_text(reply_text, parse_mode="HTML") + +async def shares_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /shares""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + message = await update.message.reply_text("⏳ Получение информации об общих папках...") + + if not synology_api.is_online(): + await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию об общих папках.", parse_mode="HTML") + return + + shares = synology_api.get_shared_folders() + + if not shares: + await message.edit_text("❌ Ошибка получения информации об общих папках", parse_mode="HTML") + return + + # Формируем сообщение об общих папках + reply_text = f"📁 Общие папки Synology NAS\n\n" + + for share in shares: + name = share.get("name", "Неизвестно") + path = share.get("path", "Неизвестно") + desc = share.get("desc", "") + + reply_text += f"• {name}\n" + reply_text += f" └ Путь: {path}\n" + + if desc: + reply_text += f" └ Описание: {desc}\n" + + await message.edit_text(reply_text, parse_mode="HTML") + +async def system_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /system""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + message = await update.message.reply_text("⏳ Получение информации о системе...") + + if not synology_api.is_online(): + await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о системе.", parse_mode="HTML") + return + + system_status = synology_api.get_system_status() + + if not system_status: + await message.edit_text("❌ Ошибка получения информации о системе", parse_mode="HTML") + return + + # Формируем сообщение о состоянии системы + model = system_status.get("model", "Неизвестно") + version = system_status.get("version", "Неизвестно") + serial = system_status.get("serial", "Неизвестно") + uptime_seconds = system_status.get("uptime", 0) + temperature = system_status.get("temperature", "?") + + # Преобразование времени работы в удобочитаемый формат + days, remainder = divmod(uptime_seconds, 86400) + hours, remainder = divmod(remainder, 3600) + minutes, seconds = divmod(remainder, 60) + uptime_str = f"{days}д {hours}ч {minutes}м {seconds}с" + + reply_text = f"🖥️ Информация о системе Synology NAS\n\n" + reply_text += f"Модель: {model}\n" + reply_text += f"Серийный номер: {serial}\n" + reply_text += f"Версия DSM: {version}\n" + reply_text += f"Время работы: {uptime_str}\n" + reply_text += f"Температура: {temperature}°C\n\n" + + # Добавляем информацию о CPU и памяти + memory = system_status.get("memory", {}) + total_memory_gb = memory.get("total_mb", 0) / 1024 + available_memory_gb = memory.get("available_mb", 0) / 1024 + memory_usage = memory.get("usage_percent", 0) + cpu_usage = system_status.get("cpu_usage", 0) + + reply_text += f"Загрузка CPU: {cpu_usage}%\n" + reply_text += f"Память: {total_memory_gb:.1f} ГБ (используется {memory_usage}%)\n" + reply_text += f"Доступно памяти: {available_memory_gb:.1f} ГБ\n\n" + + # Добавляем информацию о сетевых интерфейсах + network_info = system_status.get("network", []) + if network_info: + reply_text += "Сетевые интерфейсы:\n" + for interface in network_info: + device = interface.get("device", "Неизвестно") + ip = interface.get("ip", "Неизвестно") + mac = interface.get("mac", "Неизвестно") + + reply_text += f"• {device}\n" + reply_text += f" └ IP: {ip}, MAC: {mac}\n" + + await message.edit_text(reply_text, parse_mode="HTML") + +async def load_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /load""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + message = await update.message.reply_text("⏳ Получение информации о нагрузке системы...") + + if not synology_api.is_online(): + await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о нагрузке системы.", parse_mode="HTML") + return + + system_load = synology_api.get_system_load() + + if not system_load: + await message.edit_text("❌ Ошибка получения информации о нагрузке системы", parse_mode="HTML") + return + + # Формируем сообщение о нагрузке системы + cpu_load = system_load.get("cpu_load", 0) + memory = system_load.get("memory", {}) + memory_usage = memory.get("usage_percent", 0) + + reply_text = f"📈 Текущая нагрузка Synology NAS\n\n" + reply_text += f"Загрузка CPU: {cpu_load}%\n" + reply_text += f"Загрузка памяти: {memory_usage}%\n\n" + + # Добавляем информацию о сетевой активности + network = system_load.get("network", {}) + if network: + reply_text += "Сетевая активность:\n" + for device, stats in network.items(): + rx = int(stats.get("rx_bytes", 0)) / (1024**2) # МБ + tx = int(stats.get("tx_bytes", 0)) / (1024**2) # МБ + + reply_text += f"• {device}\n" + reply_text += f" └ Получено: {rx:.1f} МБ, Отправлено: {tx:.1f} МБ\n" + + await message.edit_text(reply_text, parse_mode="HTML") + +async def security_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /security""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + message = await update.message.reply_text("⏳ Получение информации о безопасности...") + + if not synology_api.is_online(): + await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о безопасности.", parse_mode="HTML") + return + + security_info = synology_api.get_security_status() + + if not security_info.get("success", False): + await message.edit_text("❌ Ошибка получения информации о безопасности", parse_mode="HTML") + return + + # Формируем сообщение о безопасности + status = security_info.get("status", "unknown") + is_secure = security_info.get("is_secure", False) + last_check = security_info.get("last_check", "Неизвестно") + + status_emoji = "✅" if is_secure else "⚠️" + status_text = "Безопасно" if is_secure else "Требуется внимание" + + reply_text = f"🔐 Статус безопасности Synology NAS\n\n" + reply_text += f"Статус: {status_emoji} {status_text}\n" + reply_text += f"Подробности: {status}\n" + reply_text += f"Последняя проверка: {last_check}\n" + + await message.edit_text(reply_text, parse_mode="HTML") diff --git a/.history/src/handlers/extended_handlers_20250830073718.py b/.history/src/handlers/extended_handlers_20250830073718.py new file mode 100644 index 0000000..af0cd8d --- /dev/null +++ b/.history/src/handlers/extended_handlers_20250830073718.py @@ -0,0 +1,259 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Дополнительные обработчики команд для телеграм-бота +""" + +import logging +from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup +from telegram.ext import ContextTypes + +from src.config.config import ADMIN_USER_IDS +from src.api.synology import SynologyAPI + +logger = logging.getLogger(__name__) + +# Инициализация API Synology +synology_api = SynologyAPI() + +async def storage_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /storage""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + message = await update.message.reply_text("⏳ Получение информации о хранилище...") + + if not synology_api.is_online(): + await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о хранилище.", parse_mode="HTML") + return + + try: + storage_info = synology_api.get_storage_status() + + if not storage_info: + await message.edit_text("❌ Ошибка получения информации о хранилище\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML") + return + except Exception as e: + await message.edit_text(f"❌ Ошибка получения информации о хранилище\n\nПричина: {str(e)}", parse_mode="HTML") + return + + # Формируем сообщение о состоянии хранилища + total_size_gb = storage_info.get("total_size", 0) / (1024**3) + total_used_gb = storage_info.get("total_used", 0) / (1024**3) + usage_percent = (total_used_gb / total_size_gb * 100) if total_size_gb > 0 else 0 + + reply_text = f"📊 Информация о хранилище Synology NAS\n\n" + reply_text += f"Общий размер: {total_size_gb:.2f} ГБ\n" + reply_text += f"Использовано: {total_used_gb:.2f} ГБ ({usage_percent:.1f}%)\n" + reply_text += f"Свободно: {total_size_gb - total_used_gb:.2f} ГБ\n\n" + + # Добавляем информацию о томах + volumes = storage_info.get("volumes", []) + if volumes: + reply_text += "Тома:\n" + for volume in volumes: + name = volume.get("name", "Неизвестно") + status = volume.get("status", "Неизвестно") + size_gb = volume.get("total_size", 0) / (1024**3) + used_gb = volume.get("used_size", 0) / (1024**3) + percent = volume.get("percent_used", 0) + + reply_text += f"• {name} ({status})\n" + reply_text += f" └ {used_gb:.1f} ГБ из {size_gb:.1f} ГБ ({percent}%)\n" + + # Добавляем информацию о дисках + disks = storage_info.get("disks", []) + if disks: + reply_text += "\nДиски:\n" + for disk in disks: + name = disk.get("name", "Неизвестно") + model = disk.get("model", "Неизвестно") + status = disk.get("status", "Неизвестно") + temp = disk.get("temp", "?") + + reply_text += f"• {name} - {model}\n" + reply_text += f" └ Статус: {status}, Температура: {temp}°C\n" + + await message.edit_text(reply_text, parse_mode="HTML") + +async def shares_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /shares""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + message = await update.message.reply_text("⏳ Получение информации об общих папках...") + + if not synology_api.is_online(): + await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию об общих папках.", parse_mode="HTML") + return + + shares = synology_api.get_shared_folders() + + if not shares: + await message.edit_text("❌ Ошибка получения информации об общих папках", parse_mode="HTML") + return + + # Формируем сообщение об общих папках + reply_text = f"📁 Общие папки Synology NAS\n\n" + + for share in shares: + name = share.get("name", "Неизвестно") + path = share.get("path", "Неизвестно") + desc = share.get("desc", "") + + reply_text += f"• {name}\n" + reply_text += f" └ Путь: {path}\n" + + if desc: + reply_text += f" └ Описание: {desc}\n" + + await message.edit_text(reply_text, parse_mode="HTML") + +async def system_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /system""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + message = await update.message.reply_text("⏳ Получение информации о системе...") + + if not synology_api.is_online(): + await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о системе.", parse_mode="HTML") + return + + system_status = synology_api.get_system_status() + + if not system_status: + await message.edit_text("❌ Ошибка получения информации о системе", parse_mode="HTML") + return + + # Формируем сообщение о состоянии системы + model = system_status.get("model", "Неизвестно") + version = system_status.get("version", "Неизвестно") + serial = system_status.get("serial", "Неизвестно") + uptime_seconds = system_status.get("uptime", 0) + temperature = system_status.get("temperature", "?") + + # Преобразование времени работы в удобочитаемый формат + days, remainder = divmod(uptime_seconds, 86400) + hours, remainder = divmod(remainder, 3600) + minutes, seconds = divmod(remainder, 60) + uptime_str = f"{days}д {hours}ч {minutes}м {seconds}с" + + reply_text = f"🖥️ Информация о системе Synology NAS\n\n" + reply_text += f"Модель: {model}\n" + reply_text += f"Серийный номер: {serial}\n" + reply_text += f"Версия DSM: {version}\n" + reply_text += f"Время работы: {uptime_str}\n" + reply_text += f"Температура: {temperature}°C\n\n" + + # Добавляем информацию о CPU и памяти + memory = system_status.get("memory", {}) + total_memory_gb = memory.get("total_mb", 0) / 1024 + available_memory_gb = memory.get("available_mb", 0) / 1024 + memory_usage = memory.get("usage_percent", 0) + cpu_usage = system_status.get("cpu_usage", 0) + + reply_text += f"Загрузка CPU: {cpu_usage}%\n" + reply_text += f"Память: {total_memory_gb:.1f} ГБ (используется {memory_usage}%)\n" + reply_text += f"Доступно памяти: {available_memory_gb:.1f} ГБ\n\n" + + # Добавляем информацию о сетевых интерфейсах + network_info = system_status.get("network", []) + if network_info: + reply_text += "Сетевые интерфейсы:\n" + for interface in network_info: + device = interface.get("device", "Неизвестно") + ip = interface.get("ip", "Неизвестно") + mac = interface.get("mac", "Неизвестно") + + reply_text += f"• {device}\n" + reply_text += f" └ IP: {ip}, MAC: {mac}\n" + + await message.edit_text(reply_text, parse_mode="HTML") + +async def load_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /load""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + message = await update.message.reply_text("⏳ Получение информации о нагрузке системы...") + + if not synology_api.is_online(): + await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о нагрузке системы.", parse_mode="HTML") + return + + system_load = synology_api.get_system_load() + + if not system_load: + await message.edit_text("❌ Ошибка получения информации о нагрузке системы", parse_mode="HTML") + return + + # Формируем сообщение о нагрузке системы + cpu_load = system_load.get("cpu_load", 0) + memory = system_load.get("memory", {}) + memory_usage = memory.get("usage_percent", 0) + + reply_text = f"📈 Текущая нагрузка Synology NAS\n\n" + reply_text += f"Загрузка CPU: {cpu_load}%\n" + reply_text += f"Загрузка памяти: {memory_usage}%\n\n" + + # Добавляем информацию о сетевой активности + network = system_load.get("network", {}) + if network: + reply_text += "Сетевая активность:\n" + for device, stats in network.items(): + rx = int(stats.get("rx_bytes", 0)) / (1024**2) # МБ + tx = int(stats.get("tx_bytes", 0)) / (1024**2) # МБ + + reply_text += f"• {device}\n" + reply_text += f" └ Получено: {rx:.1f} МБ, Отправлено: {tx:.1f} МБ\n" + + await message.edit_text(reply_text, parse_mode="HTML") + +async def security_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /security""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + message = await update.message.reply_text("⏳ Получение информации о безопасности...") + + if not synology_api.is_online(): + await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о безопасности.", parse_mode="HTML") + return + + security_info = synology_api.get_security_status() + + if not security_info.get("success", False): + await message.edit_text("❌ Ошибка получения информации о безопасности", parse_mode="HTML") + return + + # Формируем сообщение о безопасности + status = security_info.get("status", "unknown") + is_secure = security_info.get("is_secure", False) + last_check = security_info.get("last_check", "Неизвестно") + + status_emoji = "✅" if is_secure else "⚠️" + status_text = "Безопасно" if is_secure else "Требуется внимание" + + reply_text = f"🔐 Статус безопасности Synology NAS\n\n" + reply_text += f"Статус: {status_emoji} {status_text}\n" + reply_text += f"Подробности: {status}\n" + reply_text += f"Последняя проверка: {last_check}\n" + + await message.edit_text(reply_text, parse_mode="HTML") diff --git a/.history/src/handlers/extended_handlers_20250830073739.py b/.history/src/handlers/extended_handlers_20250830073739.py new file mode 100644 index 0000000..447b046 --- /dev/null +++ b/.history/src/handlers/extended_handlers_20250830073739.py @@ -0,0 +1,263 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Дополнительные обработчики команд для телеграм-бота +""" + +import logging +from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup +from telegram.ext import ContextTypes + +from src.config.config import ADMIN_USER_IDS +from src.api.synology import SynologyAPI + +logger = logging.getLogger(__name__) + +# Инициализация API Synology +synology_api = SynologyAPI() + +async def storage_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /storage""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + message = await update.message.reply_text("⏳ Получение информации о хранилище...") + + if not synology_api.is_online(): + await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о хранилище.", parse_mode="HTML") + return + + try: + storage_info = synology_api.get_storage_status() + + if not storage_info: + await message.edit_text("❌ Ошибка получения информации о хранилище\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML") + return + except Exception as e: + await message.edit_text(f"❌ Ошибка получения информации о хранилище\n\nПричина: {str(e)}", parse_mode="HTML") + return + + # Формируем сообщение о состоянии хранилища + total_size_gb = storage_info.get("total_size", 0) / (1024**3) + total_used_gb = storage_info.get("total_used", 0) / (1024**3) + usage_percent = (total_used_gb / total_size_gb * 100) if total_size_gb > 0 else 0 + + reply_text = f"📊 Информация о хранилище Synology NAS\n\n" + reply_text += f"Общий размер: {total_size_gb:.2f} ГБ\n" + reply_text += f"Использовано: {total_used_gb:.2f} ГБ ({usage_percent:.1f}%)\n" + reply_text += f"Свободно: {total_size_gb - total_used_gb:.2f} ГБ\n\n" + + # Добавляем информацию о томах + volumes = storage_info.get("volumes", []) + if volumes: + reply_text += "Тома:\n" + for volume in volumes: + name = volume.get("name", "Неизвестно") + status = volume.get("status", "Неизвестно") + size_gb = volume.get("total_size", 0) / (1024**3) + used_gb = volume.get("used_size", 0) / (1024**3) + percent = volume.get("percent_used", 0) + + reply_text += f"• {name} ({status})\n" + reply_text += f" └ {used_gb:.1f} ГБ из {size_gb:.1f} ГБ ({percent}%)\n" + + # Добавляем информацию о дисках + disks = storage_info.get("disks", []) + if disks: + reply_text += "\nДиски:\n" + for disk in disks: + name = disk.get("name", "Неизвестно") + model = disk.get("model", "Неизвестно") + status = disk.get("status", "Неизвестно") + temp = disk.get("temp", "?") + + reply_text += f"• {name} - {model}\n" + reply_text += f" └ Статус: {status}, Температура: {temp}°C\n" + + await message.edit_text(reply_text, parse_mode="HTML") + +async def shares_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /shares""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + message = await update.message.reply_text("⏳ Получение информации об общих папках...") + + if not synology_api.is_online(): + await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию об общих папках.", parse_mode="HTML") + return + + try: + shares = synology_api.get_shared_folders() + + if not shares: + await message.edit_text("❌ Ошибка получения информации об общих папках\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML") + return + except Exception as e: + await message.edit_text(f"❌ Ошибка получения информации об общих папках\n\nПричина: {str(e)}", parse_mode="HTML") + return + + # Формируем сообщение об общих папках + reply_text = f"📁 Общие папки Synology NAS\n\n" + + for share in shares: + name = share.get("name", "Неизвестно") + path = share.get("path", "Неизвестно") + desc = share.get("desc", "") + + reply_text += f"• {name}\n" + reply_text += f" └ Путь: {path}\n" + + if desc: + reply_text += f" └ Описание: {desc}\n" + + await message.edit_text(reply_text, parse_mode="HTML") + +async def system_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /system""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + message = await update.message.reply_text("⏳ Получение информации о системе...") + + if not synology_api.is_online(): + await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о системе.", parse_mode="HTML") + return + + system_status = synology_api.get_system_status() + + if not system_status: + await message.edit_text("❌ Ошибка получения информации о системе", parse_mode="HTML") + return + + # Формируем сообщение о состоянии системы + model = system_status.get("model", "Неизвестно") + version = system_status.get("version", "Неизвестно") + serial = system_status.get("serial", "Неизвестно") + uptime_seconds = system_status.get("uptime", 0) + temperature = system_status.get("temperature", "?") + + # Преобразование времени работы в удобочитаемый формат + days, remainder = divmod(uptime_seconds, 86400) + hours, remainder = divmod(remainder, 3600) + minutes, seconds = divmod(remainder, 60) + uptime_str = f"{days}д {hours}ч {minutes}м {seconds}с" + + reply_text = f"🖥️ Информация о системе Synology NAS\n\n" + reply_text += f"Модель: {model}\n" + reply_text += f"Серийный номер: {serial}\n" + reply_text += f"Версия DSM: {version}\n" + reply_text += f"Время работы: {uptime_str}\n" + reply_text += f"Температура: {temperature}°C\n\n" + + # Добавляем информацию о CPU и памяти + memory = system_status.get("memory", {}) + total_memory_gb = memory.get("total_mb", 0) / 1024 + available_memory_gb = memory.get("available_mb", 0) / 1024 + memory_usage = memory.get("usage_percent", 0) + cpu_usage = system_status.get("cpu_usage", 0) + + reply_text += f"Загрузка CPU: {cpu_usage}%\n" + reply_text += f"Память: {total_memory_gb:.1f} ГБ (используется {memory_usage}%)\n" + reply_text += f"Доступно памяти: {available_memory_gb:.1f} ГБ\n\n" + + # Добавляем информацию о сетевых интерфейсах + network_info = system_status.get("network", []) + if network_info: + reply_text += "Сетевые интерфейсы:\n" + for interface in network_info: + device = interface.get("device", "Неизвестно") + ip = interface.get("ip", "Неизвестно") + mac = interface.get("mac", "Неизвестно") + + reply_text += f"• {device}\n" + reply_text += f" └ IP: {ip}, MAC: {mac}\n" + + await message.edit_text(reply_text, parse_mode="HTML") + +async def load_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /load""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + message = await update.message.reply_text("⏳ Получение информации о нагрузке системы...") + + if not synology_api.is_online(): + await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о нагрузке системы.", parse_mode="HTML") + return + + system_load = synology_api.get_system_load() + + if not system_load: + await message.edit_text("❌ Ошибка получения информации о нагрузке системы", parse_mode="HTML") + return + + # Формируем сообщение о нагрузке системы + cpu_load = system_load.get("cpu_load", 0) + memory = system_load.get("memory", {}) + memory_usage = memory.get("usage_percent", 0) + + reply_text = f"📈 Текущая нагрузка Synology NAS\n\n" + reply_text += f"Загрузка CPU: {cpu_load}%\n" + reply_text += f"Загрузка памяти: {memory_usage}%\n\n" + + # Добавляем информацию о сетевой активности + network = system_load.get("network", {}) + if network: + reply_text += "Сетевая активность:\n" + for device, stats in network.items(): + rx = int(stats.get("rx_bytes", 0)) / (1024**2) # МБ + tx = int(stats.get("tx_bytes", 0)) / (1024**2) # МБ + + reply_text += f"• {device}\n" + reply_text += f" └ Получено: {rx:.1f} МБ, Отправлено: {tx:.1f} МБ\n" + + await message.edit_text(reply_text, parse_mode="HTML") + +async def security_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /security""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + message = await update.message.reply_text("⏳ Получение информации о безопасности...") + + if not synology_api.is_online(): + await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о безопасности.", parse_mode="HTML") + return + + security_info = synology_api.get_security_status() + + if not security_info.get("success", False): + await message.edit_text("❌ Ошибка получения информации о безопасности", parse_mode="HTML") + return + + # Формируем сообщение о безопасности + status = security_info.get("status", "unknown") + is_secure = security_info.get("is_secure", False) + last_check = security_info.get("last_check", "Неизвестно") + + status_emoji = "✅" if is_secure else "⚠️" + status_text = "Безопасно" if is_secure else "Требуется внимание" + + reply_text = f"🔐 Статус безопасности Synology NAS\n\n" + reply_text += f"Статус: {status_emoji} {status_text}\n" + reply_text += f"Подробности: {status}\n" + reply_text += f"Последняя проверка: {last_check}\n" + + await message.edit_text(reply_text, parse_mode="HTML") diff --git a/.history/src/handlers/extended_handlers_20250830073759.py b/.history/src/handlers/extended_handlers_20250830073759.py new file mode 100644 index 0000000..65c5d7e --- /dev/null +++ b/.history/src/handlers/extended_handlers_20250830073759.py @@ -0,0 +1,273 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Дополнительные обработчики команд для телеграм-бота +""" + +import logging +from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup +from telegram.ext import ContextTypes + +from src.config.config import ADMIN_USER_IDS +from src.api.synology import SynologyAPI + +logger = logging.getLogger(__name__) + +# Инициализация API Synology +synology_api = SynologyAPI() + +async def storage_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /storage""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + message = await update.message.reply_text("⏳ Получение информации о хранилище...") + + if not synology_api.is_online(): + await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о хранилище.", parse_mode="HTML") + return + + try: + storage_info = synology_api.get_storage_status() + + if not storage_info: + await message.edit_text("❌ Ошибка получения информации о хранилище\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML") + return + except Exception as e: + await message.edit_text(f"❌ Ошибка получения информации о хранилище\n\nПричина: {str(e)}", parse_mode="HTML") + return + + # Формируем сообщение о состоянии хранилища + total_size_gb = storage_info.get("total_size", 0) / (1024**3) + total_used_gb = storage_info.get("total_used", 0) / (1024**3) + usage_percent = (total_used_gb / total_size_gb * 100) if total_size_gb > 0 else 0 + + reply_text = f"📊 Информация о хранилище Synology NAS\n\n" + reply_text += f"Общий размер: {total_size_gb:.2f} ГБ\n" + reply_text += f"Использовано: {total_used_gb:.2f} ГБ ({usage_percent:.1f}%)\n" + reply_text += f"Свободно: {total_size_gb - total_used_gb:.2f} ГБ\n\n" + + # Добавляем информацию о томах + volumes = storage_info.get("volumes", []) + if volumes: + reply_text += "Тома:\n" + for volume in volumes: + name = volume.get("name", "Неизвестно") + status = volume.get("status", "Неизвестно") + size_gb = volume.get("total_size", 0) / (1024**3) + used_gb = volume.get("used_size", 0) / (1024**3) + percent = volume.get("percent_used", 0) + + reply_text += f"• {name} ({status})\n" + reply_text += f" └ {used_gb:.1f} ГБ из {size_gb:.1f} ГБ ({percent}%)\n" + + # Добавляем информацию о дисках + disks = storage_info.get("disks", []) + if disks: + reply_text += "\nДиски:\n" + for disk in disks: + name = disk.get("name", "Неизвестно") + model = disk.get("model", "Неизвестно") + status = disk.get("status", "Неизвестно") + temp = disk.get("temp", "?") + + reply_text += f"• {name} - {model}\n" + reply_text += f" └ Статус: {status}, Температура: {temp}°C\n" + + await message.edit_text(reply_text, parse_mode="HTML") + +async def shares_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /shares""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + message = await update.message.reply_text("⏳ Получение информации об общих папках...") + + if not synology_api.is_online(): + await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию об общих папках.", parse_mode="HTML") + return + + try: + shares = synology_api.get_shared_folders() + + if not shares: + await message.edit_text("❌ Ошибка получения информации об общих папках\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML") + return + except Exception as e: + await message.edit_text(f"❌ Ошибка получения информации об общих папках\n\nПричина: {str(e)}", parse_mode="HTML") + return + + # Формируем сообщение об общих папках + reply_text = f"📁 Общие папки Synology NAS\n\n" + + for share in shares: + name = share.get("name", "Неизвестно") + path = share.get("path", "Неизвестно") + desc = share.get("desc", "") + + reply_text += f"• {name}\n" + reply_text += f" └ Путь: {path}\n" + + if desc: + reply_text += f" └ Описание: {desc}\n" + + await message.edit_text(reply_text, parse_mode="HTML") + +async def system_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /system""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + message = await update.message.reply_text("⏳ Получение информации о системе...") + + if not synology_api.is_online(): + await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о системе.", parse_mode="HTML") + return + + try: + system_status = synology_api.get_system_status() + + if not system_status: + await message.edit_text("❌ Ошибка получения информации о системе", parse_mode="HTML") + return + + # Если получен статус с ошибкой + if system_status.get("status") == "error": + error_code = system_status.get("error_code", "неизвестно") + await message.edit_text(f"❌ Ошибка получения информации о системе\n\nКод ошибки API: {error_code}", parse_mode="HTML") + return + except Exception as e: + await message.edit_text(f"❌ Ошибка получения информации о системе\n\nПричина: {str(e)}", parse_mode="HTML") + return + + # Формируем сообщение о состоянии системы + model = system_status.get("model", "Неизвестно") + version = system_status.get("version", "Неизвестно") + serial = system_status.get("serial", "Неизвестно") + uptime_seconds = system_status.get("uptime", 0) + temperature = system_status.get("temperature", "?") + + # Преобразование времени работы в удобочитаемый формат + days, remainder = divmod(uptime_seconds, 86400) + hours, remainder = divmod(remainder, 3600) + minutes, seconds = divmod(remainder, 60) + uptime_str = f"{days}д {hours}ч {minutes}м {seconds}с" + + reply_text = f"🖥️ Информация о системе Synology NAS\n\n" + reply_text += f"Модель: {model}\n" + reply_text += f"Серийный номер: {serial}\n" + reply_text += f"Версия DSM: {version}\n" + reply_text += f"Время работы: {uptime_str}\n" + reply_text += f"Температура: {temperature}°C\n\n" + + # Добавляем информацию о CPU и памяти + memory = system_status.get("memory", {}) + total_memory_gb = memory.get("total_mb", 0) / 1024 + available_memory_gb = memory.get("available_mb", 0) / 1024 + memory_usage = memory.get("usage_percent", 0) + cpu_usage = system_status.get("cpu_usage", 0) + + reply_text += f"Загрузка CPU: {cpu_usage}%\n" + reply_text += f"Память: {total_memory_gb:.1f} ГБ (используется {memory_usage}%)\n" + reply_text += f"Доступно памяти: {available_memory_gb:.1f} ГБ\n\n" + + # Добавляем информацию о сетевых интерфейсах + network_info = system_status.get("network", []) + if network_info: + reply_text += "Сетевые интерфейсы:\n" + for interface in network_info: + device = interface.get("device", "Неизвестно") + ip = interface.get("ip", "Неизвестно") + mac = interface.get("mac", "Неизвестно") + + reply_text += f"• {device}\n" + reply_text += f" └ IP: {ip}, MAC: {mac}\n" + + await message.edit_text(reply_text, parse_mode="HTML") + +async def load_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /load""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + message = await update.message.reply_text("⏳ Получение информации о нагрузке системы...") + + if not synology_api.is_online(): + await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о нагрузке системы.", parse_mode="HTML") + return + + system_load = synology_api.get_system_load() + + if not system_load: + await message.edit_text("❌ Ошибка получения информации о нагрузке системы", parse_mode="HTML") + return + + # Формируем сообщение о нагрузке системы + cpu_load = system_load.get("cpu_load", 0) + memory = system_load.get("memory", {}) + memory_usage = memory.get("usage_percent", 0) + + reply_text = f"📈 Текущая нагрузка Synology NAS\n\n" + reply_text += f"Загрузка CPU: {cpu_load}%\n" + reply_text += f"Загрузка памяти: {memory_usage}%\n\n" + + # Добавляем информацию о сетевой активности + network = system_load.get("network", {}) + if network: + reply_text += "Сетевая активность:\n" + for device, stats in network.items(): + rx = int(stats.get("rx_bytes", 0)) / (1024**2) # МБ + tx = int(stats.get("tx_bytes", 0)) / (1024**2) # МБ + + reply_text += f"• {device}\n" + reply_text += f" └ Получено: {rx:.1f} МБ, Отправлено: {tx:.1f} МБ\n" + + await message.edit_text(reply_text, parse_mode="HTML") + +async def security_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /security""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + message = await update.message.reply_text("⏳ Получение информации о безопасности...") + + if not synology_api.is_online(): + await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о безопасности.", parse_mode="HTML") + return + + security_info = synology_api.get_security_status() + + if not security_info.get("success", False): + await message.edit_text("❌ Ошибка получения информации о безопасности", parse_mode="HTML") + return + + # Формируем сообщение о безопасности + status = security_info.get("status", "unknown") + is_secure = security_info.get("is_secure", False) + last_check = security_info.get("last_check", "Неизвестно") + + status_emoji = "✅" if is_secure else "⚠️" + status_text = "Безопасно" if is_secure else "Требуется внимание" + + reply_text = f"🔐 Статус безопасности Synology NAS\n\n" + reply_text += f"Статус: {status_emoji} {status_text}\n" + reply_text += f"Подробности: {status}\n" + reply_text += f"Последняя проверка: {last_check}\n" + + await message.edit_text(reply_text, parse_mode="HTML") diff --git a/.history/src/handlers/extended_handlers_20250830073819.py b/.history/src/handlers/extended_handlers_20250830073819.py new file mode 100644 index 0000000..e7834cb --- /dev/null +++ b/.history/src/handlers/extended_handlers_20250830073819.py @@ -0,0 +1,277 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Дополнительные обработчики команд для телеграм-бота +""" + +import logging +from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup +from telegram.ext import ContextTypes + +from src.config.config import ADMIN_USER_IDS +from src.api.synology import SynologyAPI + +logger = logging.getLogger(__name__) + +# Инициализация API Synology +synology_api = SynologyAPI() + +async def storage_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /storage""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + message = await update.message.reply_text("⏳ Получение информации о хранилище...") + + if not synology_api.is_online(): + await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о хранилище.", parse_mode="HTML") + return + + try: + storage_info = synology_api.get_storage_status() + + if not storage_info: + await message.edit_text("❌ Ошибка получения информации о хранилище\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML") + return + except Exception as e: + await message.edit_text(f"❌ Ошибка получения информации о хранилище\n\nПричина: {str(e)}", parse_mode="HTML") + return + + # Формируем сообщение о состоянии хранилища + total_size_gb = storage_info.get("total_size", 0) / (1024**3) + total_used_gb = storage_info.get("total_used", 0) / (1024**3) + usage_percent = (total_used_gb / total_size_gb * 100) if total_size_gb > 0 else 0 + + reply_text = f"📊 Информация о хранилище Synology NAS\n\n" + reply_text += f"Общий размер: {total_size_gb:.2f} ГБ\n" + reply_text += f"Использовано: {total_used_gb:.2f} ГБ ({usage_percent:.1f}%)\n" + reply_text += f"Свободно: {total_size_gb - total_used_gb:.2f} ГБ\n\n" + + # Добавляем информацию о томах + volumes = storage_info.get("volumes", []) + if volumes: + reply_text += "Тома:\n" + for volume in volumes: + name = volume.get("name", "Неизвестно") + status = volume.get("status", "Неизвестно") + size_gb = volume.get("total_size", 0) / (1024**3) + used_gb = volume.get("used_size", 0) / (1024**3) + percent = volume.get("percent_used", 0) + + reply_text += f"• {name} ({status})\n" + reply_text += f" └ {used_gb:.1f} ГБ из {size_gb:.1f} ГБ ({percent}%)\n" + + # Добавляем информацию о дисках + disks = storage_info.get("disks", []) + if disks: + reply_text += "\nДиски:\n" + for disk in disks: + name = disk.get("name", "Неизвестно") + model = disk.get("model", "Неизвестно") + status = disk.get("status", "Неизвестно") + temp = disk.get("temp", "?") + + reply_text += f"• {name} - {model}\n" + reply_text += f" └ Статус: {status}, Температура: {temp}°C\n" + + await message.edit_text(reply_text, parse_mode="HTML") + +async def shares_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /shares""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + message = await update.message.reply_text("⏳ Получение информации об общих папках...") + + if not synology_api.is_online(): + await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию об общих папках.", parse_mode="HTML") + return + + try: + shares = synology_api.get_shared_folders() + + if not shares: + await message.edit_text("❌ Ошибка получения информации об общих папках\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML") + return + except Exception as e: + await message.edit_text(f"❌ Ошибка получения информации об общих папках\n\nПричина: {str(e)}", parse_mode="HTML") + return + + # Формируем сообщение об общих папках + reply_text = f"📁 Общие папки Synology NAS\n\n" + + for share in shares: + name = share.get("name", "Неизвестно") + path = share.get("path", "Неизвестно") + desc = share.get("desc", "") + + reply_text += f"• {name}\n" + reply_text += f" └ Путь: {path}\n" + + if desc: + reply_text += f" └ Описание: {desc}\n" + + await message.edit_text(reply_text, parse_mode="HTML") + +async def system_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /system""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + message = await update.message.reply_text("⏳ Получение информации о системе...") + + if not synology_api.is_online(): + await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о системе.", parse_mode="HTML") + return + + try: + system_status = synology_api.get_system_status() + + if not system_status: + await message.edit_text("❌ Ошибка получения информации о системе", parse_mode="HTML") + return + + # Если получен статус с ошибкой + if system_status.get("status") == "error": + error_code = system_status.get("error_code", "неизвестно") + await message.edit_text(f"❌ Ошибка получения информации о системе\n\nКод ошибки API: {error_code}", parse_mode="HTML") + return + except Exception as e: + await message.edit_text(f"❌ Ошибка получения информации о системе\n\nПричина: {str(e)}", parse_mode="HTML") + return + + # Формируем сообщение о состоянии системы + model = system_status.get("model", "Неизвестно") + version = system_status.get("version", "Неизвестно") + serial = system_status.get("serial", "Неизвестно") + uptime_seconds = system_status.get("uptime", 0) + temperature = system_status.get("temperature", "?") + + # Преобразование времени работы в удобочитаемый формат + days, remainder = divmod(uptime_seconds, 86400) + hours, remainder = divmod(remainder, 3600) + minutes, seconds = divmod(remainder, 60) + uptime_str = f"{days}д {hours}ч {minutes}м {seconds}с" + + reply_text = f"🖥️ Информация о системе Synology NAS\n\n" + reply_text += f"Модель: {model}\n" + reply_text += f"Серийный номер: {serial}\n" + reply_text += f"Версия DSM: {version}\n" + reply_text += f"Время работы: {uptime_str}\n" + reply_text += f"Температура: {temperature}°C\n\n" + + # Добавляем информацию о CPU и памяти + memory = system_status.get("memory", {}) + total_memory_gb = memory.get("total_mb", 0) / 1024 + available_memory_gb = memory.get("available_mb", 0) / 1024 + memory_usage = memory.get("usage_percent", 0) + cpu_usage = system_status.get("cpu_usage", 0) + + reply_text += f"Загрузка CPU: {cpu_usage}%\n" + reply_text += f"Память: {total_memory_gb:.1f} ГБ (используется {memory_usage}%)\n" + reply_text += f"Доступно памяти: {available_memory_gb:.1f} ГБ\n\n" + + # Добавляем информацию о сетевых интерфейсах + network_info = system_status.get("network", []) + if network_info: + reply_text += "Сетевые интерфейсы:\n" + for interface in network_info: + device = interface.get("device", "Неизвестно") + ip = interface.get("ip", "Неизвестно") + mac = interface.get("mac", "Неизвестно") + + reply_text += f"• {device}\n" + reply_text += f" └ IP: {ip}, MAC: {mac}\n" + + await message.edit_text(reply_text, parse_mode="HTML") + +async def load_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /load""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + message = await update.message.reply_text("⏳ Получение информации о нагрузке системы...") + + if not synology_api.is_online(): + await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о нагрузке системы.", parse_mode="HTML") + return + + try: + system_load = synology_api.get_system_load() + + if not system_load: + await message.edit_text("❌ Ошибка получения информации о нагрузке системы\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML") + return + except Exception as e: + await message.edit_text(f"❌ Ошибка получения информации о нагрузке системы\n\nПричина: {str(e)}", parse_mode="HTML") + return + + # Формируем сообщение о нагрузке системы + cpu_load = system_load.get("cpu_load", 0) + memory = system_load.get("memory", {}) + memory_usage = memory.get("usage_percent", 0) + + reply_text = f"📈 Текущая нагрузка Synology NAS\n\n" + reply_text += f"Загрузка CPU: {cpu_load}%\n" + reply_text += f"Загрузка памяти: {memory_usage}%\n\n" + + # Добавляем информацию о сетевой активности + network = system_load.get("network", {}) + if network: + reply_text += "Сетевая активность:\n" + for device, stats in network.items(): + rx = int(stats.get("rx_bytes", 0)) / (1024**2) # МБ + tx = int(stats.get("tx_bytes", 0)) / (1024**2) # МБ + + reply_text += f"• {device}\n" + reply_text += f" └ Получено: {rx:.1f} МБ, Отправлено: {tx:.1f} МБ\n" + + await message.edit_text(reply_text, parse_mode="HTML") + +async def security_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /security""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + message = await update.message.reply_text("⏳ Получение информации о безопасности...") + + if not synology_api.is_online(): + await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о безопасности.", parse_mode="HTML") + return + + security_info = synology_api.get_security_status() + + if not security_info.get("success", False): + await message.edit_text("❌ Ошибка получения информации о безопасности", parse_mode="HTML") + return + + # Формируем сообщение о безопасности + status = security_info.get("status", "unknown") + is_secure = security_info.get("is_secure", False) + last_check = security_info.get("last_check", "Неизвестно") + + status_emoji = "✅" if is_secure else "⚠️" + status_text = "Безопасно" if is_secure else "Требуется внимание" + + reply_text = f"🔐 Статус безопасности Synology NAS\n\n" + reply_text += f"Статус: {status_emoji} {status_text}\n" + reply_text += f"Подробности: {status}\n" + reply_text += f"Последняя проверка: {last_check}\n" + + await message.edit_text(reply_text, parse_mode="HTML") diff --git a/.history/src/handlers/extended_handlers_20250830073837.py b/.history/src/handlers/extended_handlers_20250830073837.py new file mode 100644 index 0000000..cf0b14a --- /dev/null +++ b/.history/src/handlers/extended_handlers_20250830073837.py @@ -0,0 +1,281 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Дополнительные обработчики команд для телеграм-бота +""" + +import logging +from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup +from telegram.ext import ContextTypes + +from src.config.config import ADMIN_USER_IDS +from src.api.synology import SynologyAPI + +logger = logging.getLogger(__name__) + +# Инициализация API Synology +synology_api = SynologyAPI() + +async def storage_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /storage""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + message = await update.message.reply_text("⏳ Получение информации о хранилище...") + + if not synology_api.is_online(): + await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о хранилище.", parse_mode="HTML") + return + + try: + storage_info = synology_api.get_storage_status() + + if not storage_info: + await message.edit_text("❌ Ошибка получения информации о хранилище\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML") + return + except Exception as e: + await message.edit_text(f"❌ Ошибка получения информации о хранилище\n\nПричина: {str(e)}", parse_mode="HTML") + return + + # Формируем сообщение о состоянии хранилища + total_size_gb = storage_info.get("total_size", 0) / (1024**3) + total_used_gb = storage_info.get("total_used", 0) / (1024**3) + usage_percent = (total_used_gb / total_size_gb * 100) if total_size_gb > 0 else 0 + + reply_text = f"📊 Информация о хранилище Synology NAS\n\n" + reply_text += f"Общий размер: {total_size_gb:.2f} ГБ\n" + reply_text += f"Использовано: {total_used_gb:.2f} ГБ ({usage_percent:.1f}%)\n" + reply_text += f"Свободно: {total_size_gb - total_used_gb:.2f} ГБ\n\n" + + # Добавляем информацию о томах + volumes = storage_info.get("volumes", []) + if volumes: + reply_text += "Тома:\n" + for volume in volumes: + name = volume.get("name", "Неизвестно") + status = volume.get("status", "Неизвестно") + size_gb = volume.get("total_size", 0) / (1024**3) + used_gb = volume.get("used_size", 0) / (1024**3) + percent = volume.get("percent_used", 0) + + reply_text += f"• {name} ({status})\n" + reply_text += f" └ {used_gb:.1f} ГБ из {size_gb:.1f} ГБ ({percent}%)\n" + + # Добавляем информацию о дисках + disks = storage_info.get("disks", []) + if disks: + reply_text += "\nДиски:\n" + for disk in disks: + name = disk.get("name", "Неизвестно") + model = disk.get("model", "Неизвестно") + status = disk.get("status", "Неизвестно") + temp = disk.get("temp", "?") + + reply_text += f"• {name} - {model}\n" + reply_text += f" └ Статус: {status}, Температура: {temp}°C\n" + + await message.edit_text(reply_text, parse_mode="HTML") + +async def shares_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /shares""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + message = await update.message.reply_text("⏳ Получение информации об общих папках...") + + if not synology_api.is_online(): + await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию об общих папках.", parse_mode="HTML") + return + + try: + shares = synology_api.get_shared_folders() + + if not shares: + await message.edit_text("❌ Ошибка получения информации об общих папках\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML") + return + except Exception as e: + await message.edit_text(f"❌ Ошибка получения информации об общих папках\n\nПричина: {str(e)}", parse_mode="HTML") + return + + # Формируем сообщение об общих папках + reply_text = f"📁 Общие папки Synology NAS\n\n" + + for share in shares: + name = share.get("name", "Неизвестно") + path = share.get("path", "Неизвестно") + desc = share.get("desc", "") + + reply_text += f"• {name}\n" + reply_text += f" └ Путь: {path}\n" + + if desc: + reply_text += f" └ Описание: {desc}\n" + + await message.edit_text(reply_text, parse_mode="HTML") + +async def system_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /system""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + message = await update.message.reply_text("⏳ Получение информации о системе...") + + if not synology_api.is_online(): + await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о системе.", parse_mode="HTML") + return + + try: + system_status = synology_api.get_system_status() + + if not system_status: + await message.edit_text("❌ Ошибка получения информации о системе", parse_mode="HTML") + return + + # Если получен статус с ошибкой + if system_status.get("status") == "error": + error_code = system_status.get("error_code", "неизвестно") + await message.edit_text(f"❌ Ошибка получения информации о системе\n\nКод ошибки API: {error_code}", parse_mode="HTML") + return + except Exception as e: + await message.edit_text(f"❌ Ошибка получения информации о системе\n\nПричина: {str(e)}", parse_mode="HTML") + return + + # Формируем сообщение о состоянии системы + model = system_status.get("model", "Неизвестно") + version = system_status.get("version", "Неизвестно") + serial = system_status.get("serial", "Неизвестно") + uptime_seconds = system_status.get("uptime", 0) + temperature = system_status.get("temperature", "?") + + # Преобразование времени работы в удобочитаемый формат + days, remainder = divmod(uptime_seconds, 86400) + hours, remainder = divmod(remainder, 3600) + minutes, seconds = divmod(remainder, 60) + uptime_str = f"{days}д {hours}ч {minutes}м {seconds}с" + + reply_text = f"🖥️ Информация о системе Synology NAS\n\n" + reply_text += f"Модель: {model}\n" + reply_text += f"Серийный номер: {serial}\n" + reply_text += f"Версия DSM: {version}\n" + reply_text += f"Время работы: {uptime_str}\n" + reply_text += f"Температура: {temperature}°C\n\n" + + # Добавляем информацию о CPU и памяти + memory = system_status.get("memory", {}) + total_memory_gb = memory.get("total_mb", 0) / 1024 + available_memory_gb = memory.get("available_mb", 0) / 1024 + memory_usage = memory.get("usage_percent", 0) + cpu_usage = system_status.get("cpu_usage", 0) + + reply_text += f"Загрузка CPU: {cpu_usage}%\n" + reply_text += f"Память: {total_memory_gb:.1f} ГБ (используется {memory_usage}%)\n" + reply_text += f"Доступно памяти: {available_memory_gb:.1f} ГБ\n\n" + + # Добавляем информацию о сетевых интерфейсах + network_info = system_status.get("network", []) + if network_info: + reply_text += "Сетевые интерфейсы:\n" + for interface in network_info: + device = interface.get("device", "Неизвестно") + ip = interface.get("ip", "Неизвестно") + mac = interface.get("mac", "Неизвестно") + + reply_text += f"• {device}\n" + reply_text += f" └ IP: {ip}, MAC: {mac}\n" + + await message.edit_text(reply_text, parse_mode="HTML") + +async def load_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /load""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + message = await update.message.reply_text("⏳ Получение информации о нагрузке системы...") + + if not synology_api.is_online(): + await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о нагрузке системы.", parse_mode="HTML") + return + + try: + system_load = synology_api.get_system_load() + + if not system_load: + await message.edit_text("❌ Ошибка получения информации о нагрузке системы\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML") + return + except Exception as e: + await message.edit_text(f"❌ Ошибка получения информации о нагрузке системы\n\nПричина: {str(e)}", parse_mode="HTML") + return + + # Формируем сообщение о нагрузке системы + cpu_load = system_load.get("cpu_load", 0) + memory = system_load.get("memory", {}) + memory_usage = memory.get("usage_percent", 0) + + reply_text = f"📈 Текущая нагрузка Synology NAS\n\n" + reply_text += f"Загрузка CPU: {cpu_load}%\n" + reply_text += f"Загрузка памяти: {memory_usage}%\n\n" + + # Добавляем информацию о сетевой активности + network = system_load.get("network", {}) + if network: + reply_text += "Сетевая активность:\n" + for device, stats in network.items(): + rx = int(stats.get("rx_bytes", 0)) / (1024**2) # МБ + tx = int(stats.get("tx_bytes", 0)) / (1024**2) # МБ + + reply_text += f"• {device}\n" + reply_text += f" └ Получено: {rx:.1f} МБ, Отправлено: {tx:.1f} МБ\n" + + await message.edit_text(reply_text, parse_mode="HTML") + +async def security_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /security""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + message = await update.message.reply_text("⏳ Получение информации о безопасности...") + + if not synology_api.is_online(): + await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о безопасности.", parse_mode="HTML") + return + + try: + security_info = synology_api.get_security_status() + + if not security_info.get("success", False): + await message.edit_text("❌ Ошибка получения информации о безопасности\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML") + return + except Exception as e: + await message.edit_text(f"❌ Ошибка получения информации о безопасности\n\nПричина: {str(e)}", parse_mode="HTML") + return + + # Формируем сообщение о безопасности + status = security_info.get("status", "unknown") + is_secure = security_info.get("is_secure", False) + last_check = security_info.get("last_check", "Неизвестно") + + status_emoji = "✅" if is_secure else "⚠️" + status_text = "Безопасно" if is_secure else "Требуется внимание" + + reply_text = f"🔐 Статус безопасности Synology NAS\n\n" + reply_text += f"Статус: {status_emoji} {status_text}\n" + reply_text += f"Подробности: {status}\n" + reply_text += f"Последняя проверка: {last_check}\n" + + await message.edit_text(reply_text, parse_mode="HTML") diff --git a/.history/src/handlers/extended_handlers_20250830074140.py b/.history/src/handlers/extended_handlers_20250830074140.py new file mode 100644 index 0000000..cf0b14a --- /dev/null +++ b/.history/src/handlers/extended_handlers_20250830074140.py @@ -0,0 +1,281 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Дополнительные обработчики команд для телеграм-бота +""" + +import logging +from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup +from telegram.ext import ContextTypes + +from src.config.config import ADMIN_USER_IDS +from src.api.synology import SynologyAPI + +logger = logging.getLogger(__name__) + +# Инициализация API Synology +synology_api = SynologyAPI() + +async def storage_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /storage""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + message = await update.message.reply_text("⏳ Получение информации о хранилище...") + + if not synology_api.is_online(): + await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о хранилище.", parse_mode="HTML") + return + + try: + storage_info = synology_api.get_storage_status() + + if not storage_info: + await message.edit_text("❌ Ошибка получения информации о хранилище\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML") + return + except Exception as e: + await message.edit_text(f"❌ Ошибка получения информации о хранилище\n\nПричина: {str(e)}", parse_mode="HTML") + return + + # Формируем сообщение о состоянии хранилища + total_size_gb = storage_info.get("total_size", 0) / (1024**3) + total_used_gb = storage_info.get("total_used", 0) / (1024**3) + usage_percent = (total_used_gb / total_size_gb * 100) if total_size_gb > 0 else 0 + + reply_text = f"📊 Информация о хранилище Synology NAS\n\n" + reply_text += f"Общий размер: {total_size_gb:.2f} ГБ\n" + reply_text += f"Использовано: {total_used_gb:.2f} ГБ ({usage_percent:.1f}%)\n" + reply_text += f"Свободно: {total_size_gb - total_used_gb:.2f} ГБ\n\n" + + # Добавляем информацию о томах + volumes = storage_info.get("volumes", []) + if volumes: + reply_text += "Тома:\n" + for volume in volumes: + name = volume.get("name", "Неизвестно") + status = volume.get("status", "Неизвестно") + size_gb = volume.get("total_size", 0) / (1024**3) + used_gb = volume.get("used_size", 0) / (1024**3) + percent = volume.get("percent_used", 0) + + reply_text += f"• {name} ({status})\n" + reply_text += f" └ {used_gb:.1f} ГБ из {size_gb:.1f} ГБ ({percent}%)\n" + + # Добавляем информацию о дисках + disks = storage_info.get("disks", []) + if disks: + reply_text += "\nДиски:\n" + for disk in disks: + name = disk.get("name", "Неизвестно") + model = disk.get("model", "Неизвестно") + status = disk.get("status", "Неизвестно") + temp = disk.get("temp", "?") + + reply_text += f"• {name} - {model}\n" + reply_text += f" └ Статус: {status}, Температура: {temp}°C\n" + + await message.edit_text(reply_text, parse_mode="HTML") + +async def shares_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /shares""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + message = await update.message.reply_text("⏳ Получение информации об общих папках...") + + if not synology_api.is_online(): + await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию об общих папках.", parse_mode="HTML") + return + + try: + shares = synology_api.get_shared_folders() + + if not shares: + await message.edit_text("❌ Ошибка получения информации об общих папках\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML") + return + except Exception as e: + await message.edit_text(f"❌ Ошибка получения информации об общих папках\n\nПричина: {str(e)}", parse_mode="HTML") + return + + # Формируем сообщение об общих папках + reply_text = f"📁 Общие папки Synology NAS\n\n" + + for share in shares: + name = share.get("name", "Неизвестно") + path = share.get("path", "Неизвестно") + desc = share.get("desc", "") + + reply_text += f"• {name}\n" + reply_text += f" └ Путь: {path}\n" + + if desc: + reply_text += f" └ Описание: {desc}\n" + + await message.edit_text(reply_text, parse_mode="HTML") + +async def system_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /system""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + message = await update.message.reply_text("⏳ Получение информации о системе...") + + if not synology_api.is_online(): + await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о системе.", parse_mode="HTML") + return + + try: + system_status = synology_api.get_system_status() + + if not system_status: + await message.edit_text("❌ Ошибка получения информации о системе", parse_mode="HTML") + return + + # Если получен статус с ошибкой + if system_status.get("status") == "error": + error_code = system_status.get("error_code", "неизвестно") + await message.edit_text(f"❌ Ошибка получения информации о системе\n\nКод ошибки API: {error_code}", parse_mode="HTML") + return + except Exception as e: + await message.edit_text(f"❌ Ошибка получения информации о системе\n\nПричина: {str(e)}", parse_mode="HTML") + return + + # Формируем сообщение о состоянии системы + model = system_status.get("model", "Неизвестно") + version = system_status.get("version", "Неизвестно") + serial = system_status.get("serial", "Неизвестно") + uptime_seconds = system_status.get("uptime", 0) + temperature = system_status.get("temperature", "?") + + # Преобразование времени работы в удобочитаемый формат + days, remainder = divmod(uptime_seconds, 86400) + hours, remainder = divmod(remainder, 3600) + minutes, seconds = divmod(remainder, 60) + uptime_str = f"{days}д {hours}ч {minutes}м {seconds}с" + + reply_text = f"🖥️ Информация о системе Synology NAS\n\n" + reply_text += f"Модель: {model}\n" + reply_text += f"Серийный номер: {serial}\n" + reply_text += f"Версия DSM: {version}\n" + reply_text += f"Время работы: {uptime_str}\n" + reply_text += f"Температура: {temperature}°C\n\n" + + # Добавляем информацию о CPU и памяти + memory = system_status.get("memory", {}) + total_memory_gb = memory.get("total_mb", 0) / 1024 + available_memory_gb = memory.get("available_mb", 0) / 1024 + memory_usage = memory.get("usage_percent", 0) + cpu_usage = system_status.get("cpu_usage", 0) + + reply_text += f"Загрузка CPU: {cpu_usage}%\n" + reply_text += f"Память: {total_memory_gb:.1f} ГБ (используется {memory_usage}%)\n" + reply_text += f"Доступно памяти: {available_memory_gb:.1f} ГБ\n\n" + + # Добавляем информацию о сетевых интерфейсах + network_info = system_status.get("network", []) + if network_info: + reply_text += "Сетевые интерфейсы:\n" + for interface in network_info: + device = interface.get("device", "Неизвестно") + ip = interface.get("ip", "Неизвестно") + mac = interface.get("mac", "Неизвестно") + + reply_text += f"• {device}\n" + reply_text += f" └ IP: {ip}, MAC: {mac}\n" + + await message.edit_text(reply_text, parse_mode="HTML") + +async def load_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /load""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + message = await update.message.reply_text("⏳ Получение информации о нагрузке системы...") + + if not synology_api.is_online(): + await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о нагрузке системы.", parse_mode="HTML") + return + + try: + system_load = synology_api.get_system_load() + + if not system_load: + await message.edit_text("❌ Ошибка получения информации о нагрузке системы\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML") + return + except Exception as e: + await message.edit_text(f"❌ Ошибка получения информации о нагрузке системы\n\nПричина: {str(e)}", parse_mode="HTML") + return + + # Формируем сообщение о нагрузке системы + cpu_load = system_load.get("cpu_load", 0) + memory = system_load.get("memory", {}) + memory_usage = memory.get("usage_percent", 0) + + reply_text = f"📈 Текущая нагрузка Synology NAS\n\n" + reply_text += f"Загрузка CPU: {cpu_load}%\n" + reply_text += f"Загрузка памяти: {memory_usage}%\n\n" + + # Добавляем информацию о сетевой активности + network = system_load.get("network", {}) + if network: + reply_text += "Сетевая активность:\n" + for device, stats in network.items(): + rx = int(stats.get("rx_bytes", 0)) / (1024**2) # МБ + tx = int(stats.get("tx_bytes", 0)) / (1024**2) # МБ + + reply_text += f"• {device}\n" + reply_text += f" └ Получено: {rx:.1f} МБ, Отправлено: {tx:.1f} МБ\n" + + await message.edit_text(reply_text, parse_mode="HTML") + +async def security_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /security""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + message = await update.message.reply_text("⏳ Получение информации о безопасности...") + + if not synology_api.is_online(): + await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о безопасности.", parse_mode="HTML") + return + + try: + security_info = synology_api.get_security_status() + + if not security_info.get("success", False): + await message.edit_text("❌ Ошибка получения информации о безопасности\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML") + return + except Exception as e: + await message.edit_text(f"❌ Ошибка получения информации о безопасности\n\nПричина: {str(e)}", parse_mode="HTML") + return + + # Формируем сообщение о безопасности + status = security_info.get("status", "unknown") + is_secure = security_info.get("is_secure", False) + last_check = security_info.get("last_check", "Неизвестно") + + status_emoji = "✅" if is_secure else "⚠️" + status_text = "Безопасно" if is_secure else "Требуется внимание" + + reply_text = f"🔐 Статус безопасности Synology NAS\n\n" + reply_text += f"Статус: {status_emoji} {status_text}\n" + reply_text += f"Подробности: {status}\n" + reply_text += f"Последняя проверка: {last_check}\n" + + await message.edit_text(reply_text, parse_mode="HTML") diff --git a/.history/src/handlers/extended_handlers_20250830083308.py b/.history/src/handlers/extended_handlers_20250830083308.py new file mode 100644 index 0000000..efa4fa9 --- /dev/null +++ b/.history/src/handlers/extended_handlers_20250830083308.py @@ -0,0 +1,345 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Дополнительные обработчики команд для телеграм-бота +""" + +import logging +from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup +from telegram.ext import ContextTypes + +from src.config.config import ADMIN_USER_IDS +from src.api.synology import SynologyAPI + +logger = logging.getLogger(__name__) + +# Инициализация API Synology +synology_api = SynologyAPI() + +async def check_api_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /checkapi для диагностики проблем с API""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + message = await update.message.reply_text("⏳ Проверка доступных API Synology NAS...") + + from src.api.api_discovery import discover_available_apis + from src.config.config import ( + SYNOLOGY_HOST, + SYNOLOGY_PORT, + SYNOLOGY_SECURE, + SYNOLOGY_POWER_API, + SYNOLOGY_INFO_API, + SYNOLOGY_API_VERSION + ) + + # Формируем базовый URL + protocol = "https" if SYNOLOGY_SECURE else "http" + base_url = f"{protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi" + + # Получаем список доступных API + apis = discover_available_apis(base_url) + + if not apis: + await message.edit_text( + "❌ Не удалось получить список доступных API\n\n" + "Проверьте доступность NAS и сетевое подключение.", + parse_mode="HTML" + ) + return + + # Поиск API для управления питанием + power_apis = [name for name in apis.keys() if "power" in name.lower()] + system_info_apis = [name for name in apis.keys() if ("system" in name.lower() or "dsm" in name.lower()) and "info" in name.lower()] + reboot_apis = [name for name in apis.keys() if any(word in name.lower() for word in ["restart", "reboot", "power"])] + + # Формируем рекомендации + recommended_power_api = max(power_apis, key=lambda x: apis[x].get('maxVersion', 1)) if power_apis else "SYNO.Core.System" + recommended_info_api = max(system_info_apis, key=lambda x: apis[x].get('maxVersion', 1)) if system_info_apis else "SYNO.DSM.Info" + + # Формируем текст отчета + api_report = ( + f"✅ Найдено {len(apis)} доступных API\n\n" + f"API для управления питанием:\n" + f"{', '.join(power_apis[:5]) or 'Не найдены'}\n\n" + f"API для информации о системе:\n" + f"{', '.join(system_info_apis[:5]) or 'Не найдены'}\n\n" + f"API для перезагрузки:\n" + f"{', '.join(reboot_apis[:5]) or 'Не найдены'}\n\n" + f"Рекомендуемые настройки:\n" + f"Power API: {recommended_power_api}\n" + f"Info API: {recommended_info_api}\n\n" + f"Текущие настройки в конфигурации:\n" + f"SYNOLOGY_POWER_API = {SYNOLOGY_POWER_API}\n" + f"SYNOLOGY_INFO_API = {SYNOLOGY_INFO_API}\n" + f"SYNOLOGY_API_VERSION = {SYNOLOGY_API_VERSION}" + ) + + await message.edit_text(api_report, parse_mode="HTML") + +async def storage_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /storage""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + message = await update.message.reply_text("⏳ Получение информации о хранилище...") + + if not synology_api.is_online(): + await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о хранилище.", parse_mode="HTML") + return + + try: + storage_info = synology_api.get_storage_status() + + if not storage_info: + await message.edit_text("❌ Ошибка получения информации о хранилище\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML") + return + except Exception as e: + await message.edit_text(f"❌ Ошибка получения информации о хранилище\n\nПричина: {str(e)}", parse_mode="HTML") + return + + # Формируем сообщение о состоянии хранилища + total_size_gb = storage_info.get("total_size", 0) / (1024**3) + total_used_gb = storage_info.get("total_used", 0) / (1024**3) + usage_percent = (total_used_gb / total_size_gb * 100) if total_size_gb > 0 else 0 + + reply_text = f"📊 Информация о хранилище Synology NAS\n\n" + reply_text += f"Общий размер: {total_size_gb:.2f} ГБ\n" + reply_text += f"Использовано: {total_used_gb:.2f} ГБ ({usage_percent:.1f}%)\n" + reply_text += f"Свободно: {total_size_gb - total_used_gb:.2f} ГБ\n\n" + + # Добавляем информацию о томах + volumes = storage_info.get("volumes", []) + if volumes: + reply_text += "Тома:\n" + for volume in volumes: + name = volume.get("name", "Неизвестно") + status = volume.get("status", "Неизвестно") + size_gb = volume.get("total_size", 0) / (1024**3) + used_gb = volume.get("used_size", 0) / (1024**3) + percent = volume.get("percent_used", 0) + + reply_text += f"• {name} ({status})\n" + reply_text += f" └ {used_gb:.1f} ГБ из {size_gb:.1f} ГБ ({percent}%)\n" + + # Добавляем информацию о дисках + disks = storage_info.get("disks", []) + if disks: + reply_text += "\nДиски:\n" + for disk in disks: + name = disk.get("name", "Неизвестно") + model = disk.get("model", "Неизвестно") + status = disk.get("status", "Неизвестно") + temp = disk.get("temp", "?") + + reply_text += f"• {name} - {model}\n" + reply_text += f" └ Статус: {status}, Температура: {temp}°C\n" + + await message.edit_text(reply_text, parse_mode="HTML") + +async def shares_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /shares""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + message = await update.message.reply_text("⏳ Получение информации об общих папках...") + + if not synology_api.is_online(): + await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию об общих папках.", parse_mode="HTML") + return + + try: + shares = synology_api.get_shared_folders() + + if not shares: + await message.edit_text("❌ Ошибка получения информации об общих папках\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML") + return + except Exception as e: + await message.edit_text(f"❌ Ошибка получения информации об общих папках\n\nПричина: {str(e)}", parse_mode="HTML") + return + + # Формируем сообщение об общих папках + reply_text = f"📁 Общие папки Synology NAS\n\n" + + for share in shares: + name = share.get("name", "Неизвестно") + path = share.get("path", "Неизвестно") + desc = share.get("desc", "") + + reply_text += f"• {name}\n" + reply_text += f" └ Путь: {path}\n" + + if desc: + reply_text += f" └ Описание: {desc}\n" + + await message.edit_text(reply_text, parse_mode="HTML") + +async def system_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /system""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + message = await update.message.reply_text("⏳ Получение информации о системе...") + + if not synology_api.is_online(): + await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о системе.", parse_mode="HTML") + return + + try: + system_status = synology_api.get_system_status() + + if not system_status: + await message.edit_text("❌ Ошибка получения информации о системе", parse_mode="HTML") + return + + # Если получен статус с ошибкой + if system_status.get("status") == "error": + error_code = system_status.get("error_code", "неизвестно") + await message.edit_text(f"❌ Ошибка получения информации о системе\n\nКод ошибки API: {error_code}", parse_mode="HTML") + return + except Exception as e: + await message.edit_text(f"❌ Ошибка получения информации о системе\n\nПричина: {str(e)}", parse_mode="HTML") + return + + # Формируем сообщение о состоянии системы + model = system_status.get("model", "Неизвестно") + version = system_status.get("version", "Неизвестно") + serial = system_status.get("serial", "Неизвестно") + uptime_seconds = system_status.get("uptime", 0) + temperature = system_status.get("temperature", "?") + + # Преобразование времени работы в удобочитаемый формат + days, remainder = divmod(uptime_seconds, 86400) + hours, remainder = divmod(remainder, 3600) + minutes, seconds = divmod(remainder, 60) + uptime_str = f"{days}д {hours}ч {minutes}м {seconds}с" + + reply_text = f"🖥️ Информация о системе Synology NAS\n\n" + reply_text += f"Модель: {model}\n" + reply_text += f"Серийный номер: {serial}\n" + reply_text += f"Версия DSM: {version}\n" + reply_text += f"Время работы: {uptime_str}\n" + reply_text += f"Температура: {temperature}°C\n\n" + + # Добавляем информацию о CPU и памяти + memory = system_status.get("memory", {}) + total_memory_gb = memory.get("total_mb", 0) / 1024 + available_memory_gb = memory.get("available_mb", 0) / 1024 + memory_usage = memory.get("usage_percent", 0) + cpu_usage = system_status.get("cpu_usage", 0) + + reply_text += f"Загрузка CPU: {cpu_usage}%\n" + reply_text += f"Память: {total_memory_gb:.1f} ГБ (используется {memory_usage}%)\n" + reply_text += f"Доступно памяти: {available_memory_gb:.1f} ГБ\n\n" + + # Добавляем информацию о сетевых интерфейсах + network_info = system_status.get("network", []) + if network_info: + reply_text += "Сетевые интерфейсы:\n" + for interface in network_info: + device = interface.get("device", "Неизвестно") + ip = interface.get("ip", "Неизвестно") + mac = interface.get("mac", "Неизвестно") + + reply_text += f"• {device}\n" + reply_text += f" └ IP: {ip}, MAC: {mac}\n" + + await message.edit_text(reply_text, parse_mode="HTML") + +async def load_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /load""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + message = await update.message.reply_text("⏳ Получение информации о нагрузке системы...") + + if not synology_api.is_online(): + await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о нагрузке системы.", parse_mode="HTML") + return + + try: + system_load = synology_api.get_system_load() + + if not system_load: + await message.edit_text("❌ Ошибка получения информации о нагрузке системы\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML") + return + except Exception as e: + await message.edit_text(f"❌ Ошибка получения информации о нагрузке системы\n\nПричина: {str(e)}", parse_mode="HTML") + return + + # Формируем сообщение о нагрузке системы + cpu_load = system_load.get("cpu_load", 0) + memory = system_load.get("memory", {}) + memory_usage = memory.get("usage_percent", 0) + + reply_text = f"📈 Текущая нагрузка Synology NAS\n\n" + reply_text += f"Загрузка CPU: {cpu_load}%\n" + reply_text += f"Загрузка памяти: {memory_usage}%\n\n" + + # Добавляем информацию о сетевой активности + network = system_load.get("network", {}) + if network: + reply_text += "Сетевая активность:\n" + for device, stats in network.items(): + rx = int(stats.get("rx_bytes", 0)) / (1024**2) # МБ + tx = int(stats.get("tx_bytes", 0)) / (1024**2) # МБ + + reply_text += f"• {device}\n" + reply_text += f" └ Получено: {rx:.1f} МБ, Отправлено: {tx:.1f} МБ\n" + + await message.edit_text(reply_text, parse_mode="HTML") + +async def security_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /security""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + message = await update.message.reply_text("⏳ Получение информации о безопасности...") + + if not synology_api.is_online(): + await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о безопасности.", parse_mode="HTML") + return + + try: + security_info = synology_api.get_security_status() + + if not security_info.get("success", False): + await message.edit_text("❌ Ошибка получения информации о безопасности\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML") + return + except Exception as e: + await message.edit_text(f"❌ Ошибка получения информации о безопасности\n\nПричина: {str(e)}", parse_mode="HTML") + return + + # Формируем сообщение о безопасности + status = security_info.get("status", "unknown") + is_secure = security_info.get("is_secure", False) + last_check = security_info.get("last_check", "Неизвестно") + + status_emoji = "✅" if is_secure else "⚠️" + status_text = "Безопасно" if is_secure else "Требуется внимание" + + reply_text = f"🔐 Статус безопасности Synology NAS\n\n" + reply_text += f"Статус: {status_emoji} {status_text}\n" + reply_text += f"Подробности: {status}\n" + reply_text += f"Последняя проверка: {last_check}\n" + + await message.edit_text(reply_text, parse_mode="HTML") diff --git a/.history/src/handlers/extended_handlers_20250830083502.py b/.history/src/handlers/extended_handlers_20250830083502.py new file mode 100644 index 0000000..efa4fa9 --- /dev/null +++ b/.history/src/handlers/extended_handlers_20250830083502.py @@ -0,0 +1,345 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Дополнительные обработчики команд для телеграм-бота +""" + +import logging +from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup +from telegram.ext import ContextTypes + +from src.config.config import ADMIN_USER_IDS +from src.api.synology import SynologyAPI + +logger = logging.getLogger(__name__) + +# Инициализация API Synology +synology_api = SynologyAPI() + +async def check_api_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /checkapi для диагностики проблем с API""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + message = await update.message.reply_text("⏳ Проверка доступных API Synology NAS...") + + from src.api.api_discovery import discover_available_apis + from src.config.config import ( + SYNOLOGY_HOST, + SYNOLOGY_PORT, + SYNOLOGY_SECURE, + SYNOLOGY_POWER_API, + SYNOLOGY_INFO_API, + SYNOLOGY_API_VERSION + ) + + # Формируем базовый URL + protocol = "https" if SYNOLOGY_SECURE else "http" + base_url = f"{protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi" + + # Получаем список доступных API + apis = discover_available_apis(base_url) + + if not apis: + await message.edit_text( + "❌ Не удалось получить список доступных API\n\n" + "Проверьте доступность NAS и сетевое подключение.", + parse_mode="HTML" + ) + return + + # Поиск API для управления питанием + power_apis = [name for name in apis.keys() if "power" in name.lower()] + system_info_apis = [name for name in apis.keys() if ("system" in name.lower() or "dsm" in name.lower()) and "info" in name.lower()] + reboot_apis = [name for name in apis.keys() if any(word in name.lower() for word in ["restart", "reboot", "power"])] + + # Формируем рекомендации + recommended_power_api = max(power_apis, key=lambda x: apis[x].get('maxVersion', 1)) if power_apis else "SYNO.Core.System" + recommended_info_api = max(system_info_apis, key=lambda x: apis[x].get('maxVersion', 1)) if system_info_apis else "SYNO.DSM.Info" + + # Формируем текст отчета + api_report = ( + f"✅ Найдено {len(apis)} доступных API\n\n" + f"API для управления питанием:\n" + f"{', '.join(power_apis[:5]) or 'Не найдены'}\n\n" + f"API для информации о системе:\n" + f"{', '.join(system_info_apis[:5]) or 'Не найдены'}\n\n" + f"API для перезагрузки:\n" + f"{', '.join(reboot_apis[:5]) or 'Не найдены'}\n\n" + f"Рекомендуемые настройки:\n" + f"Power API: {recommended_power_api}\n" + f"Info API: {recommended_info_api}\n\n" + f"Текущие настройки в конфигурации:\n" + f"SYNOLOGY_POWER_API = {SYNOLOGY_POWER_API}\n" + f"SYNOLOGY_INFO_API = {SYNOLOGY_INFO_API}\n" + f"SYNOLOGY_API_VERSION = {SYNOLOGY_API_VERSION}" + ) + + await message.edit_text(api_report, parse_mode="HTML") + +async def storage_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /storage""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + message = await update.message.reply_text("⏳ Получение информации о хранилище...") + + if not synology_api.is_online(): + await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о хранилище.", parse_mode="HTML") + return + + try: + storage_info = synology_api.get_storage_status() + + if not storage_info: + await message.edit_text("❌ Ошибка получения информации о хранилище\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML") + return + except Exception as e: + await message.edit_text(f"❌ Ошибка получения информации о хранилище\n\nПричина: {str(e)}", parse_mode="HTML") + return + + # Формируем сообщение о состоянии хранилища + total_size_gb = storage_info.get("total_size", 0) / (1024**3) + total_used_gb = storage_info.get("total_used", 0) / (1024**3) + usage_percent = (total_used_gb / total_size_gb * 100) if total_size_gb > 0 else 0 + + reply_text = f"📊 Информация о хранилище Synology NAS\n\n" + reply_text += f"Общий размер: {total_size_gb:.2f} ГБ\n" + reply_text += f"Использовано: {total_used_gb:.2f} ГБ ({usage_percent:.1f}%)\n" + reply_text += f"Свободно: {total_size_gb - total_used_gb:.2f} ГБ\n\n" + + # Добавляем информацию о томах + volumes = storage_info.get("volumes", []) + if volumes: + reply_text += "Тома:\n" + for volume in volumes: + name = volume.get("name", "Неизвестно") + status = volume.get("status", "Неизвестно") + size_gb = volume.get("total_size", 0) / (1024**3) + used_gb = volume.get("used_size", 0) / (1024**3) + percent = volume.get("percent_used", 0) + + reply_text += f"• {name} ({status})\n" + reply_text += f" └ {used_gb:.1f} ГБ из {size_gb:.1f} ГБ ({percent}%)\n" + + # Добавляем информацию о дисках + disks = storage_info.get("disks", []) + if disks: + reply_text += "\nДиски:\n" + for disk in disks: + name = disk.get("name", "Неизвестно") + model = disk.get("model", "Неизвестно") + status = disk.get("status", "Неизвестно") + temp = disk.get("temp", "?") + + reply_text += f"• {name} - {model}\n" + reply_text += f" └ Статус: {status}, Температура: {temp}°C\n" + + await message.edit_text(reply_text, parse_mode="HTML") + +async def shares_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /shares""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + message = await update.message.reply_text("⏳ Получение информации об общих папках...") + + if not synology_api.is_online(): + await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию об общих папках.", parse_mode="HTML") + return + + try: + shares = synology_api.get_shared_folders() + + if not shares: + await message.edit_text("❌ Ошибка получения информации об общих папках\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML") + return + except Exception as e: + await message.edit_text(f"❌ Ошибка получения информации об общих папках\n\nПричина: {str(e)}", parse_mode="HTML") + return + + # Формируем сообщение об общих папках + reply_text = f"📁 Общие папки Synology NAS\n\n" + + for share in shares: + name = share.get("name", "Неизвестно") + path = share.get("path", "Неизвестно") + desc = share.get("desc", "") + + reply_text += f"• {name}\n" + reply_text += f" └ Путь: {path}\n" + + if desc: + reply_text += f" └ Описание: {desc}\n" + + await message.edit_text(reply_text, parse_mode="HTML") + +async def system_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /system""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + message = await update.message.reply_text("⏳ Получение информации о системе...") + + if not synology_api.is_online(): + await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о системе.", parse_mode="HTML") + return + + try: + system_status = synology_api.get_system_status() + + if not system_status: + await message.edit_text("❌ Ошибка получения информации о системе", parse_mode="HTML") + return + + # Если получен статус с ошибкой + if system_status.get("status") == "error": + error_code = system_status.get("error_code", "неизвестно") + await message.edit_text(f"❌ Ошибка получения информации о системе\n\nКод ошибки API: {error_code}", parse_mode="HTML") + return + except Exception as e: + await message.edit_text(f"❌ Ошибка получения информации о системе\n\nПричина: {str(e)}", parse_mode="HTML") + return + + # Формируем сообщение о состоянии системы + model = system_status.get("model", "Неизвестно") + version = system_status.get("version", "Неизвестно") + serial = system_status.get("serial", "Неизвестно") + uptime_seconds = system_status.get("uptime", 0) + temperature = system_status.get("temperature", "?") + + # Преобразование времени работы в удобочитаемый формат + days, remainder = divmod(uptime_seconds, 86400) + hours, remainder = divmod(remainder, 3600) + minutes, seconds = divmod(remainder, 60) + uptime_str = f"{days}д {hours}ч {minutes}м {seconds}с" + + reply_text = f"🖥️ Информация о системе Synology NAS\n\n" + reply_text += f"Модель: {model}\n" + reply_text += f"Серийный номер: {serial}\n" + reply_text += f"Версия DSM: {version}\n" + reply_text += f"Время работы: {uptime_str}\n" + reply_text += f"Температура: {temperature}°C\n\n" + + # Добавляем информацию о CPU и памяти + memory = system_status.get("memory", {}) + total_memory_gb = memory.get("total_mb", 0) / 1024 + available_memory_gb = memory.get("available_mb", 0) / 1024 + memory_usage = memory.get("usage_percent", 0) + cpu_usage = system_status.get("cpu_usage", 0) + + reply_text += f"Загрузка CPU: {cpu_usage}%\n" + reply_text += f"Память: {total_memory_gb:.1f} ГБ (используется {memory_usage}%)\n" + reply_text += f"Доступно памяти: {available_memory_gb:.1f} ГБ\n\n" + + # Добавляем информацию о сетевых интерфейсах + network_info = system_status.get("network", []) + if network_info: + reply_text += "Сетевые интерфейсы:\n" + for interface in network_info: + device = interface.get("device", "Неизвестно") + ip = interface.get("ip", "Неизвестно") + mac = interface.get("mac", "Неизвестно") + + reply_text += f"• {device}\n" + reply_text += f" └ IP: {ip}, MAC: {mac}\n" + + await message.edit_text(reply_text, parse_mode="HTML") + +async def load_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /load""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + message = await update.message.reply_text("⏳ Получение информации о нагрузке системы...") + + if not synology_api.is_online(): + await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о нагрузке системы.", parse_mode="HTML") + return + + try: + system_load = synology_api.get_system_load() + + if not system_load: + await message.edit_text("❌ Ошибка получения информации о нагрузке системы\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML") + return + except Exception as e: + await message.edit_text(f"❌ Ошибка получения информации о нагрузке системы\n\nПричина: {str(e)}", parse_mode="HTML") + return + + # Формируем сообщение о нагрузке системы + cpu_load = system_load.get("cpu_load", 0) + memory = system_load.get("memory", {}) + memory_usage = memory.get("usage_percent", 0) + + reply_text = f"📈 Текущая нагрузка Synology NAS\n\n" + reply_text += f"Загрузка CPU: {cpu_load}%\n" + reply_text += f"Загрузка памяти: {memory_usage}%\n\n" + + # Добавляем информацию о сетевой активности + network = system_load.get("network", {}) + if network: + reply_text += "Сетевая активность:\n" + for device, stats in network.items(): + rx = int(stats.get("rx_bytes", 0)) / (1024**2) # МБ + tx = int(stats.get("tx_bytes", 0)) / (1024**2) # МБ + + reply_text += f"• {device}\n" + reply_text += f" └ Получено: {rx:.1f} МБ, Отправлено: {tx:.1f} МБ\n" + + await message.edit_text(reply_text, parse_mode="HTML") + +async def security_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /security""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + message = await update.message.reply_text("⏳ Получение информации о безопасности...") + + if not synology_api.is_online(): + await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о безопасности.", parse_mode="HTML") + return + + try: + security_info = synology_api.get_security_status() + + if not security_info.get("success", False): + await message.edit_text("❌ Ошибка получения информации о безопасности\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML") + return + except Exception as e: + await message.edit_text(f"❌ Ошибка получения информации о безопасности\n\nПричина: {str(e)}", parse_mode="HTML") + return + + # Формируем сообщение о безопасности + status = security_info.get("status", "unknown") + is_secure = security_info.get("is_secure", False) + last_check = security_info.get("last_check", "Неизвестно") + + status_emoji = "✅" if is_secure else "⚠️" + status_text = "Безопасно" if is_secure else "Требуется внимание" + + reply_text = f"🔐 Статус безопасности Synology NAS\n\n" + reply_text += f"Статус: {status_emoji} {status_text}\n" + reply_text += f"Подробности: {status}\n" + reply_text += f"Последняя проверка: {last_check}\n" + + await message.edit_text(reply_text, parse_mode="HTML") diff --git a/.history/src/handlers/extended_handlers_20250830095429.py b/.history/src/handlers/extended_handlers_20250830095429.py new file mode 100644 index 0000000..36ac9ca --- /dev/null +++ b/.history/src/handlers/extended_handlers_20250830095429.py @@ -0,0 +1,349 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Дополнительные обработчики команд для телеграм-бота +""" + +import logging +from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup +from telegram.ext import ContextTypes + +from src.config.config import ADMIN_USER_IDS +from src.api.synology import SynologyAPI + +logger = logging.getLogger(__name__) + +# Инициализация API Synology +synology_api = SynologyAPI() + +async def check_api_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /checkapi для диагностики проблем с API""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + message = await update.message.reply_text("⏳ Проверка доступных API Synology NAS...") + + from src.api.api_discovery import discover_available_apis + from src.config.config import ( + SYNOLOGY_HOST, + SYNOLOGY_PORT, + SYNOLOGY_SECURE, + SYNOLOGY_POWER_API, + SYNOLOGY_INFO_API, + SYNOLOGY_API_VERSION + ) + + # Формируем базовый URL + protocol = "https" if SYNOLOGY_SECURE else "http" + base_url = f"{protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi" + + # Получаем список доступных API + apis = discover_available_apis(base_url) + + if not apis: + await message.edit_text( + "❌ Не удалось получить список доступных API\n\n" + "Проверьте доступность NAS и сетевое подключение.", + parse_mode="HTML" + ) + return + + # Поиск API для управления питанием + power_apis = [name for name in apis.keys() if "power" in name.lower()] + system_info_apis = [name for name in apis.keys() if ("system" in name.lower() or "dsm" in name.lower()) and "info" in name.lower()] + reboot_apis = [name for name in apis.keys() if any(word in name.lower() for word in ["restart", "reboot", "power"])] + + # Формируем рекомендации + recommended_power_api = max(power_apis, key=lambda x: apis[x].get('maxVersion', 1)) if power_apis else "SYNO.Core.System" + recommended_info_api = max(system_info_apis, key=lambda x: apis[x].get('maxVersion', 1)) if system_info_apis else "SYNO.DSM.Info" + + # Формируем текст отчета + api_report = ( + f"✅ Найдено {len(apis)} доступных API\n\n" + f"API для управления питанием:\n" + f"{', '.join(power_apis[:5]) or 'Не найдены'}\n\n" + f"API для информации о системе:\n" + f"{', '.join(system_info_apis[:5]) or 'Не найдены'}\n\n" + f"API для перезагрузки:\n" + f"{', '.join(reboot_apis[:5]) or 'Не найдены'}\n\n" + f"Рекомендуемые настройки:\n" + f"Power API: {recommended_power_api}\n" + f"Info API: {recommended_info_api}\n\n" + f"Текущие настройки в конфигурации:\n" + f"SYNOLOGY_POWER_API = {SYNOLOGY_POWER_API}\n" + f"SYNOLOGY_INFO_API = {SYNOLOGY_INFO_API}\n" + f"SYNOLOGY_API_VERSION = {SYNOLOGY_API_VERSION}" + ) + + await message.edit_text(api_report, parse_mode="HTML") + +async def storage_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /storage""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + message = await update.message.reply_text("⏳ Получение информации о хранилище...") + + if not synology_api.is_online(): + await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о хранилище.", parse_mode="HTML") + return + + try: + storage_info = synology_api.get_storage_status() + + if not storage_info: + await message.edit_text("❌ Ошибка получения информации о хранилище\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML") + return + except Exception as e: + await message.edit_text(f"❌ Ошибка получения информации о хранилище\n\nПричина: {str(e)}", parse_mode="HTML") + return + + # Формируем сообщение о состоянии хранилища + summary = storage_info.get("summary", {}) + total_size_gb = summary.get("total_space_gb", 0) + total_used_gb = summary.get("used_space_gb", 0) + free_space_gb = summary.get("free_space_gb", 0) + usage_percent = summary.get("usage_percent", 0) + + reply_text = f"📊 Информация о хранилище Synology NAS\n\n" + reply_text += f"Общий размер: {total_size_gb:.2f} ГБ\n" + reply_text += f"Использовано: {total_used_gb:.2f} ГБ ({usage_percent:.1f}%)\n" + reply_text += f"Свободно: {free_space_gb:.2f} ГБ\n\n" + + # Добавляем информацию о томах + volumes = storage_info.get("volumes", []) + if volumes: + reply_text += "Тома:\n" + for volume in volumes: + name = volume.get("name", "Неизвестно") + status = volume.get("status", "Неизвестно") + size = volume.get("size", 0) + used_size = volume.get("used_size", 0) + size_gb = size / (1024**3) + used_gb = used_size / (1024**3) + percent = round((used_size / size) * 100, 1) if size > 0 else 0 + + reply_text += f"• {name} ({status})\n" + reply_text += f" └ {used_gb:.1f} ГБ из {size_gb:.1f} ГБ ({percent}%)\n" + + # Добавляем информацию о дисках + disks = storage_info.get("disks", []) + if disks: + reply_text += "\nДиски:\n" + for disk in disks: + name = disk.get("name", "Неизвестно") + model = disk.get("model", "Неизвестно") + status = disk.get("status", "Неизвестно") + temp = disk.get("temp", "?") + + reply_text += f"• {name} - {model}\n" + reply_text += f" └ Статус: {status}, Температура: {temp}°C\n" + + await message.edit_text(reply_text, parse_mode="HTML") + +async def shares_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /shares""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + message = await update.message.reply_text("⏳ Получение информации об общих папках...") + + if not synology_api.is_online(): + await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию об общих папках.", parse_mode="HTML") + return + + try: + shares = synology_api.get_shared_folders() + + if not shares: + await message.edit_text("❌ Ошибка получения информации об общих папках\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML") + return + except Exception as e: + await message.edit_text(f"❌ Ошибка получения информации об общих папках\n\nПричина: {str(e)}", parse_mode="HTML") + return + + # Формируем сообщение об общих папках + reply_text = f"📁 Общие папки Synology NAS\n\n" + + for share in shares: + name = share.get("name", "Неизвестно") + path = share.get("path", "Неизвестно") + desc = share.get("desc", "") + + reply_text += f"• {name}\n" + reply_text += f" └ Путь: {path}\n" + + if desc: + reply_text += f" └ Описание: {desc}\n" + + await message.edit_text(reply_text, parse_mode="HTML") + +async def system_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /system""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + message = await update.message.reply_text("⏳ Получение информации о системе...") + + if not synology_api.is_online(): + await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о системе.", parse_mode="HTML") + return + + try: + system_status = synology_api.get_system_status() + + if not system_status: + await message.edit_text("❌ Ошибка получения информации о системе", parse_mode="HTML") + return + + # Если получен статус с ошибкой + if system_status.get("status") == "error": + error_code = system_status.get("error_code", "неизвестно") + await message.edit_text(f"❌ Ошибка получения информации о системе\n\nКод ошибки API: {error_code}", parse_mode="HTML") + return + except Exception as e: + await message.edit_text(f"❌ Ошибка получения информации о системе\n\nПричина: {str(e)}", parse_mode="HTML") + return + + # Формируем сообщение о состоянии системы + model = system_status.get("model", "Неизвестно") + version = system_status.get("version", "Неизвестно") + serial = system_status.get("serial", "Неизвестно") + uptime_seconds = system_status.get("uptime", 0) + temperature = system_status.get("temperature", "?") + + # Преобразование времени работы в удобочитаемый формат + days, remainder = divmod(uptime_seconds, 86400) + hours, remainder = divmod(remainder, 3600) + minutes, seconds = divmod(remainder, 60) + uptime_str = f"{days}д {hours}ч {minutes}м {seconds}с" + + reply_text = f"🖥️ Информация о системе Synology NAS\n\n" + reply_text += f"Модель: {model}\n" + reply_text += f"Серийный номер: {serial}\n" + reply_text += f"Версия DSM: {version}\n" + reply_text += f"Время работы: {uptime_str}\n" + reply_text += f"Температура: {temperature}°C\n\n" + + # Добавляем информацию о CPU и памяти + memory = system_status.get("memory", {}) + total_memory_gb = memory.get("total_mb", 0) / 1024 + available_memory_gb = memory.get("available_mb", 0) / 1024 + memory_usage = memory.get("usage_percent", 0) + cpu_usage = system_status.get("cpu_usage", 0) + + reply_text += f"Загрузка CPU: {cpu_usage}%\n" + reply_text += f"Память: {total_memory_gb:.1f} ГБ (используется {memory_usage}%)\n" + reply_text += f"Доступно памяти: {available_memory_gb:.1f} ГБ\n\n" + + # Добавляем информацию о сетевых интерфейсах + network_info = system_status.get("network", []) + if network_info: + reply_text += "Сетевые интерфейсы:\n" + for interface in network_info: + device = interface.get("device", "Неизвестно") + ip = interface.get("ip", "Неизвестно") + mac = interface.get("mac", "Неизвестно") + + reply_text += f"• {device}\n" + reply_text += f" └ IP: {ip}, MAC: {mac}\n" + + await message.edit_text(reply_text, parse_mode="HTML") + +async def load_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /load""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + message = await update.message.reply_text("⏳ Получение информации о нагрузке системы...") + + if not synology_api.is_online(): + await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о нагрузке системы.", parse_mode="HTML") + return + + try: + system_load = synology_api.get_system_load() + + if not system_load: + await message.edit_text("❌ Ошибка получения информации о нагрузке системы\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML") + return + except Exception as e: + await message.edit_text(f"❌ Ошибка получения информации о нагрузке системы\n\nПричина: {str(e)}", parse_mode="HTML") + return + + # Формируем сообщение о нагрузке системы + cpu_load = system_load.get("cpu_load", 0) + memory = system_load.get("memory", {}) + memory_usage = memory.get("usage_percent", 0) + + reply_text = f"📈 Текущая нагрузка Synology NAS\n\n" + reply_text += f"Загрузка CPU: {cpu_load}%\n" + reply_text += f"Загрузка памяти: {memory_usage}%\n\n" + + # Добавляем информацию о сетевой активности + network = system_load.get("network", {}) + if network: + reply_text += "Сетевая активность:\n" + for device, stats in network.items(): + rx = int(stats.get("rx_bytes", 0)) / (1024**2) # МБ + tx = int(stats.get("tx_bytes", 0)) / (1024**2) # МБ + + reply_text += f"• {device}\n" + reply_text += f" └ Получено: {rx:.1f} МБ, Отправлено: {tx:.1f} МБ\n" + + await message.edit_text(reply_text, parse_mode="HTML") + +async def security_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /security""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + message = await update.message.reply_text("⏳ Получение информации о безопасности...") + + if not synology_api.is_online(): + await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о безопасности.", parse_mode="HTML") + return + + try: + security_info = synology_api.get_security_status() + + if not security_info.get("success", False): + await message.edit_text("❌ Ошибка получения информации о безопасности\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML") + return + except Exception as e: + await message.edit_text(f"❌ Ошибка получения информации о безопасности\n\nПричина: {str(e)}", parse_mode="HTML") + return + + # Формируем сообщение о безопасности + status = security_info.get("status", "unknown") + is_secure = security_info.get("is_secure", False) + last_check = security_info.get("last_check", "Неизвестно") + + status_emoji = "✅" if is_secure else "⚠️" + status_text = "Безопасно" if is_secure else "Требуется внимание" + + reply_text = f"🔐 Статус безопасности Synology NAS\n\n" + reply_text += f"Статус: {status_emoji} {status_text}\n" + reply_text += f"Подробности: {status}\n" + reply_text += f"Последняя проверка: {last_check}\n" + + await message.edit_text(reply_text, parse_mode="HTML") diff --git a/.history/src/handlers/extended_handlers_20250830095445.py b/.history/src/handlers/extended_handlers_20250830095445.py new file mode 100644 index 0000000..773bfa4 --- /dev/null +++ b/.history/src/handlers/extended_handlers_20250830095445.py @@ -0,0 +1,352 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Дополнительные обработчики команд для телеграм-бота +""" + +import logging +from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup +from telegram.ext import ContextTypes + +from src.config.config import ADMIN_USER_IDS +from src.api.synology import SynologyAPI + +logger = logging.getLogger(__name__) + +# Инициализация API Synology +synology_api = SynologyAPI() + +async def check_api_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /checkapi для диагностики проблем с API""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + message = await update.message.reply_text("⏳ Проверка доступных API Synology NAS...") + + from src.api.api_discovery import discover_available_apis + from src.config.config import ( + SYNOLOGY_HOST, + SYNOLOGY_PORT, + SYNOLOGY_SECURE, + SYNOLOGY_POWER_API, + SYNOLOGY_INFO_API, + SYNOLOGY_API_VERSION + ) + + # Формируем базовый URL + protocol = "https" if SYNOLOGY_SECURE else "http" + base_url = f"{protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi" + + # Получаем список доступных API + apis = discover_available_apis(base_url) + + if not apis: + await message.edit_text( + "❌ Не удалось получить список доступных API\n\n" + "Проверьте доступность NAS и сетевое подключение.", + parse_mode="HTML" + ) + return + + # Поиск API для управления питанием + power_apis = [name for name in apis.keys() if "power" in name.lower()] + system_info_apis = [name for name in apis.keys() if ("system" in name.lower() or "dsm" in name.lower()) and "info" in name.lower()] + reboot_apis = [name for name in apis.keys() if any(word in name.lower() for word in ["restart", "reboot", "power"])] + + # Формируем рекомендации + recommended_power_api = max(power_apis, key=lambda x: apis[x].get('maxVersion', 1)) if power_apis else "SYNO.Core.System" + recommended_info_api = max(system_info_apis, key=lambda x: apis[x].get('maxVersion', 1)) if system_info_apis else "SYNO.DSM.Info" + + # Формируем текст отчета + api_report = ( + f"✅ Найдено {len(apis)} доступных API\n\n" + f"API для управления питанием:\n" + f"{', '.join(power_apis[:5]) or 'Не найдены'}\n\n" + f"API для информации о системе:\n" + f"{', '.join(system_info_apis[:5]) or 'Не найдены'}\n\n" + f"API для перезагрузки:\n" + f"{', '.join(reboot_apis[:5]) or 'Не найдены'}\n\n" + f"Рекомендуемые настройки:\n" + f"Power API: {recommended_power_api}\n" + f"Info API: {recommended_info_api}\n\n" + f"Текущие настройки в конфигурации:\n" + f"SYNOLOGY_POWER_API = {SYNOLOGY_POWER_API}\n" + f"SYNOLOGY_INFO_API = {SYNOLOGY_INFO_API}\n" + f"SYNOLOGY_API_VERSION = {SYNOLOGY_API_VERSION}" + ) + + await message.edit_text(api_report, parse_mode="HTML") + +async def storage_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /storage""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + message = await update.message.reply_text("⏳ Получение информации о хранилище...") + + if not synology_api.is_online(): + await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о хранилище.", parse_mode="HTML") + return + + try: + storage_info = synology_api.get_storage_status() + + if not storage_info: + await message.edit_text("❌ Ошибка получения информации о хранилище\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML") + return + except Exception as e: + await message.edit_text(f"❌ Ошибка получения информации о хранилище\n\nПричина: {str(e)}", parse_mode="HTML") + return + + # Формируем сообщение о состоянии хранилища + summary = storage_info.get("summary", {}) + total_size_gb = summary.get("total_space_gb", 0) + total_used_gb = summary.get("used_space_gb", 0) + free_space_gb = summary.get("free_space_gb", 0) + usage_percent = summary.get("usage_percent", 0) + + reply_text = f"📊 Информация о хранилище Synology NAS\n\n" + reply_text += f"Общий размер: {total_size_gb:.2f} ГБ\n" + reply_text += f"Использовано: {total_used_gb:.2f} ГБ ({usage_percent:.1f}%)\n" + reply_text += f"Свободно: {free_space_gb:.2f} ГБ\n\n" + + # Добавляем информацию о томах + volumes = storage_info.get("volumes", []) + if volumes: + reply_text += "Тома:\n" + for volume in volumes: + name = volume.get("name", "Неизвестно") + status = volume.get("status", "Неизвестно") + size = volume.get("size", 0) + used_size = volume.get("used_size", 0) + size_gb = size / (1024**3) + used_gb = used_size / (1024**3) + percent = round((used_size / size) * 100, 1) if size > 0 else 0 + + reply_text += f"• {name} ({status})\n" + reply_text += f" └ {used_gb:.1f} ГБ из {size_gb:.1f} ГБ ({percent}%)\n" + + # Добавляем информацию о дисках + disks = storage_info.get("disks", []) + if disks: + reply_text += "\nДиски:\n" + for disk in disks: + name = disk.get("name", "Неизвестно") + model = disk.get("model", "Неизвестно") + status = disk.get("status", "Неизвестно") + temp = disk.get("temp", "?") + + reply_text += f"• {name} - {model}\n" + reply_text += f" └ Статус: {status}, Температура: {temp}°C\n" + + await message.edit_text(reply_text, parse_mode="HTML") + +async def shares_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /shares""" + if not update.message or not update.effective_user: + return + + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + message = await update.message.reply_text("⏳ Получение информации об общих папках...") + + if not synology_api.is_online(): + await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию об общих папках.", parse_mode="HTML") + return + + try: + shares = synology_api.get_shared_folders() + + if not shares: + await message.edit_text("❌ Ошибка получения информации об общих папках\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML") + return + except Exception as e: + await message.edit_text(f"❌ Ошибка получения информации об общих папках\n\nПричина: {str(e)}", parse_mode="HTML") + return + + # Формируем сообщение об общих папках + reply_text = f"📁 Общие папки Synology NAS\n\n" + + for share in shares: + name = share.get("name", "Неизвестно") + path = share.get("path", "Неизвестно") + desc = share.get("desc", "") + + reply_text += f"• {name}\n" + reply_text += f" └ Путь: {path}\n" + + if desc: + reply_text += f" └ Описание: {desc}\n" + + await message.edit_text(reply_text, parse_mode="HTML") + +async def system_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /system""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + message = await update.message.reply_text("⏳ Получение информации о системе...") + + if not synology_api.is_online(): + await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о системе.", parse_mode="HTML") + return + + try: + system_status = synology_api.get_system_status() + + if not system_status: + await message.edit_text("❌ Ошибка получения информации о системе", parse_mode="HTML") + return + + # Если получен статус с ошибкой + if system_status.get("status") == "error": + error_code = system_status.get("error_code", "неизвестно") + await message.edit_text(f"❌ Ошибка получения информации о системе\n\nКод ошибки API: {error_code}", parse_mode="HTML") + return + except Exception as e: + await message.edit_text(f"❌ Ошибка получения информации о системе\n\nПричина: {str(e)}", parse_mode="HTML") + return + + # Формируем сообщение о состоянии системы + model = system_status.get("model", "Неизвестно") + version = system_status.get("version", "Неизвестно") + serial = system_status.get("serial", "Неизвестно") + uptime_seconds = system_status.get("uptime", 0) + temperature = system_status.get("temperature", "?") + + # Преобразование времени работы в удобочитаемый формат + days, remainder = divmod(uptime_seconds, 86400) + hours, remainder = divmod(remainder, 3600) + minutes, seconds = divmod(remainder, 60) + uptime_str = f"{days}д {hours}ч {minutes}м {seconds}с" + + reply_text = f"🖥️ Информация о системе Synology NAS\n\n" + reply_text += f"Модель: {model}\n" + reply_text += f"Серийный номер: {serial}\n" + reply_text += f"Версия DSM: {version}\n" + reply_text += f"Время работы: {uptime_str}\n" + reply_text += f"Температура: {temperature}°C\n\n" + + # Добавляем информацию о CPU и памяти + memory = system_status.get("memory", {}) + total_memory_gb = memory.get("total_mb", 0) / 1024 + available_memory_gb = memory.get("available_mb", 0) / 1024 + memory_usage = memory.get("usage_percent", 0) + cpu_usage = system_status.get("cpu_usage", 0) + + reply_text += f"Загрузка CPU: {cpu_usage}%\n" + reply_text += f"Память: {total_memory_gb:.1f} ГБ (используется {memory_usage}%)\n" + reply_text += f"Доступно памяти: {available_memory_gb:.1f} ГБ\n\n" + + # Добавляем информацию о сетевых интерфейсах + network_info = system_status.get("network", []) + if network_info: + reply_text += "Сетевые интерфейсы:\n" + for interface in network_info: + device = interface.get("device", "Неизвестно") + ip = interface.get("ip", "Неизвестно") + mac = interface.get("mac", "Неизвестно") + + reply_text += f"• {device}\n" + reply_text += f" └ IP: {ip}, MAC: {mac}\n" + + await message.edit_text(reply_text, parse_mode="HTML") + +async def load_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /load""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + message = await update.message.reply_text("⏳ Получение информации о нагрузке системы...") + + if not synology_api.is_online(): + await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о нагрузке системы.", parse_mode="HTML") + return + + try: + system_load = synology_api.get_system_load() + + if not system_load: + await message.edit_text("❌ Ошибка получения информации о нагрузке системы\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML") + return + except Exception as e: + await message.edit_text(f"❌ Ошибка получения информации о нагрузке системы\n\nПричина: {str(e)}", parse_mode="HTML") + return + + # Формируем сообщение о нагрузке системы + cpu_load = system_load.get("cpu_load", 0) + memory = system_load.get("memory", {}) + memory_usage = memory.get("usage_percent", 0) + + reply_text = f"📈 Текущая нагрузка Synology NAS\n\n" + reply_text += f"Загрузка CPU: {cpu_load}%\n" + reply_text += f"Загрузка памяти: {memory_usage}%\n\n" + + # Добавляем информацию о сетевой активности + network = system_load.get("network", {}) + if network: + reply_text += "Сетевая активность:\n" + for device, stats in network.items(): + rx = int(stats.get("rx_bytes", 0)) / (1024**2) # МБ + tx = int(stats.get("tx_bytes", 0)) / (1024**2) # МБ + + reply_text += f"• {device}\n" + reply_text += f" └ Получено: {rx:.1f} МБ, Отправлено: {tx:.1f} МБ\n" + + await message.edit_text(reply_text, parse_mode="HTML") + +async def security_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /security""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + message = await update.message.reply_text("⏳ Получение информации о безопасности...") + + if not synology_api.is_online(): + await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о безопасности.", parse_mode="HTML") + return + + try: + security_info = synology_api.get_security_status() + + if not security_info.get("success", False): + await message.edit_text("❌ Ошибка получения информации о безопасности\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML") + return + except Exception as e: + await message.edit_text(f"❌ Ошибка получения информации о безопасности\n\nПричина: {str(e)}", parse_mode="HTML") + return + + # Формируем сообщение о безопасности + status = security_info.get("status", "unknown") + is_secure = security_info.get("is_secure", False) + last_check = security_info.get("last_check", "Неизвестно") + + status_emoji = "✅" if is_secure else "⚠️" + status_text = "Безопасно" if is_secure else "Требуется внимание" + + reply_text = f"🔐 Статус безопасности Synology NAS\n\n" + reply_text += f"Статус: {status_emoji} {status_text}\n" + reply_text += f"Подробности: {status}\n" + reply_text += f"Последняя проверка: {last_check}\n" + + await message.edit_text(reply_text, parse_mode="HTML") diff --git a/.history/src/handlers/extended_handlers_20250830095502.py b/.history/src/handlers/extended_handlers_20250830095502.py new file mode 100644 index 0000000..7c9a0a4 --- /dev/null +++ b/.history/src/handlers/extended_handlers_20250830095502.py @@ -0,0 +1,355 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Дополнительные обработчики команд для телеграм-бота +""" + +import logging +from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup +from telegram.ext import ContextTypes + +from src.config.config import ADMIN_USER_IDS +from src.api.synology import SynologyAPI + +logger = logging.getLogger(__name__) + +# Инициализация API Synology +synology_api = SynologyAPI() + +async def check_api_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /checkapi для диагностики проблем с API""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + message = await update.message.reply_text("⏳ Проверка доступных API Synology NAS...") + + from src.api.api_discovery import discover_available_apis + from src.config.config import ( + SYNOLOGY_HOST, + SYNOLOGY_PORT, + SYNOLOGY_SECURE, + SYNOLOGY_POWER_API, + SYNOLOGY_INFO_API, + SYNOLOGY_API_VERSION + ) + + # Формируем базовый URL + protocol = "https" if SYNOLOGY_SECURE else "http" + base_url = f"{protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi" + + # Получаем список доступных API + apis = discover_available_apis(base_url) + + if not apis: + await message.edit_text( + "❌ Не удалось получить список доступных API\n\n" + "Проверьте доступность NAS и сетевое подключение.", + parse_mode="HTML" + ) + return + + # Поиск API для управления питанием + power_apis = [name for name in apis.keys() if "power" in name.lower()] + system_info_apis = [name for name in apis.keys() if ("system" in name.lower() or "dsm" in name.lower()) and "info" in name.lower()] + reboot_apis = [name for name in apis.keys() if any(word in name.lower() for word in ["restart", "reboot", "power"])] + + # Формируем рекомендации + recommended_power_api = max(power_apis, key=lambda x: apis[x].get('maxVersion', 1)) if power_apis else "SYNO.Core.System" + recommended_info_api = max(system_info_apis, key=lambda x: apis[x].get('maxVersion', 1)) if system_info_apis else "SYNO.DSM.Info" + + # Формируем текст отчета + api_report = ( + f"✅ Найдено {len(apis)} доступных API\n\n" + f"API для управления питанием:\n" + f"{', '.join(power_apis[:5]) or 'Не найдены'}\n\n" + f"API для информации о системе:\n" + f"{', '.join(system_info_apis[:5]) or 'Не найдены'}\n\n" + f"API для перезагрузки:\n" + f"{', '.join(reboot_apis[:5]) or 'Не найдены'}\n\n" + f"Рекомендуемые настройки:\n" + f"Power API: {recommended_power_api}\n" + f"Info API: {recommended_info_api}\n\n" + f"Текущие настройки в конфигурации:\n" + f"SYNOLOGY_POWER_API = {SYNOLOGY_POWER_API}\n" + f"SYNOLOGY_INFO_API = {SYNOLOGY_INFO_API}\n" + f"SYNOLOGY_API_VERSION = {SYNOLOGY_API_VERSION}" + ) + + await message.edit_text(api_report, parse_mode="HTML") + +async def storage_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /storage""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + message = await update.message.reply_text("⏳ Получение информации о хранилище...") + + if not synology_api.is_online(): + await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о хранилище.", parse_mode="HTML") + return + + try: + storage_info = synology_api.get_storage_status() + + if not storage_info: + await message.edit_text("❌ Ошибка получения информации о хранилище\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML") + return + except Exception as e: + await message.edit_text(f"❌ Ошибка получения информации о хранилище\n\nПричина: {str(e)}", parse_mode="HTML") + return + + # Формируем сообщение о состоянии хранилища + summary = storage_info.get("summary", {}) + total_size_gb = summary.get("total_space_gb", 0) + total_used_gb = summary.get("used_space_gb", 0) + free_space_gb = summary.get("free_space_gb", 0) + usage_percent = summary.get("usage_percent", 0) + + reply_text = f"📊 Информация о хранилище Synology NAS\n\n" + reply_text += f"Общий размер: {total_size_gb:.2f} ГБ\n" + reply_text += f"Использовано: {total_used_gb:.2f} ГБ ({usage_percent:.1f}%)\n" + reply_text += f"Свободно: {free_space_gb:.2f} ГБ\n\n" + + # Добавляем информацию о томах + volumes = storage_info.get("volumes", []) + if volumes: + reply_text += "Тома:\n" + for volume in volumes: + name = volume.get("name", "Неизвестно") + status = volume.get("status", "Неизвестно") + size = volume.get("size", 0) + used_size = volume.get("used_size", 0) + size_gb = size / (1024**3) + used_gb = used_size / (1024**3) + percent = round((used_size / size) * 100, 1) if size > 0 else 0 + + reply_text += f"• {name} ({status})\n" + reply_text += f" └ {used_gb:.1f} ГБ из {size_gb:.1f} ГБ ({percent}%)\n" + + # Добавляем информацию о дисках + disks = storage_info.get("disks", []) + if disks: + reply_text += "\nДиски:\n" + for disk in disks: + name = disk.get("name", "Неизвестно") + model = disk.get("model", "Неизвестно") + status = disk.get("status", "Неизвестно") + temp = disk.get("temp", "?") + + reply_text += f"• {name} - {model}\n" + reply_text += f" └ Статус: {status}, Температура: {temp}°C\n" + + await message.edit_text(reply_text, parse_mode="HTML") + +async def shares_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /shares""" + if not update.message or not update.effective_user: + return + + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + message = await update.message.reply_text("⏳ Получение информации об общих папках...") + + if not synology_api.is_online(): + await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию об общих папках.", parse_mode="HTML") + return + + try: + shares = synology_api.get_shared_folders() + + if not shares: + await message.edit_text("❌ Ошибка получения информации об общих папках\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML") + return + except Exception as e: + await message.edit_text(f"❌ Ошибка получения информации об общих папках\n\nПричина: {str(e)}", parse_mode="HTML") + return + + # Формируем сообщение об общих папках + reply_text = f"📁 Общие папки Synology NAS\n\n" + + for share in shares: + name = share.get("name", "Неизвестно") + path = share.get("path", "Неизвестно") + desc = share.get("desc", "") + + reply_text += f"• {name}\n" + reply_text += f" └ Путь: {path}\n" + + if desc: + reply_text += f" └ Описание: {desc}\n" + + await message.edit_text(reply_text, parse_mode="HTML") + +async def system_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /system""" + if not update.message or not update.effective_user: + return + + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + message = await update.message.reply_text("⏳ Получение информации о системе...") + + if not synology_api.is_online(): + await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о системе.", parse_mode="HTML") + return + + try: + system_status = synology_api.get_system_status() + + if not system_status: + await message.edit_text("❌ Ошибка получения информации о системе", parse_mode="HTML") + return + + # Если получен статус с ошибкой + if system_status.get("status") == "error": + error_code = system_status.get("error_code", "неизвестно") + await message.edit_text(f"❌ Ошибка получения информации о системе\n\nКод ошибки API: {error_code}", parse_mode="HTML") + return + except Exception as e: + await message.edit_text(f"❌ Ошибка получения информации о системе\n\nПричина: {str(e)}", parse_mode="HTML") + return + + # Формируем сообщение о состоянии системы + model = system_status.get("model", "Неизвестно") + version = system_status.get("version", "Неизвестно") + serial = system_status.get("serial", "Неизвестно") + uptime_seconds = system_status.get("uptime", 0) + temperature = system_status.get("temperature", "?") + + # Преобразование времени работы в удобочитаемый формат + days, remainder = divmod(uptime_seconds, 86400) + hours, remainder = divmod(remainder, 3600) + minutes, seconds = divmod(remainder, 60) + uptime_str = f"{days}д {hours}ч {minutes}м {seconds}с" + + reply_text = f"🖥️ Информация о системе Synology NAS\n\n" + reply_text += f"Модель: {model}\n" + reply_text += f"Серийный номер: {serial}\n" + reply_text += f"Версия DSM: {version}\n" + reply_text += f"Время работы: {uptime_str}\n" + reply_text += f"Температура: {temperature}°C\n\n" + + # Добавляем информацию о CPU и памяти + memory = system_status.get("memory", {}) + total_memory_gb = memory.get("total_mb", 0) / 1024 + available_memory_gb = memory.get("available_mb", 0) / 1024 + memory_usage = memory.get("usage_percent", 0) + cpu_usage = system_status.get("cpu_usage", 0) + + reply_text += f"Загрузка CPU: {cpu_usage}%\n" + reply_text += f"Память: {total_memory_gb:.1f} ГБ (используется {memory_usage}%)\n" + reply_text += f"Доступно памяти: {available_memory_gb:.1f} ГБ\n\n" + + # Добавляем информацию о сетевых интерфейсах + network_info = system_status.get("network", []) + if network_info: + reply_text += "Сетевые интерфейсы:\n" + for interface in network_info: + device = interface.get("device", "Неизвестно") + ip = interface.get("ip", "Неизвестно") + mac = interface.get("mac", "Неизвестно") + + reply_text += f"• {device}\n" + reply_text += f" └ IP: {ip}, MAC: {mac}\n" + + await message.edit_text(reply_text, parse_mode="HTML") + +async def load_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /load""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + message = await update.message.reply_text("⏳ Получение информации о нагрузке системы...") + + if not synology_api.is_online(): + await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о нагрузке системы.", parse_mode="HTML") + return + + try: + system_load = synology_api.get_system_load() + + if not system_load: + await message.edit_text("❌ Ошибка получения информации о нагрузке системы\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML") + return + except Exception as e: + await message.edit_text(f"❌ Ошибка получения информации о нагрузке системы\n\nПричина: {str(e)}", parse_mode="HTML") + return + + # Формируем сообщение о нагрузке системы + cpu_load = system_load.get("cpu_load", 0) + memory = system_load.get("memory", {}) + memory_usage = memory.get("usage_percent", 0) + + reply_text = f"📈 Текущая нагрузка Synology NAS\n\n" + reply_text += f"Загрузка CPU: {cpu_load}%\n" + reply_text += f"Загрузка памяти: {memory_usage}%\n\n" + + # Добавляем информацию о сетевой активности + network = system_load.get("network", {}) + if network: + reply_text += "Сетевая активность:\n" + for device, stats in network.items(): + rx = int(stats.get("rx_bytes", 0)) / (1024**2) # МБ + tx = int(stats.get("tx_bytes", 0)) / (1024**2) # МБ + + reply_text += f"• {device}\n" + reply_text += f" └ Получено: {rx:.1f} МБ, Отправлено: {tx:.1f} МБ\n" + + await message.edit_text(reply_text, parse_mode="HTML") + +async def security_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /security""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + message = await update.message.reply_text("⏳ Получение информации о безопасности...") + + if not synology_api.is_online(): + await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о безопасности.", parse_mode="HTML") + return + + try: + security_info = synology_api.get_security_status() + + if not security_info.get("success", False): + await message.edit_text("❌ Ошибка получения информации о безопасности\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML") + return + except Exception as e: + await message.edit_text(f"❌ Ошибка получения информации о безопасности\n\nПричина: {str(e)}", parse_mode="HTML") + return + + # Формируем сообщение о безопасности + status = security_info.get("status", "unknown") + is_secure = security_info.get("is_secure", False) + last_check = security_info.get("last_check", "Неизвестно") + + status_emoji = "✅" if is_secure else "⚠️" + status_text = "Безопасно" if is_secure else "Требуется внимание" + + reply_text = f"🔐 Статус безопасности Synology NAS\n\n" + reply_text += f"Статус: {status_emoji} {status_text}\n" + reply_text += f"Подробности: {status}\n" + reply_text += f"Последняя проверка: {last_check}\n" + + await message.edit_text(reply_text, parse_mode="HTML") diff --git a/.history/src/handlers/extended_handlers_20250830095518.py b/.history/src/handlers/extended_handlers_20250830095518.py new file mode 100644 index 0000000..4fc38c8 --- /dev/null +++ b/.history/src/handlers/extended_handlers_20250830095518.py @@ -0,0 +1,358 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Дополнительные обработчики команд для телеграм-бота +""" + +import logging +from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup +from telegram.ext import ContextTypes + +from src.config.config import ADMIN_USER_IDS +from src.api.synology import SynologyAPI + +logger = logging.getLogger(__name__) + +# Инициализация API Synology +synology_api = SynologyAPI() + +async def check_api_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /checkapi для диагностики проблем с API""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + message = await update.message.reply_text("⏳ Проверка доступных API Synology NAS...") + + from src.api.api_discovery import discover_available_apis + from src.config.config import ( + SYNOLOGY_HOST, + SYNOLOGY_PORT, + SYNOLOGY_SECURE, + SYNOLOGY_POWER_API, + SYNOLOGY_INFO_API, + SYNOLOGY_API_VERSION + ) + + # Формируем базовый URL + protocol = "https" if SYNOLOGY_SECURE else "http" + base_url = f"{protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi" + + # Получаем список доступных API + apis = discover_available_apis(base_url) + + if not apis: + await message.edit_text( + "❌ Не удалось получить список доступных API\n\n" + "Проверьте доступность NAS и сетевое подключение.", + parse_mode="HTML" + ) + return + + # Поиск API для управления питанием + power_apis = [name for name in apis.keys() if "power" in name.lower()] + system_info_apis = [name for name in apis.keys() if ("system" in name.lower() or "dsm" in name.lower()) and "info" in name.lower()] + reboot_apis = [name for name in apis.keys() if any(word in name.lower() for word in ["restart", "reboot", "power"])] + + # Формируем рекомендации + recommended_power_api = max(power_apis, key=lambda x: apis[x].get('maxVersion', 1)) if power_apis else "SYNO.Core.System" + recommended_info_api = max(system_info_apis, key=lambda x: apis[x].get('maxVersion', 1)) if system_info_apis else "SYNO.DSM.Info" + + # Формируем текст отчета + api_report = ( + f"✅ Найдено {len(apis)} доступных API\n\n" + f"API для управления питанием:\n" + f"{', '.join(power_apis[:5]) or 'Не найдены'}\n\n" + f"API для информации о системе:\n" + f"{', '.join(system_info_apis[:5]) or 'Не найдены'}\n\n" + f"API для перезагрузки:\n" + f"{', '.join(reboot_apis[:5]) or 'Не найдены'}\n\n" + f"Рекомендуемые настройки:\n" + f"Power API: {recommended_power_api}\n" + f"Info API: {recommended_info_api}\n\n" + f"Текущие настройки в конфигурации:\n" + f"SYNOLOGY_POWER_API = {SYNOLOGY_POWER_API}\n" + f"SYNOLOGY_INFO_API = {SYNOLOGY_INFO_API}\n" + f"SYNOLOGY_API_VERSION = {SYNOLOGY_API_VERSION}" + ) + + await message.edit_text(api_report, parse_mode="HTML") + +async def storage_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /storage""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + message = await update.message.reply_text("⏳ Получение информации о хранилище...") + + if not synology_api.is_online(): + await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о хранилище.", parse_mode="HTML") + return + + try: + storage_info = synology_api.get_storage_status() + + if not storage_info: + await message.edit_text("❌ Ошибка получения информации о хранилище\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML") + return + except Exception as e: + await message.edit_text(f"❌ Ошибка получения информации о хранилище\n\nПричина: {str(e)}", parse_mode="HTML") + return + + # Формируем сообщение о состоянии хранилища + summary = storage_info.get("summary", {}) + total_size_gb = summary.get("total_space_gb", 0) + total_used_gb = summary.get("used_space_gb", 0) + free_space_gb = summary.get("free_space_gb", 0) + usage_percent = summary.get("usage_percent", 0) + + reply_text = f"📊 Информация о хранилище Synology NAS\n\n" + reply_text += f"Общий размер: {total_size_gb:.2f} ГБ\n" + reply_text += f"Использовано: {total_used_gb:.2f} ГБ ({usage_percent:.1f}%)\n" + reply_text += f"Свободно: {free_space_gb:.2f} ГБ\n\n" + + # Добавляем информацию о томах + volumes = storage_info.get("volumes", []) + if volumes: + reply_text += "Тома:\n" + for volume in volumes: + name = volume.get("name", "Неизвестно") + status = volume.get("status", "Неизвестно") + size = volume.get("size", 0) + used_size = volume.get("used_size", 0) + size_gb = size / (1024**3) + used_gb = used_size / (1024**3) + percent = round((used_size / size) * 100, 1) if size > 0 else 0 + + reply_text += f"• {name} ({status})\n" + reply_text += f" └ {used_gb:.1f} ГБ из {size_gb:.1f} ГБ ({percent}%)\n" + + # Добавляем информацию о дисках + disks = storage_info.get("disks", []) + if disks: + reply_text += "\nДиски:\n" + for disk in disks: + name = disk.get("name", "Неизвестно") + model = disk.get("model", "Неизвестно") + status = disk.get("status", "Неизвестно") + temp = disk.get("temp", "?") + + reply_text += f"• {name} - {model}\n" + reply_text += f" └ Статус: {status}, Температура: {temp}°C\n" + + await message.edit_text(reply_text, parse_mode="HTML") + +async def shares_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /shares""" + if not update.message or not update.effective_user: + return + + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + message = await update.message.reply_text("⏳ Получение информации об общих папках...") + + if not synology_api.is_online(): + await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию об общих папках.", parse_mode="HTML") + return + + try: + shares = synology_api.get_shared_folders() + + if not shares: + await message.edit_text("❌ Ошибка получения информации об общих папках\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML") + return + except Exception as e: + await message.edit_text(f"❌ Ошибка получения информации об общих папках\n\nПричина: {str(e)}", parse_mode="HTML") + return + + # Формируем сообщение об общих папках + reply_text = f"📁 Общие папки Synology NAS\n\n" + + for share in shares: + name = share.get("name", "Неизвестно") + path = share.get("path", "Неизвестно") + desc = share.get("desc", "") + + reply_text += f"• {name}\n" + reply_text += f" └ Путь: {path}\n" + + if desc: + reply_text += f" └ Описание: {desc}\n" + + await message.edit_text(reply_text, parse_mode="HTML") + +async def system_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /system""" + if not update.message or not update.effective_user: + return + + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + message = await update.message.reply_text("⏳ Получение информации о системе...") + + if not synology_api.is_online(): + await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о системе.", parse_mode="HTML") + return + + try: + system_status = synology_api.get_system_status() + + if not system_status: + await message.edit_text("❌ Ошибка получения информации о системе", parse_mode="HTML") + return + + # Если получен статус с ошибкой + if system_status.get("status") == "error": + error_code = system_status.get("error_code", "неизвестно") + await message.edit_text(f"❌ Ошибка получения информации о системе\n\nКод ошибки API: {error_code}", parse_mode="HTML") + return + except Exception as e: + await message.edit_text(f"❌ Ошибка получения информации о системе\n\nПричина: {str(e)}", parse_mode="HTML") + return + + # Формируем сообщение о состоянии системы + model = system_status.get("model", "Неизвестно") + version = system_status.get("version", "Неизвестно") + serial = system_status.get("serial", "Неизвестно") + uptime_seconds = system_status.get("uptime", 0) + temperature = system_status.get("temperature", "?") + + # Преобразование времени работы в удобочитаемый формат + days, remainder = divmod(uptime_seconds, 86400) + hours, remainder = divmod(remainder, 3600) + minutes, seconds = divmod(remainder, 60) + uptime_str = f"{days}д {hours}ч {minutes}м {seconds}с" + + reply_text = f"🖥️ Информация о системе Synology NAS\n\n" + reply_text += f"Модель: {model}\n" + reply_text += f"Серийный номер: {serial}\n" + reply_text += f"Версия DSM: {version}\n" + reply_text += f"Время работы: {uptime_str}\n" + reply_text += f"Температура: {temperature}°C\n\n" + + # Добавляем информацию о CPU и памяти + memory = system_status.get("memory", {}) + total_memory_gb = memory.get("total_mb", 0) / 1024 + available_memory_gb = memory.get("available_mb", 0) / 1024 + memory_usage = memory.get("usage_percent", 0) + cpu_usage = system_status.get("cpu_usage", 0) + + reply_text += f"Загрузка CPU: {cpu_usage}%\n" + reply_text += f"Память: {total_memory_gb:.1f} ГБ (используется {memory_usage}%)\n" + reply_text += f"Доступно памяти: {available_memory_gb:.1f} ГБ\n\n" + + # Добавляем информацию о сетевых интерфейсах + network_info = system_status.get("network", []) + if network_info: + reply_text += "Сетевые интерфейсы:\n" + for interface in network_info: + device = interface.get("device", "Неизвестно") + ip = interface.get("ip", "Неизвестно") + mac = interface.get("mac", "Неизвестно") + + reply_text += f"• {device}\n" + reply_text += f" └ IP: {ip}, MAC: {mac}\n" + + await message.edit_text(reply_text, parse_mode="HTML") + +async def load_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /load""" + if not update.message or not update.effective_user: + return + + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + message = await update.message.reply_text("⏳ Получение информации о нагрузке системы...") + + if not synology_api.is_online(): + await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о нагрузке системы.", parse_mode="HTML") + return + + try: + system_load = synology_api.get_system_load() + + if not system_load: + await message.edit_text("❌ Ошибка получения информации о нагрузке системы\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML") + return + except Exception as e: + await message.edit_text(f"❌ Ошибка получения информации о нагрузке системы\n\nПричина: {str(e)}", parse_mode="HTML") + return + + # Формируем сообщение о нагрузке системы + cpu_load = system_load.get("cpu_load", 0) + memory = system_load.get("memory", {}) + memory_usage = memory.get("usage_percent", 0) + + reply_text = f"📈 Текущая нагрузка Synology NAS\n\n" + reply_text += f"Загрузка CPU: {cpu_load}%\n" + reply_text += f"Загрузка памяти: {memory_usage}%\n\n" + + # Добавляем информацию о сетевой активности + network = system_load.get("network", {}) + if network: + reply_text += "Сетевая активность:\n" + for device, stats in network.items(): + rx = int(stats.get("rx_bytes", 0)) / (1024**2) # МБ + tx = int(stats.get("tx_bytes", 0)) / (1024**2) # МБ + + reply_text += f"• {device}\n" + reply_text += f" └ Получено: {rx:.1f} МБ, Отправлено: {tx:.1f} МБ\n" + + await message.edit_text(reply_text, parse_mode="HTML") + +async def security_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /security""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + message = await update.message.reply_text("⏳ Получение информации о безопасности...") + + if not synology_api.is_online(): + await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о безопасности.", parse_mode="HTML") + return + + try: + security_info = synology_api.get_security_status() + + if not security_info.get("success", False): + await message.edit_text("❌ Ошибка получения информации о безопасности\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML") + return + except Exception as e: + await message.edit_text(f"❌ Ошибка получения информации о безопасности\n\nПричина: {str(e)}", parse_mode="HTML") + return + + # Формируем сообщение о безопасности + status = security_info.get("status", "unknown") + is_secure = security_info.get("is_secure", False) + last_check = security_info.get("last_check", "Неизвестно") + + status_emoji = "✅" if is_secure else "⚠️" + status_text = "Безопасно" if is_secure else "Требуется внимание" + + reply_text = f"🔐 Статус безопасности Synology NAS\n\n" + reply_text += f"Статус: {status_emoji} {status_text}\n" + reply_text += f"Подробности: {status}\n" + reply_text += f"Последняя проверка: {last_check}\n" + + await message.edit_text(reply_text, parse_mode="HTML") diff --git a/.history/src/handlers/extended_handlers_20250830095533.py b/.history/src/handlers/extended_handlers_20250830095533.py new file mode 100644 index 0000000..44d29a0 --- /dev/null +++ b/.history/src/handlers/extended_handlers_20250830095533.py @@ -0,0 +1,361 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Дополнительные обработчики команд для телеграм-бота +""" + +import logging +from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup +from telegram.ext import ContextTypes + +from src.config.config import ADMIN_USER_IDS +from src.api.synology import SynologyAPI + +logger = logging.getLogger(__name__) + +# Инициализация API Synology +synology_api = SynologyAPI() + +async def check_api_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /checkapi для диагностики проблем с API""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + message = await update.message.reply_text("⏳ Проверка доступных API Synology NAS...") + + from src.api.api_discovery import discover_available_apis + from src.config.config import ( + SYNOLOGY_HOST, + SYNOLOGY_PORT, + SYNOLOGY_SECURE, + SYNOLOGY_POWER_API, + SYNOLOGY_INFO_API, + SYNOLOGY_API_VERSION + ) + + # Формируем базовый URL + protocol = "https" if SYNOLOGY_SECURE else "http" + base_url = f"{protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi" + + # Получаем список доступных API + apis = discover_available_apis(base_url) + + if not apis: + await message.edit_text( + "❌ Не удалось получить список доступных API\n\n" + "Проверьте доступность NAS и сетевое подключение.", + parse_mode="HTML" + ) + return + + # Поиск API для управления питанием + power_apis = [name for name in apis.keys() if "power" in name.lower()] + system_info_apis = [name for name in apis.keys() if ("system" in name.lower() or "dsm" in name.lower()) and "info" in name.lower()] + reboot_apis = [name for name in apis.keys() if any(word in name.lower() for word in ["restart", "reboot", "power"])] + + # Формируем рекомендации + recommended_power_api = max(power_apis, key=lambda x: apis[x].get('maxVersion', 1)) if power_apis else "SYNO.Core.System" + recommended_info_api = max(system_info_apis, key=lambda x: apis[x].get('maxVersion', 1)) if system_info_apis else "SYNO.DSM.Info" + + # Формируем текст отчета + api_report = ( + f"✅ Найдено {len(apis)} доступных API\n\n" + f"API для управления питанием:\n" + f"{', '.join(power_apis[:5]) or 'Не найдены'}\n\n" + f"API для информации о системе:\n" + f"{', '.join(system_info_apis[:5]) or 'Не найдены'}\n\n" + f"API для перезагрузки:\n" + f"{', '.join(reboot_apis[:5]) or 'Не найдены'}\n\n" + f"Рекомендуемые настройки:\n" + f"Power API: {recommended_power_api}\n" + f"Info API: {recommended_info_api}\n\n" + f"Текущие настройки в конфигурации:\n" + f"SYNOLOGY_POWER_API = {SYNOLOGY_POWER_API}\n" + f"SYNOLOGY_INFO_API = {SYNOLOGY_INFO_API}\n" + f"SYNOLOGY_API_VERSION = {SYNOLOGY_API_VERSION}" + ) + + await message.edit_text(api_report, parse_mode="HTML") + +async def storage_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /storage""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + message = await update.message.reply_text("⏳ Получение информации о хранилище...") + + if not synology_api.is_online(): + await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о хранилище.", parse_mode="HTML") + return + + try: + storage_info = synology_api.get_storage_status() + + if not storage_info: + await message.edit_text("❌ Ошибка получения информации о хранилище\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML") + return + except Exception as e: + await message.edit_text(f"❌ Ошибка получения информации о хранилище\n\nПричина: {str(e)}", parse_mode="HTML") + return + + # Формируем сообщение о состоянии хранилища + summary = storage_info.get("summary", {}) + total_size_gb = summary.get("total_space_gb", 0) + total_used_gb = summary.get("used_space_gb", 0) + free_space_gb = summary.get("free_space_gb", 0) + usage_percent = summary.get("usage_percent", 0) + + reply_text = f"📊 Информация о хранилище Synology NAS\n\n" + reply_text += f"Общий размер: {total_size_gb:.2f} ГБ\n" + reply_text += f"Использовано: {total_used_gb:.2f} ГБ ({usage_percent:.1f}%)\n" + reply_text += f"Свободно: {free_space_gb:.2f} ГБ\n\n" + + # Добавляем информацию о томах + volumes = storage_info.get("volumes", []) + if volumes: + reply_text += "Тома:\n" + for volume in volumes: + name = volume.get("name", "Неизвестно") + status = volume.get("status", "Неизвестно") + size = volume.get("size", 0) + used_size = volume.get("used_size", 0) + size_gb = size / (1024**3) + used_gb = used_size / (1024**3) + percent = round((used_size / size) * 100, 1) if size > 0 else 0 + + reply_text += f"• {name} ({status})\n" + reply_text += f" └ {used_gb:.1f} ГБ из {size_gb:.1f} ГБ ({percent}%)\n" + + # Добавляем информацию о дисках + disks = storage_info.get("disks", []) + if disks: + reply_text += "\nДиски:\n" + for disk in disks: + name = disk.get("name", "Неизвестно") + model = disk.get("model", "Неизвестно") + status = disk.get("status", "Неизвестно") + temp = disk.get("temp", "?") + + reply_text += f"• {name} - {model}\n" + reply_text += f" └ Статус: {status}, Температура: {temp}°C\n" + + await message.edit_text(reply_text, parse_mode="HTML") + +async def shares_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /shares""" + if not update.message or not update.effective_user: + return + + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + message = await update.message.reply_text("⏳ Получение информации об общих папках...") + + if not synology_api.is_online(): + await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию об общих папках.", parse_mode="HTML") + return + + try: + shares = synology_api.get_shared_folders() + + if not shares: + await message.edit_text("❌ Ошибка получения информации об общих папках\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML") + return + except Exception as e: + await message.edit_text(f"❌ Ошибка получения информации об общих папках\n\nПричина: {str(e)}", parse_mode="HTML") + return + + # Формируем сообщение об общих папках + reply_text = f"📁 Общие папки Synology NAS\n\n" + + for share in shares: + name = share.get("name", "Неизвестно") + path = share.get("path", "Неизвестно") + desc = share.get("desc", "") + + reply_text += f"• {name}\n" + reply_text += f" └ Путь: {path}\n" + + if desc: + reply_text += f" └ Описание: {desc}\n" + + await message.edit_text(reply_text, parse_mode="HTML") + +async def system_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /system""" + if not update.message or not update.effective_user: + return + + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + message = await update.message.reply_text("⏳ Получение информации о системе...") + + if not synology_api.is_online(): + await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о системе.", parse_mode="HTML") + return + + try: + system_status = synology_api.get_system_status() + + if not system_status: + await message.edit_text("❌ Ошибка получения информации о системе", parse_mode="HTML") + return + + # Если получен статус с ошибкой + if system_status.get("status") == "error": + error_code = system_status.get("error_code", "неизвестно") + await message.edit_text(f"❌ Ошибка получения информации о системе\n\nКод ошибки API: {error_code}", parse_mode="HTML") + return + except Exception as e: + await message.edit_text(f"❌ Ошибка получения информации о системе\n\nПричина: {str(e)}", parse_mode="HTML") + return + + # Формируем сообщение о состоянии системы + model = system_status.get("model", "Неизвестно") + version = system_status.get("version", "Неизвестно") + serial = system_status.get("serial", "Неизвестно") + uptime_seconds = system_status.get("uptime", 0) + temperature = system_status.get("temperature", "?") + + # Преобразование времени работы в удобочитаемый формат + days, remainder = divmod(uptime_seconds, 86400) + hours, remainder = divmod(remainder, 3600) + minutes, seconds = divmod(remainder, 60) + uptime_str = f"{days}д {hours}ч {minutes}м {seconds}с" + + reply_text = f"🖥️ Информация о системе Synology NAS\n\n" + reply_text += f"Модель: {model}\n" + reply_text += f"Серийный номер: {serial}\n" + reply_text += f"Версия DSM: {version}\n" + reply_text += f"Время работы: {uptime_str}\n" + reply_text += f"Температура: {temperature}°C\n\n" + + # Добавляем информацию о CPU и памяти + memory = system_status.get("memory", {}) + total_memory_gb = memory.get("total_mb", 0) / 1024 + available_memory_gb = memory.get("available_mb", 0) / 1024 + memory_usage = memory.get("usage_percent", 0) + cpu_usage = system_status.get("cpu_usage", 0) + + reply_text += f"Загрузка CPU: {cpu_usage}%\n" + reply_text += f"Память: {total_memory_gb:.1f} ГБ (используется {memory_usage}%)\n" + reply_text += f"Доступно памяти: {available_memory_gb:.1f} ГБ\n\n" + + # Добавляем информацию о сетевых интерфейсах + network_info = system_status.get("network", []) + if network_info: + reply_text += "Сетевые интерфейсы:\n" + for interface in network_info: + device = interface.get("device", "Неизвестно") + ip = interface.get("ip", "Неизвестно") + mac = interface.get("mac", "Неизвестно") + + reply_text += f"• {device}\n" + reply_text += f" └ IP: {ip}, MAC: {mac}\n" + + await message.edit_text(reply_text, parse_mode="HTML") + +async def load_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /load""" + if not update.message or not update.effective_user: + return + + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + message = await update.message.reply_text("⏳ Получение информации о нагрузке системы...") + + if not synology_api.is_online(): + await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о нагрузке системы.", parse_mode="HTML") + return + + try: + system_load = synology_api.get_system_load() + + if not system_load: + await message.edit_text("❌ Ошибка получения информации о нагрузке системы\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML") + return + except Exception as e: + await message.edit_text(f"❌ Ошибка получения информации о нагрузке системы\n\nПричина: {str(e)}", parse_mode="HTML") + return + + # Формируем сообщение о нагрузке системы + cpu_load = system_load.get("cpu_load", 0) + memory = system_load.get("memory", {}) + memory_usage = memory.get("usage_percent", 0) + + reply_text = f"📈 Текущая нагрузка Synology NAS\n\n" + reply_text += f"Загрузка CPU: {cpu_load}%\n" + reply_text += f"Загрузка памяти: {memory_usage}%\n\n" + + # Добавляем информацию о сетевой активности + network = system_load.get("network", {}) + if network: + reply_text += "Сетевая активность:\n" + for device, stats in network.items(): + rx = int(stats.get("rx_bytes", 0)) / (1024**2) # МБ + tx = int(stats.get("tx_bytes", 0)) / (1024**2) # МБ + + reply_text += f"• {device}\n" + reply_text += f" └ Получено: {rx:.1f} МБ, Отправлено: {tx:.1f} МБ\n" + + await message.edit_text(reply_text, parse_mode="HTML") + +async def security_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /security""" + if not update.message or not update.effective_user: + return + + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + message = await update.message.reply_text("⏳ Получение информации о безопасности...") + + if not synology_api.is_online(): + await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о безопасности.", parse_mode="HTML") + return + + try: + security_info = synology_api.get_security_status() + + if not security_info.get("success", False): + await message.edit_text("❌ Ошибка получения информации о безопасности\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML") + return + except Exception as e: + await message.edit_text(f"❌ Ошибка получения информации о безопасности\n\nПричина: {str(e)}", parse_mode="HTML") + return + + # Формируем сообщение о безопасности + status = security_info.get("status", "unknown") + is_secure = security_info.get("is_secure", False) + last_check = security_info.get("last_check", "Неизвестно") + + status_emoji = "✅" if is_secure else "⚠️" + status_text = "Безопасно" if is_secure else "Требуется внимание" + + reply_text = f"🔐 Статус безопасности Synology NAS\n\n" + reply_text += f"Статус: {status_emoji} {status_text}\n" + reply_text += f"Подробности: {status}\n" + reply_text += f"Последняя проверка: {last_check}\n" + + await message.edit_text(reply_text, parse_mode="HTML") diff --git a/.history/src/handlers/extended_handlers_20250830095550.py b/.history/src/handlers/extended_handlers_20250830095550.py new file mode 100644 index 0000000..e8fe930 --- /dev/null +++ b/.history/src/handlers/extended_handlers_20250830095550.py @@ -0,0 +1,364 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Дополнительные обработчики команд для телеграм-бота +""" + +import logging +from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup +from telegram.ext import ContextTypes + +from src.config.config import ADMIN_USER_IDS +from src.api.synology import SynologyAPI + +logger = logging.getLogger(__name__) + +# Инициализация API Synology +synology_api = SynologyAPI() + +async def check_api_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /checkapi для диагностики проблем с API""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + message = await update.message.reply_text("⏳ Проверка доступных API Synology NAS...") + + from src.api.api_discovery import discover_available_apis + from src.config.config import ( + SYNOLOGY_HOST, + SYNOLOGY_PORT, + SYNOLOGY_SECURE, + SYNOLOGY_POWER_API, + SYNOLOGY_INFO_API, + SYNOLOGY_API_VERSION + ) + + # Формируем базовый URL + protocol = "https" if SYNOLOGY_SECURE else "http" + base_url = f"{protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi" + + # Получаем список доступных API + apis = discover_available_apis(base_url) + + if not apis: + await message.edit_text( + "❌ Не удалось получить список доступных API\n\n" + "Проверьте доступность NAS и сетевое подключение.", + parse_mode="HTML" + ) + return + + # Поиск API для управления питанием + power_apis = [name for name in apis.keys() if "power" in name.lower()] + system_info_apis = [name for name in apis.keys() if ("system" in name.lower() or "dsm" in name.lower()) and "info" in name.lower()] + reboot_apis = [name for name in apis.keys() if any(word in name.lower() for word in ["restart", "reboot", "power"])] + + # Формируем рекомендации + recommended_power_api = max(power_apis, key=lambda x: apis[x].get('maxVersion', 1)) if power_apis else "SYNO.Core.System" + recommended_info_api = max(system_info_apis, key=lambda x: apis[x].get('maxVersion', 1)) if system_info_apis else "SYNO.DSM.Info" + + # Формируем текст отчета + api_report = ( + f"✅ Найдено {len(apis)} доступных API\n\n" + f"API для управления питанием:\n" + f"{', '.join(power_apis[:5]) or 'Не найдены'}\n\n" + f"API для информации о системе:\n" + f"{', '.join(system_info_apis[:5]) or 'Не найдены'}\n\n" + f"API для перезагрузки:\n" + f"{', '.join(reboot_apis[:5]) or 'Не найдены'}\n\n" + f"Рекомендуемые настройки:\n" + f"Power API: {recommended_power_api}\n" + f"Info API: {recommended_info_api}\n\n" + f"Текущие настройки в конфигурации:\n" + f"SYNOLOGY_POWER_API = {SYNOLOGY_POWER_API}\n" + f"SYNOLOGY_INFO_API = {SYNOLOGY_INFO_API}\n" + f"SYNOLOGY_API_VERSION = {SYNOLOGY_API_VERSION}" + ) + + await message.edit_text(api_report, parse_mode="HTML") + +async def storage_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /storage""" + if not update.message or not update.effective_user: + return + + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + message = await update.message.reply_text("⏳ Получение информации о хранилище...") + + if not synology_api.is_online(): + await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о хранилище.", parse_mode="HTML") + return + + try: + storage_info = synology_api.get_storage_status() + + if not storage_info: + await message.edit_text("❌ Ошибка получения информации о хранилище\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML") + return + except Exception as e: + await message.edit_text(f"❌ Ошибка получения информации о хранилище\n\nПричина: {str(e)}", parse_mode="HTML") + return + + # Формируем сообщение о состоянии хранилища + summary = storage_info.get("summary", {}) + total_size_gb = summary.get("total_space_gb", 0) + total_used_gb = summary.get("used_space_gb", 0) + free_space_gb = summary.get("free_space_gb", 0) + usage_percent = summary.get("usage_percent", 0) + + reply_text = f"📊 Информация о хранилище Synology NAS\n\n" + reply_text += f"Общий размер: {total_size_gb:.2f} ГБ\n" + reply_text += f"Использовано: {total_used_gb:.2f} ГБ ({usage_percent:.1f}%)\n" + reply_text += f"Свободно: {free_space_gb:.2f} ГБ\n\n" + + # Добавляем информацию о томах + volumes = storage_info.get("volumes", []) + if volumes: + reply_text += "Тома:\n" + for volume in volumes: + name = volume.get("name", "Неизвестно") + status = volume.get("status", "Неизвестно") + size = volume.get("size", 0) + used_size = volume.get("used_size", 0) + size_gb = size / (1024**3) + used_gb = used_size / (1024**3) + percent = round((used_size / size) * 100, 1) if size > 0 else 0 + + reply_text += f"• {name} ({status})\n" + reply_text += f" └ {used_gb:.1f} ГБ из {size_gb:.1f} ГБ ({percent}%)\n" + + # Добавляем информацию о дисках + disks = storage_info.get("disks", []) + if disks: + reply_text += "\nДиски:\n" + for disk in disks: + name = disk.get("name", "Неизвестно") + model = disk.get("model", "Неизвестно") + status = disk.get("status", "Неизвестно") + temp = disk.get("temp", "?") + + reply_text += f"• {name} - {model}\n" + reply_text += f" └ Статус: {status}, Температура: {temp}°C\n" + + await message.edit_text(reply_text, parse_mode="HTML") + +async def shares_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /shares""" + if not update.message or not update.effective_user: + return + + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + message = await update.message.reply_text("⏳ Получение информации об общих папках...") + + if not synology_api.is_online(): + await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию об общих папках.", parse_mode="HTML") + return + + try: + shares = synology_api.get_shared_folders() + + if not shares: + await message.edit_text("❌ Ошибка получения информации об общих папках\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML") + return + except Exception as e: + await message.edit_text(f"❌ Ошибка получения информации об общих папках\n\nПричина: {str(e)}", parse_mode="HTML") + return + + # Формируем сообщение об общих папках + reply_text = f"📁 Общие папки Synology NAS\n\n" + + for share in shares: + name = share.get("name", "Неизвестно") + path = share.get("path", "Неизвестно") + desc = share.get("desc", "") + + reply_text += f"• {name}\n" + reply_text += f" └ Путь: {path}\n" + + if desc: + reply_text += f" └ Описание: {desc}\n" + + await message.edit_text(reply_text, parse_mode="HTML") + +async def system_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /system""" + if not update.message or not update.effective_user: + return + + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + message = await update.message.reply_text("⏳ Получение информации о системе...") + + if not synology_api.is_online(): + await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о системе.", parse_mode="HTML") + return + + try: + system_status = synology_api.get_system_status() + + if not system_status: + await message.edit_text("❌ Ошибка получения информации о системе", parse_mode="HTML") + return + + # Если получен статус с ошибкой + if system_status.get("status") == "error": + error_code = system_status.get("error_code", "неизвестно") + await message.edit_text(f"❌ Ошибка получения информации о системе\n\nКод ошибки API: {error_code}", parse_mode="HTML") + return + except Exception as e: + await message.edit_text(f"❌ Ошибка получения информации о системе\n\nПричина: {str(e)}", parse_mode="HTML") + return + + # Формируем сообщение о состоянии системы + model = system_status.get("model", "Неизвестно") + version = system_status.get("version", "Неизвестно") + serial = system_status.get("serial", "Неизвестно") + uptime_seconds = system_status.get("uptime", 0) + temperature = system_status.get("temperature", "?") + + # Преобразование времени работы в удобочитаемый формат + days, remainder = divmod(uptime_seconds, 86400) + hours, remainder = divmod(remainder, 3600) + minutes, seconds = divmod(remainder, 60) + uptime_str = f"{days}д {hours}ч {minutes}м {seconds}с" + + reply_text = f"🖥️ Информация о системе Synology NAS\n\n" + reply_text += f"Модель: {model}\n" + reply_text += f"Серийный номер: {serial}\n" + reply_text += f"Версия DSM: {version}\n" + reply_text += f"Время работы: {uptime_str}\n" + reply_text += f"Температура: {temperature}°C\n\n" + + # Добавляем информацию о CPU и памяти + memory = system_status.get("memory", {}) + total_memory_gb = memory.get("total_mb", 0) / 1024 + available_memory_gb = memory.get("available_mb", 0) / 1024 + memory_usage = memory.get("usage_percent", 0) + cpu_usage = system_status.get("cpu_usage", 0) + + reply_text += f"Загрузка CPU: {cpu_usage}%\n" + reply_text += f"Память: {total_memory_gb:.1f} ГБ (используется {memory_usage}%)\n" + reply_text += f"Доступно памяти: {available_memory_gb:.1f} ГБ\n\n" + + # Добавляем информацию о сетевых интерфейсах + network_info = system_status.get("network", []) + if network_info: + reply_text += "Сетевые интерфейсы:\n" + for interface in network_info: + device = interface.get("device", "Неизвестно") + ip = interface.get("ip", "Неизвестно") + mac = interface.get("mac", "Неизвестно") + + reply_text += f"• {device}\n" + reply_text += f" └ IP: {ip}, MAC: {mac}\n" + + await message.edit_text(reply_text, parse_mode="HTML") + +async def load_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /load""" + if not update.message or not update.effective_user: + return + + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + message = await update.message.reply_text("⏳ Получение информации о нагрузке системы...") + + if not synology_api.is_online(): + await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о нагрузке системы.", parse_mode="HTML") + return + + try: + system_load = synology_api.get_system_load() + + if not system_load: + await message.edit_text("❌ Ошибка получения информации о нагрузке системы\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML") + return + except Exception as e: + await message.edit_text(f"❌ Ошибка получения информации о нагрузке системы\n\nПричина: {str(e)}", parse_mode="HTML") + return + + # Формируем сообщение о нагрузке системы + cpu_load = system_load.get("cpu_load", 0) + memory = system_load.get("memory", {}) + memory_usage = memory.get("usage_percent", 0) + + reply_text = f"📈 Текущая нагрузка Synology NAS\n\n" + reply_text += f"Загрузка CPU: {cpu_load}%\n" + reply_text += f"Загрузка памяти: {memory_usage}%\n\n" + + # Добавляем информацию о сетевой активности + network = system_load.get("network", {}) + if network: + reply_text += "Сетевая активность:\n" + for device, stats in network.items(): + rx = int(stats.get("rx_bytes", 0)) / (1024**2) # МБ + tx = int(stats.get("tx_bytes", 0)) / (1024**2) # МБ + + reply_text += f"• {device}\n" + reply_text += f" └ Получено: {rx:.1f} МБ, Отправлено: {tx:.1f} МБ\n" + + await message.edit_text(reply_text, parse_mode="HTML") + +async def security_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /security""" + if not update.message or not update.effective_user: + return + + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + message = await update.message.reply_text("⏳ Получение информации о безопасности...") + + if not synology_api.is_online(): + await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о безопасности.", parse_mode="HTML") + return + + try: + security_info = synology_api.get_security_status() + + if not security_info.get("success", False): + await message.edit_text("❌ Ошибка получения информации о безопасности\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML") + return + except Exception as e: + await message.edit_text(f"❌ Ошибка получения информации о безопасности\n\nПричина: {str(e)}", parse_mode="HTML") + return + + # Формируем сообщение о безопасности + status = security_info.get("status", "unknown") + is_secure = security_info.get("is_secure", False) + last_check = security_info.get("last_check", "Неизвестно") + + status_emoji = "✅" if is_secure else "⚠️" + status_text = "Безопасно" if is_secure else "Требуется внимание" + + reply_text = f"🔐 Статус безопасности Synology NAS\n\n" + reply_text += f"Статус: {status_emoji} {status_text}\n" + reply_text += f"Подробности: {status}\n" + reply_text += f"Последняя проверка: {last_check}\n" + + await message.edit_text(reply_text, parse_mode="HTML") diff --git a/.history/src/handlers/extended_handlers_20250830095606.py b/.history/src/handlers/extended_handlers_20250830095606.py new file mode 100644 index 0000000..ca60d52 --- /dev/null +++ b/.history/src/handlers/extended_handlers_20250830095606.py @@ -0,0 +1,367 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Дополнительные обработчики команд для телеграм-бота +""" + +import logging +from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup +from telegram.ext import ContextTypes + +from src.config.config import ADMIN_USER_IDS +from src.api.synology import SynologyAPI + +logger = logging.getLogger(__name__) + +# Инициализация API Synology +synology_api = SynologyAPI() + +async def check_api_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /checkapi для диагностики проблем с API""" + if not update.message or not update.effective_user: + return + + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + message = await update.message.reply_text("⏳ Проверка доступных API Synology NAS...") + + from src.api.api_discovery import discover_available_apis + from src.config.config import ( + SYNOLOGY_HOST, + SYNOLOGY_PORT, + SYNOLOGY_SECURE, + SYNOLOGY_POWER_API, + SYNOLOGY_INFO_API, + SYNOLOGY_API_VERSION + ) + + # Формируем базовый URL + protocol = "https" if SYNOLOGY_SECURE else "http" + base_url = f"{protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi" + + # Получаем список доступных API + apis = discover_available_apis(base_url) + + if not apis: + await message.edit_text( + "❌ Не удалось получить список доступных API\n\n" + "Проверьте доступность NAS и сетевое подключение.", + parse_mode="HTML" + ) + return + + # Поиск API для управления питанием + power_apis = [name for name in apis.keys() if "power" in name.lower()] + system_info_apis = [name for name in apis.keys() if ("system" in name.lower() or "dsm" in name.lower()) and "info" in name.lower()] + reboot_apis = [name for name in apis.keys() if any(word in name.lower() for word in ["restart", "reboot", "power"])] + + # Формируем рекомендации + recommended_power_api = max(power_apis, key=lambda x: apis[x].get('maxVersion', 1)) if power_apis else "SYNO.Core.System" + recommended_info_api = max(system_info_apis, key=lambda x: apis[x].get('maxVersion', 1)) if system_info_apis else "SYNO.DSM.Info" + + # Формируем текст отчета + api_report = ( + f"✅ Найдено {len(apis)} доступных API\n\n" + f"API для управления питанием:\n" + f"{', '.join(power_apis[:5]) or 'Не найдены'}\n\n" + f"API для информации о системе:\n" + f"{', '.join(system_info_apis[:5]) or 'Не найдены'}\n\n" + f"API для перезагрузки:\n" + f"{', '.join(reboot_apis[:5]) or 'Не найдены'}\n\n" + f"Рекомендуемые настройки:\n" + f"Power API: {recommended_power_api}\n" + f"Info API: {recommended_info_api}\n\n" + f"Текущие настройки в конфигурации:\n" + f"SYNOLOGY_POWER_API = {SYNOLOGY_POWER_API}\n" + f"SYNOLOGY_INFO_API = {SYNOLOGY_INFO_API}\n" + f"SYNOLOGY_API_VERSION = {SYNOLOGY_API_VERSION}" + ) + + await message.edit_text(api_report, parse_mode="HTML") + +async def storage_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /storage""" + if not update.message or not update.effective_user: + return + + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + message = await update.message.reply_text("⏳ Получение информации о хранилище...") + + if not synology_api.is_online(): + await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о хранилище.", parse_mode="HTML") + return + + try: + storage_info = synology_api.get_storage_status() + + if not storage_info: + await message.edit_text("❌ Ошибка получения информации о хранилище\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML") + return + except Exception as e: + await message.edit_text(f"❌ Ошибка получения информации о хранилище\n\nПричина: {str(e)}", parse_mode="HTML") + return + + # Формируем сообщение о состоянии хранилища + summary = storage_info.get("summary", {}) + total_size_gb = summary.get("total_space_gb", 0) + total_used_gb = summary.get("used_space_gb", 0) + free_space_gb = summary.get("free_space_gb", 0) + usage_percent = summary.get("usage_percent", 0) + + reply_text = f"📊 Информация о хранилище Synology NAS\n\n" + reply_text += f"Общий размер: {total_size_gb:.2f} ГБ\n" + reply_text += f"Использовано: {total_used_gb:.2f} ГБ ({usage_percent:.1f}%)\n" + reply_text += f"Свободно: {free_space_gb:.2f} ГБ\n\n" + + # Добавляем информацию о томах + volumes = storage_info.get("volumes", []) + if volumes: + reply_text += "Тома:\n" + for volume in volumes: + name = volume.get("name", "Неизвестно") + status = volume.get("status", "Неизвестно") + size = volume.get("size", 0) + used_size = volume.get("used_size", 0) + size_gb = size / (1024**3) + used_gb = used_size / (1024**3) + percent = round((used_size / size) * 100, 1) if size > 0 else 0 + + reply_text += f"• {name} ({status})\n" + reply_text += f" └ {used_gb:.1f} ГБ из {size_gb:.1f} ГБ ({percent}%)\n" + + # Добавляем информацию о дисках + disks = storage_info.get("disks", []) + if disks: + reply_text += "\nДиски:\n" + for disk in disks: + name = disk.get("name", "Неизвестно") + model = disk.get("model", "Неизвестно") + status = disk.get("status", "Неизвестно") + temp = disk.get("temp", "?") + + reply_text += f"• {name} - {model}\n" + reply_text += f" └ Статус: {status}, Температура: {temp}°C\n" + + await message.edit_text(reply_text, parse_mode="HTML") + +async def shares_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /shares""" + if not update.message or not update.effective_user: + return + + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + message = await update.message.reply_text("⏳ Получение информации об общих папках...") + + if not synology_api.is_online(): + await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию об общих папках.", parse_mode="HTML") + return + + try: + shares = synology_api.get_shared_folders() + + if not shares: + await message.edit_text("❌ Ошибка получения информации об общих папках\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML") + return + except Exception as e: + await message.edit_text(f"❌ Ошибка получения информации об общих папках\n\nПричина: {str(e)}", parse_mode="HTML") + return + + # Формируем сообщение об общих папках + reply_text = f"📁 Общие папки Synology NAS\n\n" + + for share in shares: + name = share.get("name", "Неизвестно") + path = share.get("path", "Неизвестно") + desc = share.get("desc", "") + + reply_text += f"• {name}\n" + reply_text += f" └ Путь: {path}\n" + + if desc: + reply_text += f" └ Описание: {desc}\n" + + await message.edit_text(reply_text, parse_mode="HTML") + +async def system_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /system""" + if not update.message or not update.effective_user: + return + + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + message = await update.message.reply_text("⏳ Получение информации о системе...") + + if not synology_api.is_online(): + await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о системе.", parse_mode="HTML") + return + + try: + system_status = synology_api.get_system_status() + + if not system_status: + await message.edit_text("❌ Ошибка получения информации о системе", parse_mode="HTML") + return + + # Если получен статус с ошибкой + if system_status.get("status") == "error": + error_code = system_status.get("error_code", "неизвестно") + await message.edit_text(f"❌ Ошибка получения информации о системе\n\nКод ошибки API: {error_code}", parse_mode="HTML") + return + except Exception as e: + await message.edit_text(f"❌ Ошибка получения информации о системе\n\nПричина: {str(e)}", parse_mode="HTML") + return + + # Формируем сообщение о состоянии системы + model = system_status.get("model", "Неизвестно") + version = system_status.get("version", "Неизвестно") + serial = system_status.get("serial", "Неизвестно") + uptime_seconds = system_status.get("uptime", 0) + temperature = system_status.get("temperature", "?") + + # Преобразование времени работы в удобочитаемый формат + days, remainder = divmod(uptime_seconds, 86400) + hours, remainder = divmod(remainder, 3600) + minutes, seconds = divmod(remainder, 60) + uptime_str = f"{days}д {hours}ч {minutes}м {seconds}с" + + reply_text = f"🖥️ Информация о системе Synology NAS\n\n" + reply_text += f"Модель: {model}\n" + reply_text += f"Серийный номер: {serial}\n" + reply_text += f"Версия DSM: {version}\n" + reply_text += f"Время работы: {uptime_str}\n" + reply_text += f"Температура: {temperature}°C\n\n" + + # Добавляем информацию о CPU и памяти + memory = system_status.get("memory", {}) + total_memory_gb = memory.get("total_mb", 0) / 1024 + available_memory_gb = memory.get("available_mb", 0) / 1024 + memory_usage = memory.get("usage_percent", 0) + cpu_usage = system_status.get("cpu_usage", 0) + + reply_text += f"Загрузка CPU: {cpu_usage}%\n" + reply_text += f"Память: {total_memory_gb:.1f} ГБ (используется {memory_usage}%)\n" + reply_text += f"Доступно памяти: {available_memory_gb:.1f} ГБ\n\n" + + # Добавляем информацию о сетевых интерфейсах + network_info = system_status.get("network", []) + if network_info: + reply_text += "Сетевые интерфейсы:\n" + for interface in network_info: + device = interface.get("device", "Неизвестно") + ip = interface.get("ip", "Неизвестно") + mac = interface.get("mac", "Неизвестно") + + reply_text += f"• {device}\n" + reply_text += f" └ IP: {ip}, MAC: {mac}\n" + + await message.edit_text(reply_text, parse_mode="HTML") + +async def load_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /load""" + if not update.message or not update.effective_user: + return + + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + message = await update.message.reply_text("⏳ Получение информации о нагрузке системы...") + + if not synology_api.is_online(): + await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о нагрузке системы.", parse_mode="HTML") + return + + try: + system_load = synology_api.get_system_load() + + if not system_load: + await message.edit_text("❌ Ошибка получения информации о нагрузке системы\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML") + return + except Exception as e: + await message.edit_text(f"❌ Ошибка получения информации о нагрузке системы\n\nПричина: {str(e)}", parse_mode="HTML") + return + + # Формируем сообщение о нагрузке системы + cpu_load = system_load.get("cpu_load", 0) + memory = system_load.get("memory", {}) + memory_usage = memory.get("usage_percent", 0) + + reply_text = f"📈 Текущая нагрузка Synology NAS\n\n" + reply_text += f"Загрузка CPU: {cpu_load}%\n" + reply_text += f"Загрузка памяти: {memory_usage}%\n\n" + + # Добавляем информацию о сетевой активности + network = system_load.get("network", {}) + if network: + reply_text += "Сетевая активность:\n" + for device, stats in network.items(): + rx = int(stats.get("rx_bytes", 0)) / (1024**2) # МБ + tx = int(stats.get("tx_bytes", 0)) / (1024**2) # МБ + + reply_text += f"• {device}\n" + reply_text += f" └ Получено: {rx:.1f} МБ, Отправлено: {tx:.1f} МБ\n" + + await message.edit_text(reply_text, parse_mode="HTML") + +async def security_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /security""" + if not update.message or not update.effective_user: + return + + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + message = await update.message.reply_text("⏳ Получение информации о безопасности...") + + if not synology_api.is_online(): + await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о безопасности.", parse_mode="HTML") + return + + try: + security_info = synology_api.get_security_status() + + if not security_info.get("success", False): + await message.edit_text("❌ Ошибка получения информации о безопасности\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML") + return + except Exception as e: + await message.edit_text(f"❌ Ошибка получения информации о безопасности\n\nПричина: {str(e)}", parse_mode="HTML") + return + + # Формируем сообщение о безопасности + status = security_info.get("status", "unknown") + is_secure = security_info.get("is_secure", False) + last_check = security_info.get("last_check", "Неизвестно") + + status_emoji = "✅" if is_secure else "⚠️" + status_text = "Безопасно" if is_secure else "Требуется внимание" + + reply_text = f"🔐 Статус безопасности Synology NAS\n\n" + reply_text += f"Статус: {status_emoji} {status_text}\n" + reply_text += f"Подробности: {status}\n" + reply_text += f"Последняя проверка: {last_check}\n" + + await message.edit_text(reply_text, parse_mode="HTML") diff --git a/.history/src/handlers/extended_handlers_20250830095651.py b/.history/src/handlers/extended_handlers_20250830095651.py new file mode 100644 index 0000000..ca60d52 --- /dev/null +++ b/.history/src/handlers/extended_handlers_20250830095651.py @@ -0,0 +1,367 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Дополнительные обработчики команд для телеграм-бота +""" + +import logging +from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup +from telegram.ext import ContextTypes + +from src.config.config import ADMIN_USER_IDS +from src.api.synology import SynologyAPI + +logger = logging.getLogger(__name__) + +# Инициализация API Synology +synology_api = SynologyAPI() + +async def check_api_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /checkapi для диагностики проблем с API""" + if not update.message or not update.effective_user: + return + + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + message = await update.message.reply_text("⏳ Проверка доступных API Synology NAS...") + + from src.api.api_discovery import discover_available_apis + from src.config.config import ( + SYNOLOGY_HOST, + SYNOLOGY_PORT, + SYNOLOGY_SECURE, + SYNOLOGY_POWER_API, + SYNOLOGY_INFO_API, + SYNOLOGY_API_VERSION + ) + + # Формируем базовый URL + protocol = "https" if SYNOLOGY_SECURE else "http" + base_url = f"{protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi" + + # Получаем список доступных API + apis = discover_available_apis(base_url) + + if not apis: + await message.edit_text( + "❌ Не удалось получить список доступных API\n\n" + "Проверьте доступность NAS и сетевое подключение.", + parse_mode="HTML" + ) + return + + # Поиск API для управления питанием + power_apis = [name for name in apis.keys() if "power" in name.lower()] + system_info_apis = [name for name in apis.keys() if ("system" in name.lower() or "dsm" in name.lower()) and "info" in name.lower()] + reboot_apis = [name for name in apis.keys() if any(word in name.lower() for word in ["restart", "reboot", "power"])] + + # Формируем рекомендации + recommended_power_api = max(power_apis, key=lambda x: apis[x].get('maxVersion', 1)) if power_apis else "SYNO.Core.System" + recommended_info_api = max(system_info_apis, key=lambda x: apis[x].get('maxVersion', 1)) if system_info_apis else "SYNO.DSM.Info" + + # Формируем текст отчета + api_report = ( + f"✅ Найдено {len(apis)} доступных API\n\n" + f"API для управления питанием:\n" + f"{', '.join(power_apis[:5]) or 'Не найдены'}\n\n" + f"API для информации о системе:\n" + f"{', '.join(system_info_apis[:5]) or 'Не найдены'}\n\n" + f"API для перезагрузки:\n" + f"{', '.join(reboot_apis[:5]) or 'Не найдены'}\n\n" + f"Рекомендуемые настройки:\n" + f"Power API: {recommended_power_api}\n" + f"Info API: {recommended_info_api}\n\n" + f"Текущие настройки в конфигурации:\n" + f"SYNOLOGY_POWER_API = {SYNOLOGY_POWER_API}\n" + f"SYNOLOGY_INFO_API = {SYNOLOGY_INFO_API}\n" + f"SYNOLOGY_API_VERSION = {SYNOLOGY_API_VERSION}" + ) + + await message.edit_text(api_report, parse_mode="HTML") + +async def storage_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /storage""" + if not update.message or not update.effective_user: + return + + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + message = await update.message.reply_text("⏳ Получение информации о хранилище...") + + if not synology_api.is_online(): + await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о хранилище.", parse_mode="HTML") + return + + try: + storage_info = synology_api.get_storage_status() + + if not storage_info: + await message.edit_text("❌ Ошибка получения информации о хранилище\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML") + return + except Exception as e: + await message.edit_text(f"❌ Ошибка получения информации о хранилище\n\nПричина: {str(e)}", parse_mode="HTML") + return + + # Формируем сообщение о состоянии хранилища + summary = storage_info.get("summary", {}) + total_size_gb = summary.get("total_space_gb", 0) + total_used_gb = summary.get("used_space_gb", 0) + free_space_gb = summary.get("free_space_gb", 0) + usage_percent = summary.get("usage_percent", 0) + + reply_text = f"📊 Информация о хранилище Synology NAS\n\n" + reply_text += f"Общий размер: {total_size_gb:.2f} ГБ\n" + reply_text += f"Использовано: {total_used_gb:.2f} ГБ ({usage_percent:.1f}%)\n" + reply_text += f"Свободно: {free_space_gb:.2f} ГБ\n\n" + + # Добавляем информацию о томах + volumes = storage_info.get("volumes", []) + if volumes: + reply_text += "Тома:\n" + for volume in volumes: + name = volume.get("name", "Неизвестно") + status = volume.get("status", "Неизвестно") + size = volume.get("size", 0) + used_size = volume.get("used_size", 0) + size_gb = size / (1024**3) + used_gb = used_size / (1024**3) + percent = round((used_size / size) * 100, 1) if size > 0 else 0 + + reply_text += f"• {name} ({status})\n" + reply_text += f" └ {used_gb:.1f} ГБ из {size_gb:.1f} ГБ ({percent}%)\n" + + # Добавляем информацию о дисках + disks = storage_info.get("disks", []) + if disks: + reply_text += "\nДиски:\n" + for disk in disks: + name = disk.get("name", "Неизвестно") + model = disk.get("model", "Неизвестно") + status = disk.get("status", "Неизвестно") + temp = disk.get("temp", "?") + + reply_text += f"• {name} - {model}\n" + reply_text += f" └ Статус: {status}, Температура: {temp}°C\n" + + await message.edit_text(reply_text, parse_mode="HTML") + +async def shares_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /shares""" + if not update.message or not update.effective_user: + return + + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + message = await update.message.reply_text("⏳ Получение информации об общих папках...") + + if not synology_api.is_online(): + await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию об общих папках.", parse_mode="HTML") + return + + try: + shares = synology_api.get_shared_folders() + + if not shares: + await message.edit_text("❌ Ошибка получения информации об общих папках\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML") + return + except Exception as e: + await message.edit_text(f"❌ Ошибка получения информации об общих папках\n\nПричина: {str(e)}", parse_mode="HTML") + return + + # Формируем сообщение об общих папках + reply_text = f"📁 Общие папки Synology NAS\n\n" + + for share in shares: + name = share.get("name", "Неизвестно") + path = share.get("path", "Неизвестно") + desc = share.get("desc", "") + + reply_text += f"• {name}\n" + reply_text += f" └ Путь: {path}\n" + + if desc: + reply_text += f" └ Описание: {desc}\n" + + await message.edit_text(reply_text, parse_mode="HTML") + +async def system_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /system""" + if not update.message or not update.effective_user: + return + + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + message = await update.message.reply_text("⏳ Получение информации о системе...") + + if not synology_api.is_online(): + await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о системе.", parse_mode="HTML") + return + + try: + system_status = synology_api.get_system_status() + + if not system_status: + await message.edit_text("❌ Ошибка получения информации о системе", parse_mode="HTML") + return + + # Если получен статус с ошибкой + if system_status.get("status") == "error": + error_code = system_status.get("error_code", "неизвестно") + await message.edit_text(f"❌ Ошибка получения информации о системе\n\nКод ошибки API: {error_code}", parse_mode="HTML") + return + except Exception as e: + await message.edit_text(f"❌ Ошибка получения информации о системе\n\nПричина: {str(e)}", parse_mode="HTML") + return + + # Формируем сообщение о состоянии системы + model = system_status.get("model", "Неизвестно") + version = system_status.get("version", "Неизвестно") + serial = system_status.get("serial", "Неизвестно") + uptime_seconds = system_status.get("uptime", 0) + temperature = system_status.get("temperature", "?") + + # Преобразование времени работы в удобочитаемый формат + days, remainder = divmod(uptime_seconds, 86400) + hours, remainder = divmod(remainder, 3600) + minutes, seconds = divmod(remainder, 60) + uptime_str = f"{days}д {hours}ч {minutes}м {seconds}с" + + reply_text = f"🖥️ Информация о системе Synology NAS\n\n" + reply_text += f"Модель: {model}\n" + reply_text += f"Серийный номер: {serial}\n" + reply_text += f"Версия DSM: {version}\n" + reply_text += f"Время работы: {uptime_str}\n" + reply_text += f"Температура: {temperature}°C\n\n" + + # Добавляем информацию о CPU и памяти + memory = system_status.get("memory", {}) + total_memory_gb = memory.get("total_mb", 0) / 1024 + available_memory_gb = memory.get("available_mb", 0) / 1024 + memory_usage = memory.get("usage_percent", 0) + cpu_usage = system_status.get("cpu_usage", 0) + + reply_text += f"Загрузка CPU: {cpu_usage}%\n" + reply_text += f"Память: {total_memory_gb:.1f} ГБ (используется {memory_usage}%)\n" + reply_text += f"Доступно памяти: {available_memory_gb:.1f} ГБ\n\n" + + # Добавляем информацию о сетевых интерфейсах + network_info = system_status.get("network", []) + if network_info: + reply_text += "Сетевые интерфейсы:\n" + for interface in network_info: + device = interface.get("device", "Неизвестно") + ip = interface.get("ip", "Неизвестно") + mac = interface.get("mac", "Неизвестно") + + reply_text += f"• {device}\n" + reply_text += f" └ IP: {ip}, MAC: {mac}\n" + + await message.edit_text(reply_text, parse_mode="HTML") + +async def load_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /load""" + if not update.message or not update.effective_user: + return + + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + message = await update.message.reply_text("⏳ Получение информации о нагрузке системы...") + + if not synology_api.is_online(): + await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о нагрузке системы.", parse_mode="HTML") + return + + try: + system_load = synology_api.get_system_load() + + if not system_load: + await message.edit_text("❌ Ошибка получения информации о нагрузке системы\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML") + return + except Exception as e: + await message.edit_text(f"❌ Ошибка получения информации о нагрузке системы\n\nПричина: {str(e)}", parse_mode="HTML") + return + + # Формируем сообщение о нагрузке системы + cpu_load = system_load.get("cpu_load", 0) + memory = system_load.get("memory", {}) + memory_usage = memory.get("usage_percent", 0) + + reply_text = f"📈 Текущая нагрузка Synology NAS\n\n" + reply_text += f"Загрузка CPU: {cpu_load}%\n" + reply_text += f"Загрузка памяти: {memory_usage}%\n\n" + + # Добавляем информацию о сетевой активности + network = system_load.get("network", {}) + if network: + reply_text += "Сетевая активность:\n" + for device, stats in network.items(): + rx = int(stats.get("rx_bytes", 0)) / (1024**2) # МБ + tx = int(stats.get("tx_bytes", 0)) / (1024**2) # МБ + + reply_text += f"• {device}\n" + reply_text += f" └ Получено: {rx:.1f} МБ, Отправлено: {tx:.1f} МБ\n" + + await message.edit_text(reply_text, parse_mode="HTML") + +async def security_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /security""" + if not update.message or not update.effective_user: + return + + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + message = await update.message.reply_text("⏳ Получение информации о безопасности...") + + if not synology_api.is_online(): + await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о безопасности.", parse_mode="HTML") + return + + try: + security_info = synology_api.get_security_status() + + if not security_info.get("success", False): + await message.edit_text("❌ Ошибка получения информации о безопасности\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML") + return + except Exception as e: + await message.edit_text(f"❌ Ошибка получения информации о безопасности\n\nПричина: {str(e)}", parse_mode="HTML") + return + + # Формируем сообщение о безопасности + status = security_info.get("status", "unknown") + is_secure = security_info.get("is_secure", False) + last_check = security_info.get("last_check", "Неизвестно") + + status_emoji = "✅" if is_secure else "⚠️" + status_text = "Безопасно" if is_secure else "Требуется внимание" + + reply_text = f"🔐 Статус безопасности Synology NAS\n\n" + reply_text += f"Статус: {status_emoji} {status_text}\n" + reply_text += f"Подробности: {status}\n" + reply_text += f"Последняя проверка: {last_check}\n" + + await message.edit_text(reply_text, parse_mode="HTML") diff --git a/.history/src/handlers/help_handlers_20250830091943.py b/.history/src/handlers/help_handlers_20250830091943.py new file mode 100644 index 0000000..28d248b --- /dev/null +++ b/.history/src/handlers/help_handlers_20250830091943.py @@ -0,0 +1,82 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Модуль с функциями для генерации справочных сообщений о командах бота +""" + +import logging +from telegram import Update +from telegram.ext import ContextTypes +from telegram.constants import ParseMode + +logger = logging.getLogger(__name__) + +async def help_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """ + Обработчик команды /help - выводит справку по всем доступным командам + """ + user_id = update.effective_user.id + username = update.effective_user.username + + logger.info(f"User {user_id} (@{username}) requested help") + + help_text = ( + "🤖 Synology Power Control Bot\n\n" + "БАЗОВЫЕ КОМАНДЫ:\n" + "/status - Проверить состояние NAS\n" + "/power - Управление питанием NAS (меню)\n" + "/reboot - Перезагрузка NAS (с подтверждением)\n" + "/sleep - Перевод NAS в спящий режим (с подтверждением)\n\n" + + "ИНФОРМАЦИОННЫЕ КОМАНДЫ:\n" + "/system - Информация о системе\n" + "/storage - Состояние хранилища\n" + "/shares - Список общих папок\n" + "/load - Нагрузка на систему\n" + "/security - Информация о безопасности\n" + "/temperature - Температура устройства\n" + "/processes - Список активных процессов\n" + "/network - Сетевая информация\n\n" + + "РАСШИРЕННЫЕ КОМАНДЫ:\n" + "/schedule - Расписание питания\n" + "/browse - Просмотр файлов\n" + "/search <запрос> - Поиск файлов\n" + "/updates - Проверка обновлений\n" + "/backup - Статус резервного копирования\n" + "/quota - Квоты пользователей\n\n" + + "БЫСТРЫЕ КОМАНДЫ:\n" + "/quickreboot - Быстрая перезагрузка\n" + "/wakeup - Пробуждение NAS (WOL)\n\n" + + "СЛУЖЕБНЫЕ КОМАНДЫ:\n" + "/checkapi - Проверка API\n" + "/help - Эта справка\n" + ) + + await update.message.reply_text(help_text, parse_mode=ParseMode.HTML) + +async def start_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """ + Обработчик команды /start - приветствие и краткая информация + """ + user_id = update.effective_user.id + username = update.effective_user.username + + logger.info(f"User {user_id} (@{username}) started the bot") + + welcome_text = ( + "👋 Добро пожаловать в Synology Power Control Bot!\n\n" + "С помощью этого бота вы можете управлять питанием вашего Synology NAS " + "и получать различную информацию о его состоянии.\n\n" + "Для просмотра списка доступных команд используйте /help\n\n" + "Базовые команды:\n" + "• /status - Проверить состояние NAS\n" + "• /power - Управление питанием\n" + "• /system - Информация о системе\n" + "• /storage - Состояние хранилища" + ) + + await update.message.reply_text(welcome_text, parse_mode=ParseMode.HTML) diff --git a/.history/src/handlers/help_handlers_20250830091955.py b/.history/src/handlers/help_handlers_20250830091955.py new file mode 100644 index 0000000..9c5a8d7 --- /dev/null +++ b/.history/src/handlers/help_handlers_20250830091955.py @@ -0,0 +1,82 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Модуль с функциями для генерации справочных сообщений о командах бота +""" + +import logging +from telegram import Update +from telegram.ext import ContextTypes +from telegram.constants import ParseMode + +logger = logging.getLogger(__name__) + +async def help_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """ + Обработчик команды /help - выводит справку по всем доступным командам + """ + user_id = update.effective_user.id if update.effective_user else "Unknown" + username = update.effective_user.username if update.effective_user else "Unknown" + + logger.info(f"User {user_id} (@{username}) requested help") + + help_text = ( + "🤖 Synology Power Control Bot\n\n" + "БАЗОВЫЕ КОМАНДЫ:\n" + "/status - Проверить состояние NAS\n" + "/power - Управление питанием NAS (меню)\n" + "/reboot - Перезагрузка NAS (с подтверждением)\n" + "/sleep - Перевод NAS в спящий режим (с подтверждением)\n\n" + + "ИНФОРМАЦИОННЫЕ КОМАНДЫ:\n" + "/system - Информация о системе\n" + "/storage - Состояние хранилища\n" + "/shares - Список общих папок\n" + "/load - Нагрузка на систему\n" + "/security - Информация о безопасности\n" + "/temperature - Температура устройства\n" + "/processes - Список активных процессов\n" + "/network - Сетевая информация\n\n" + + "РАСШИРЕННЫЕ КОМАНДЫ:\n" + "/schedule - Расписание питания\n" + "/browse - Просмотр файлов\n" + "/search <запрос> - Поиск файлов\n" + "/updates - Проверка обновлений\n" + "/backup - Статус резервного копирования\n" + "/quota - Квоты пользователей\n\n" + + "БЫСТРЫЕ КОМАНДЫ:\n" + "/quickreboot - Быстрая перезагрузка\n" + "/wakeup - Пробуждение NAS (WOL)\n\n" + + "СЛУЖЕБНЫЕ КОМАНДЫ:\n" + "/checkapi - Проверка API\n" + "/help - Эта справка\n" + ) + + await update.message.reply_text(help_text, parse_mode=ParseMode.HTML) + +async def start_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """ + Обработчик команды /start - приветствие и краткая информация + """ + user_id = update.effective_user.id + username = update.effective_user.username + + logger.info(f"User {user_id} (@{username}) started the bot") + + welcome_text = ( + "👋 Добро пожаловать в Synology Power Control Bot!\n\n" + "С помощью этого бота вы можете управлять питанием вашего Synology NAS " + "и получать различную информацию о его состоянии.\n\n" + "Для просмотра списка доступных команд используйте /help\n\n" + "Базовые команды:\n" + "• /status - Проверить состояние NAS\n" + "• /power - Управление питанием\n" + "• /system - Информация о системе\n" + "• /storage - Состояние хранилища" + ) + + await update.message.reply_text(welcome_text, parse_mode=ParseMode.HTML) diff --git a/.history/src/handlers/help_handlers_20250830092004.py b/.history/src/handlers/help_handlers_20250830092004.py new file mode 100644 index 0000000..7d0f885 --- /dev/null +++ b/.history/src/handlers/help_handlers_20250830092004.py @@ -0,0 +1,82 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Модуль с функциями для генерации справочных сообщений о командах бота +""" + +import logging +from telegram import Update +from telegram.ext import ContextTypes +from telegram.constants import ParseMode + +logger = logging.getLogger(__name__) + +async def help_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """ + Обработчик команды /help - выводит справку по всем доступным командам + """ + user_id = update.effective_user.id if update.effective_user else "Unknown" + username = update.effective_user.username if update.effective_user else "Unknown" + + logger.info(f"User {user_id} (@{username}) requested help") + + help_text = ( + "🤖 Synology Power Control Bot\n\n" + "БАЗОВЫЕ КОМАНДЫ:\n" + "/status - Проверить состояние NAS\n" + "/power - Управление питанием NAS (меню)\n" + "/reboot - Перезагрузка NAS (с подтверждением)\n" + "/sleep - Перевод NAS в спящий режим (с подтверждением)\n\n" + + "ИНФОРМАЦИОННЫЕ КОМАНДЫ:\n" + "/system - Информация о системе\n" + "/storage - Состояние хранилища\n" + "/shares - Список общих папок\n" + "/load - Нагрузка на систему\n" + "/security - Информация о безопасности\n" + "/temperature - Температура устройства\n" + "/processes - Список активных процессов\n" + "/network - Сетевая информация\n\n" + + "РАСШИРЕННЫЕ КОМАНДЫ:\n" + "/schedule - Расписание питания\n" + "/browse - Просмотр файлов\n" + "/search <запрос> - Поиск файлов\n" + "/updates - Проверка обновлений\n" + "/backup - Статус резервного копирования\n" + "/quota - Квоты пользователей\n\n" + + "БЫСТРЫЕ КОМАНДЫ:\n" + "/quickreboot - Быстрая перезагрузка\n" + "/wakeup - Пробуждение NAS (WOL)\n\n" + + "СЛУЖЕБНЫЕ КОМАНДЫ:\n" + "/checkapi - Проверка API\n" + "/help - Эта справка\n" + ) + + await update.message.reply_text(help_text, parse_mode=ParseMode.HTML) + +async def start_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """ + Обработчик команды /start - приветствие и краткая информация + """ + user_id = update.effective_user.id if update.effective_user else "Unknown" + username = update.effective_user.username if update.effective_user else "Unknown" + + logger.info(f"User {user_id} (@{username}) started the bot") + + welcome_text = ( + "👋 Добро пожаловать в Synology Power Control Bot!\n\n" + "С помощью этого бота вы можете управлять питанием вашего Synology NAS " + "и получать различную информацию о его состоянии.\n\n" + "Для просмотра списка доступных команд используйте /help\n\n" + "Базовые команды:\n" + "• /status - Проверить состояние NAS\n" + "• /power - Управление питанием\n" + "• /system - Информация о системе\n" + "• /storage - Состояние хранилища" + ) + + await update.message.reply_text(welcome_text, parse_mode=ParseMode.HTML) diff --git a/.history/src/handlers/help_handlers_20250830092014.py b/.history/src/handlers/help_handlers_20250830092014.py new file mode 100644 index 0000000..3df863d --- /dev/null +++ b/.history/src/handlers/help_handlers_20250830092014.py @@ -0,0 +1,85 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Модуль с функциями для генерации справочных сообщений о командах бота +""" + +import logging +from telegram import Update +from telegram.ext import ContextTypes +from telegram.constants import ParseMode + +logger = logging.getLogger(__name__) + +async def help_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """ + Обработчик команды /help - выводит справку по всем доступным командам + """ + user_id = update.effective_user.id if update.effective_user else "Unknown" + username = update.effective_user.username if update.effective_user else "Unknown" + + logger.info(f"User {user_id} (@{username}) requested help") + + help_text = ( + "🤖 Synology Power Control Bot\n\n" + "БАЗОВЫЕ КОМАНДЫ:\n" + "/status - Проверить состояние NAS\n" + "/power - Управление питанием NAS (меню)\n" + "/reboot - Перезагрузка NAS (с подтверждением)\n" + "/sleep - Перевод NAS в спящий режим (с подтверждением)\n\n" + + "ИНФОРМАЦИОННЫЕ КОМАНДЫ:\n" + "/system - Информация о системе\n" + "/storage - Состояние хранилища\n" + "/shares - Список общих папок\n" + "/load - Нагрузка на систему\n" + "/security - Информация о безопасности\n" + "/temperature - Температура устройства\n" + "/processes - Список активных процессов\n" + "/network - Сетевая информация\n\n" + + "РАСШИРЕННЫЕ КОМАНДЫ:\n" + "/schedule - Расписание питания\n" + "/browse - Просмотр файлов\n" + "/search <запрос> - Поиск файлов\n" + "/updates - Проверка обновлений\n" + "/backup - Статус резервного копирования\n" + "/quota - Квоты пользователей\n\n" + + "БЫСТРЫЕ КОМАНДЫ:\n" + "/quickreboot - Быстрая перезагрузка\n" + "/wakeup - Пробуждение NAS (WOL)\n\n" + + "СЛУЖЕБНЫЕ КОМАНДЫ:\n" + "/checkapi - Проверка API\n" + "/help - Эта справка\n" + ) + + if update.message: + await update.message.reply_text(help_text, parse_mode=ParseMode.HTML) + elif update.callback_query: + await update.callback_query.message.reply_text(help_text, parse_mode=ParseMode.HTML) + +async def start_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """ + Обработчик команды /start - приветствие и краткая информация + """ + user_id = update.effective_user.id if update.effective_user else "Unknown" + username = update.effective_user.username if update.effective_user else "Unknown" + + logger.info(f"User {user_id} (@{username}) started the bot") + + welcome_text = ( + "👋 Добро пожаловать в Synology Power Control Bot!\n\n" + "С помощью этого бота вы можете управлять питанием вашего Synology NAS " + "и получать различную информацию о его состоянии.\n\n" + "Для просмотра списка доступных команд используйте /help\n\n" + "Базовые команды:\n" + "• /status - Проверить состояние NAS\n" + "• /power - Управление питанием\n" + "• /system - Информация о системе\n" + "• /storage - Состояние хранилища" + ) + + await update.message.reply_text(welcome_text, parse_mode=ParseMode.HTML) diff --git a/.history/src/handlers/help_handlers_20250830092029.py b/.history/src/handlers/help_handlers_20250830092029.py new file mode 100644 index 0000000..34506f3 --- /dev/null +++ b/.history/src/handlers/help_handlers_20250830092029.py @@ -0,0 +1,86 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Модуль с функциями для генерации справочных сообщений о командах бота +""" + +import logging +from telegram import Update +from telegram.ext import ContextTypes +from telegram.constants import ParseMode + +logger = logging.getLogger(__name__) + +async def help_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """ + Обработчик команды /help - выводит справку по всем доступным командам + """ + user_id = update.effective_user.id if update.effective_user else "Unknown" + username = update.effective_user.username if update.effective_user else "Unknown" + + logger.info(f"User {user_id} (@{username}) requested help") + + help_text = ( + "🤖 Synology Power Control Bot\n\n" + "БАЗОВЫЕ КОМАНДЫ:\n" + "/status - Проверить состояние NAS\n" + "/power - Управление питанием NAS (меню)\n" + "/reboot - Перезагрузка NAS (с подтверждением)\n" + "/sleep - Перевод NAS в спящий режим (с подтверждением)\n\n" + + "ИНФОРМАЦИОННЫЕ КОМАНДЫ:\n" + "/system - Информация о системе\n" + "/storage - Состояние хранилища\n" + "/shares - Список общих папок\n" + "/load - Нагрузка на систему\n" + "/security - Информация о безопасности\n" + "/temperature - Температура устройства\n" + "/processes - Список активных процессов\n" + "/network - Сетевая информация\n\n" + + "РАСШИРЕННЫЕ КОМАНДЫ:\n" + "/schedule - Расписание питания\n" + "/browse - Просмотр файлов\n" + "/search <запрос> - Поиск файлов\n" + "/updates - Проверка обновлений\n" + "/backup - Статус резервного копирования\n" + "/quota - Квоты пользователей\n\n" + + "БЫСТРЫЕ КОМАНДЫ:\n" + "/quickreboot - Быстрая перезагрузка\n" + "/wakeup - Пробуждение NAS (WOL)\n\n" + + "СЛУЖЕБНЫЕ КОМАНДЫ:\n" + "/checkapi - Проверка API\n" + "/help - Эта справка\n" + ) + + if update.message: + await update.message.reply_text(help_text, parse_mode=ParseMode.HTML) + elif update.callback_query: + await update.callback_query.answer() + await update.callback_query.message.edit_text(help_text, parse_mode=ParseMode.HTML) + +async def start_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """ + Обработчик команды /start - приветствие и краткая информация + """ + user_id = update.effective_user.id if update.effective_user else "Unknown" + username = update.effective_user.username if update.effective_user else "Unknown" + + logger.info(f"User {user_id} (@{username}) started the bot") + + welcome_text = ( + "👋 Добро пожаловать в Synology Power Control Bot!\n\n" + "С помощью этого бота вы можете управлять питанием вашего Synology NAS " + "и получать различную информацию о его состоянии.\n\n" + "Для просмотра списка доступных команд используйте /help\n\n" + "Базовые команды:\n" + "• /status - Проверить состояние NAS\n" + "• /power - Управление питанием\n" + "• /system - Информация о системе\n" + "• /storage - Состояние хранилища" + ) + + await update.message.reply_text(welcome_text, parse_mode=ParseMode.HTML) diff --git a/.history/src/handlers/help_handlers_20250830092040.py b/.history/src/handlers/help_handlers_20250830092040.py new file mode 100644 index 0000000..bcd68f7 --- /dev/null +++ b/.history/src/handlers/help_handlers_20250830092040.py @@ -0,0 +1,87 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Модуль с функциями для генерации справочных сообщений о командах бота +""" + +import logging +from telegram import Update +from telegram.ext import ContextTypes +from telegram.constants import ParseMode + +logger = logging.getLogger(__name__) + +async def help_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """ + Обработчик команды /help - выводит справку по всем доступным командам + """ + user_id = update.effective_user.id if update.effective_user else "Unknown" + username = update.effective_user.username if update.effective_user else "Unknown" + + logger.info(f"User {user_id} (@{username}) requested help") + + help_text = ( + "🤖 Synology Power Control Bot\n\n" + "БАЗОВЫЕ КОМАНДЫ:\n" + "/status - Проверить состояние NAS\n" + "/power - Управление питанием NAS (меню)\n" + "/reboot - Перезагрузка NAS (с подтверждением)\n" + "/sleep - Перевод NAS в спящий режим (с подтверждением)\n\n" + + "ИНФОРМАЦИОННЫЕ КОМАНДЫ:\n" + "/system - Информация о системе\n" + "/storage - Состояние хранилища\n" + "/shares - Список общих папок\n" + "/load - Нагрузка на систему\n" + "/security - Информация о безопасности\n" + "/temperature - Температура устройства\n" + "/processes - Список активных процессов\n" + "/network - Сетевая информация\n\n" + + "РАСШИРЕННЫЕ КОМАНДЫ:\n" + "/schedule - Расписание питания\n" + "/browse - Просмотр файлов\n" + "/search <запрос> - Поиск файлов\n" + "/updates - Проверка обновлений\n" + "/backup - Статус резервного копирования\n" + "/quota - Квоты пользователей\n\n" + + "БЫСТРЫЕ КОМАНДЫ:\n" + "/quickreboot - Быстрая перезагрузка\n" + "/wakeup - Пробуждение NAS (WOL)\n\n" + + "СЛУЖЕБНЫЕ КОМАНДЫ:\n" + "/checkapi - Проверка API\n" + "/help - Эта справка\n" + ) + + if update.message: + await update.message.reply_text(help_text, parse_mode=ParseMode.HTML) + elif update.callback_query: + await update.callback_query.answer() + await update.callback_query.message.edit_text(help_text, parse_mode=ParseMode.HTML) + +async def start_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """ + Обработчик команды /start - приветствие и краткая информация + """ + user_id = update.effective_user.id if update.effective_user else "Unknown" + username = update.effective_user.username if update.effective_user else "Unknown" + + logger.info(f"User {user_id} (@{username}) started the bot") + + welcome_text = ( + "👋 Добро пожаловать в Synology Power Control Bot!\n\n" + "С помощью этого бота вы можете управлять питанием вашего Synology NAS " + "и получать различную информацию о его состоянии.\n\n" + "Для просмотра списка доступных команд используйте /help\n\n" + "Базовые команды:\n" + "• /status - Проверить состояние NAS\n" + "• /power - Управление питанием\n" + "• /system - Информация о системе\n" + "• /storage - Состояние хранилища" + ) + + if update.message: + await update.message.reply_text(welcome_text, parse_mode=ParseMode.HTML) diff --git a/.history/src/handlers/help_handlers_20250830092051.py b/.history/src/handlers/help_handlers_20250830092051.py new file mode 100644 index 0000000..2181fe6 --- /dev/null +++ b/.history/src/handlers/help_handlers_20250830092051.py @@ -0,0 +1,92 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Модуль с функциями для генерации справочных сообщений о командах бота +""" + +import logging +from telegram import Update +from telegram.ext import ContextTypes +from telegram.constants import ParseMode + +logger = logging.getLogger(__name__) + +async def help_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """ + Обработчик команды /help - выводит справку по всем доступным командам + """ + user_id = update.effective_user.id if update.effective_user else "Unknown" + username = update.effective_user.username if update.effective_user else "Unknown" + + logger.info(f"User {user_id} (@{username}) requested help") + + help_text = ( + "🤖 Synology Power Control Bot\n\n" + "БАЗОВЫЕ КОМАНДЫ:\n" + "/status - Проверить состояние NAS\n" + "/power - Управление питанием NAS (меню)\n" + "/reboot - Перезагрузка NAS (с подтверждением)\n" + "/sleep - Перевод NAS в спящий режим (с подтверждением)\n\n" + + "ИНФОРМАЦИОННЫЕ КОМАНДЫ:\n" + "/system - Информация о системе\n" + "/storage - Состояние хранилища\n" + "/shares - Список общих папок\n" + "/load - Нагрузка на систему\n" + "/security - Информация о безопасности\n" + "/temperature - Температура устройства\n" + "/processes - Список активных процессов\n" + "/network - Сетевая информация\n\n" + + "РАСШИРЕННЫЕ КОМАНДЫ:\n" + "/schedule - Расписание питания\n" + "/browse - Просмотр файлов\n" + "/search <запрос> - Поиск файлов\n" + "/updates - Проверка обновлений\n" + "/backup - Статус резервного копирования\n" + "/quota - Квоты пользователей\n\n" + + "БЫСТРЫЕ КОМАНДЫ:\n" + "/quickreboot - Быстрая перезагрузка\n" + "/wakeup - Пробуждение NAS (WOL)\n\n" + + "СЛУЖЕБНЫЕ КОМАНДЫ:\n" + "/checkapi - Проверка API\n" + "/help - Эта справка\n" + ) + + if update.message: + await update.message.reply_text(help_text, parse_mode=ParseMode.HTML) + elif update.callback_query and update.callback_query.message: + await update.callback_query.answer() + try: + await update.callback_query.message.edit_text(help_text, parse_mode=ParseMode.HTML) + except Exception as e: + logger.error(f"Failed to edit message: {e}") + # Отправляем новое сообщение, если не можем отредактировать + await update.callback_query.message.reply_text(help_text, parse_mode=ParseMode.HTML) + +async def start_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """ + Обработчик команды /start - приветствие и краткая информация + """ + user_id = update.effective_user.id if update.effective_user else "Unknown" + username = update.effective_user.username if update.effective_user else "Unknown" + + logger.info(f"User {user_id} (@{username}) started the bot") + + welcome_text = ( + "👋 Добро пожаловать в Synology Power Control Bot!\n\n" + "С помощью этого бота вы можете управлять питанием вашего Synology NAS " + "и получать различную информацию о его состоянии.\n\n" + "Для просмотра списка доступных команд используйте /help\n\n" + "Базовые команды:\n" + "• /status - Проверить состояние NAS\n" + "• /power - Управление питанием\n" + "• /system - Информация о системе\n" + "• /storage - Состояние хранилища" + ) + + if update.message: + await update.message.reply_text(welcome_text, parse_mode=ParseMode.HTML) diff --git a/.history/src/handlers/help_handlers_20250830092139.py b/.history/src/handlers/help_handlers_20250830092139.py new file mode 100644 index 0000000..00a9ca6 --- /dev/null +++ b/.history/src/handlers/help_handlers_20250830092139.py @@ -0,0 +1,111 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Модуль с функциями для генерации справочных сообщений о командах бота +""" + +import logging +from telegram import Update +from telegram.ext import ContextTypes +from telegram.constants import ParseMode + +from src.config.config import ADMIN_USER_IDS + +logger = logging.getLogger(__name__) + +async def help_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """ + Обработчик команды /help - выводит справку по всем доступным командам + """ + user_id = update.effective_user.id if update.effective_user else "Unknown" + username = update.effective_user.username if update.effective_user else "Unknown" + + logger.info(f"User {user_id} (@{username}) requested help") + + # Проверка прав доступа + if user_id not in ADMIN_USER_IDS and isinstance(user_id, int): + if update.message: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + help_text = ( + "🤖 Synology Power Control Bot\n\n" + "БАЗОВЫЕ КОМАНДЫ:\n" + "/status - Проверить состояние NAS\n" + "/power - Управление питанием NAS (меню)\n" + "/reboot - Перезагрузка NAS (с подтверждением)\n" + "/sleep - Перевод NAS в спящий режим (с подтверждением)\n\n" + + "ИНФОРМАЦИОННЫЕ КОМАНДЫ:\n" + "/system - Информация о системе\n" + "/storage - Состояние хранилища\n" + "/shares - Список общих папок\n" + "/load - Нагрузка на систему\n" + "/security - Информация о безопасности\n" + "/temperature - Температура устройства\n" + "/processes - Список активных процессов\n" + "/network - Сетевая информация\n\n" + + "РАСШИРЕННЫЕ КОМАНДЫ:\n" + "/schedule - Расписание питания\n" + "/browse - Просмотр файлов\n" + "/search <запрос> - Поиск файлов\n" + "/updates - Проверка обновлений\n" + "/backup - Статус резервного копирования\n" + "/quota - Квоты пользователей\n\n" + + "БЫСТРЫЕ КОМАНДЫ:\n" + "/quickreboot - Быстрая перезагрузка\n" + "/wakeup - Пробуждение NAS (WOL)\n\n" + + "СЛУЖЕБНЫЕ КОМАНДЫ:\n" + "/checkapi - Проверка API\n" + "/help - Эта справка\n" + ) + + if update.message: + await update.message.reply_text(help_text, parse_mode=ParseMode.HTML) + elif update.callback_query: + await update.callback_query.answer() + if update.callback_query.message: + try: + await update.callback_query.message.edit_text(help_text, parse_mode=ParseMode.HTML) + except Exception as e: + logger.error(f"Failed to edit message: {e}") + # Отправляем новое сообщение в текущий чат + await context.bot.send_message( + chat_id=update.callback_query.message.chat_id, + text=help_text, + parse_mode=ParseMode.HTML + ) + +async def start_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """ + Обработчик команды /start - приветствие и краткая информация + """ + user_id = update.effective_user.id if update.effective_user else "Unknown" + username = update.effective_user.username if update.effective_user else "Unknown" + + logger.info(f"User {user_id} (@{username}) started the bot") + + # Проверка прав доступа + if user_id not in ADMIN_USER_IDS and isinstance(user_id, int): + if update.message: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + welcome_text = ( + "👋 Добро пожаловать в Synology Power Control Bot!\n\n" + "С помощью этого бота вы можете управлять питанием вашего Synology NAS " + "и получать различную информацию о его состоянии.\n\n" + "Для просмотра списка доступных команд используйте /help\n\n" + "Базовые команды:\n" + "• /status - Проверить состояние NAS\n" + "• /power - Управление питанием\n" + "• /system - Информация о системе\n" + "• /storage - Состояние хранилища" + ) + + if update.message: + await update.message.reply_text(welcome_text, parse_mode=ParseMode.HTML) diff --git a/.history/src/handlers/help_handlers_20250830092441.py b/.history/src/handlers/help_handlers_20250830092441.py new file mode 100644 index 0000000..00a9ca6 --- /dev/null +++ b/.history/src/handlers/help_handlers_20250830092441.py @@ -0,0 +1,111 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Модуль с функциями для генерации справочных сообщений о командах бота +""" + +import logging +from telegram import Update +from telegram.ext import ContextTypes +from telegram.constants import ParseMode + +from src.config.config import ADMIN_USER_IDS + +logger = logging.getLogger(__name__) + +async def help_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """ + Обработчик команды /help - выводит справку по всем доступным командам + """ + user_id = update.effective_user.id if update.effective_user else "Unknown" + username = update.effective_user.username if update.effective_user else "Unknown" + + logger.info(f"User {user_id} (@{username}) requested help") + + # Проверка прав доступа + if user_id not in ADMIN_USER_IDS and isinstance(user_id, int): + if update.message: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + help_text = ( + "🤖 Synology Power Control Bot\n\n" + "БАЗОВЫЕ КОМАНДЫ:\n" + "/status - Проверить состояние NAS\n" + "/power - Управление питанием NAS (меню)\n" + "/reboot - Перезагрузка NAS (с подтверждением)\n" + "/sleep - Перевод NAS в спящий режим (с подтверждением)\n\n" + + "ИНФОРМАЦИОННЫЕ КОМАНДЫ:\n" + "/system - Информация о системе\n" + "/storage - Состояние хранилища\n" + "/shares - Список общих папок\n" + "/load - Нагрузка на систему\n" + "/security - Информация о безопасности\n" + "/temperature - Температура устройства\n" + "/processes - Список активных процессов\n" + "/network - Сетевая информация\n\n" + + "РАСШИРЕННЫЕ КОМАНДЫ:\n" + "/schedule - Расписание питания\n" + "/browse - Просмотр файлов\n" + "/search <запрос> - Поиск файлов\n" + "/updates - Проверка обновлений\n" + "/backup - Статус резервного копирования\n" + "/quota - Квоты пользователей\n\n" + + "БЫСТРЫЕ КОМАНДЫ:\n" + "/quickreboot - Быстрая перезагрузка\n" + "/wakeup - Пробуждение NAS (WOL)\n\n" + + "СЛУЖЕБНЫЕ КОМАНДЫ:\n" + "/checkapi - Проверка API\n" + "/help - Эта справка\n" + ) + + if update.message: + await update.message.reply_text(help_text, parse_mode=ParseMode.HTML) + elif update.callback_query: + await update.callback_query.answer() + if update.callback_query.message: + try: + await update.callback_query.message.edit_text(help_text, parse_mode=ParseMode.HTML) + except Exception as e: + logger.error(f"Failed to edit message: {e}") + # Отправляем новое сообщение в текущий чат + await context.bot.send_message( + chat_id=update.callback_query.message.chat_id, + text=help_text, + parse_mode=ParseMode.HTML + ) + +async def start_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """ + Обработчик команды /start - приветствие и краткая информация + """ + user_id = update.effective_user.id if update.effective_user else "Unknown" + username = update.effective_user.username if update.effective_user else "Unknown" + + logger.info(f"User {user_id} (@{username}) started the bot") + + # Проверка прав доступа + if user_id not in ADMIN_USER_IDS and isinstance(user_id, int): + if update.message: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + welcome_text = ( + "👋 Добро пожаловать в Synology Power Control Bot!\n\n" + "С помощью этого бота вы можете управлять питанием вашего Synology NAS " + "и получать различную информацию о его состоянии.\n\n" + "Для просмотра списка доступных команд используйте /help\n\n" + "Базовые команды:\n" + "• /status - Проверить состояние NAS\n" + "• /power - Управление питанием\n" + "• /system - Информация о системе\n" + "• /storage - Состояние хранилища" + ) + + if update.message: + await update.message.reply_text(welcome_text, parse_mode=ParseMode.HTML) diff --git a/.history/src/handlers/help_handlers_20250830095731.py b/.history/src/handlers/help_handlers_20250830095731.py new file mode 100644 index 0000000..80fa480 --- /dev/null +++ b/.history/src/handlers/help_handlers_20250830095731.py @@ -0,0 +1,111 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Модуль с функциями для генерации справочных сообщений о командах бота +""" + +import logging +from telegram import Update +from telegram.ext import ContextTypes +from telegram.constants import ParseMode + +from src.config.config import ADMIN_USER_IDS + +logger = logging.getLogger(__name__) + +async def help_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """ + Обработчик команды /help - выводит справку по всем доступным командам + """ + user_id = update.effective_user.id if update.effective_user else "Unknown" + username = update.effective_user.username if update.effective_user else "Unknown" + + logger.info(f"User {user_id} (@{username}) requested help") + + # Проверка прав доступа + if user_id not in ADMIN_USER_IDS and isinstance(user_id, int): + if update.message: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + help_text = ( + "🤖 Synology Power Control Bot\n\n" + "БАЗОВЫЕ КОМАНДЫ:\n" + "/status - Проверить состояние NAS\n" + "/power - Управление питанием NAS (меню)\n" + "/reboot - Перезагрузка NAS (с подтверждением)\n" + "/sleep - Перевод NAS в спящий режим (с подтверждением)\n\n" + + "ИНФОРМАЦИОННЫЕ КОМАНДЫ:\n" + "/system - Информация о системе\n" + "/storage - Состояние хранилища\n" + "/shares - Список общих папок\n" + "/load - Нагрузка на систему\n" + "/security - Информация о безопасности\n" + "/temperature - Температура устройства\n" + "/processes - Список активных процессов\n" + "/network - Сетевая информация\n\n" + + "РАСШИРЕННЫЕ КОМАНДЫ:\n" + "/schedule - Расписание питания\n" + "/browse - Просмотр файлов\n" + "/search <запрос> - Поиск файлов\n" + "/updates - Проверка обновлений\n" + "/backup - Статус резервного копирования\n" + "/quota - Квоты пользователей\n\n" + + "БЫСТРЫЕ КОМАНДЫ:\n" + "/quickreboot - Быстрая перезагрузка\n" + "/wakeup - Пробуждение NAS (WOL)\n\n" + + "СЛУЖЕБНЫЕ КОМАНДЫ:\n" + "/checkapi - Проверка API\n" + "/help - Эта справка\n" + ) + + if update.message: + await update.message.reply_text(help_text, parse_mode=ParseMode.HTML) + elif update.callback_query: + await update.callback_query.answer() + if update.callback_query.message: + try: + await update.callback_query.message.edit_text(help_text, parse_mode=ParseMode.HTML) + except Exception as e: + logger.error(f"Failed to edit message: {e}") + # Отправляем новое сообщение в текущий чат + await context.bot.send_message( + chat_id=update.callback_query.message.chat_id, + text=help_text, + parse_mode=ParseMode.HTML + ) + +async def start_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """ + Обработчик команды /start - приветствие и краткая информация + """ + user_id = update.effective_user.id if update.effective_user else "Unknown" + username = update.effective_user.username if update.effective_user else "Unknown" + + logger.info(f"User {user_id} (@{username}) started the bot") + + # Проверка прав доступа + if user_id not in ADMIN_USER_IDS and isinstance(user_id, int): + if update.message: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + welcome_text = ( + "👋 Добро пожаловать в Synology Power Control Bot!\n\n" + "С помощью этого бота вы можете управлять питанием вашего Synology NAS " + "и получать различную информацию о его состоянии.\n\n" + "Для просмотра списка доступных команд используйте /help\n\n" + "Базовые команды:\n" + "• /status - Проверить состояние NAS\n" + "• /power - Управление питанием\n" + "• /system - Информация о системе\n" + "• /storage - Состояние хранилища" + ) + + if update.message: + await update.message.reply_text(welcome_text, parse_mode=ParseMode.HTML) diff --git a/.history/src/handlers/help_handlers_20250830095750.py b/.history/src/handlers/help_handlers_20250830095750.py new file mode 100644 index 0000000..80fa480 --- /dev/null +++ b/.history/src/handlers/help_handlers_20250830095750.py @@ -0,0 +1,111 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Модуль с функциями для генерации справочных сообщений о командах бота +""" + +import logging +from telegram import Update +from telegram.ext import ContextTypes +from telegram.constants import ParseMode + +from src.config.config import ADMIN_USER_IDS + +logger = logging.getLogger(__name__) + +async def help_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """ + Обработчик команды /help - выводит справку по всем доступным командам + """ + user_id = update.effective_user.id if update.effective_user else "Unknown" + username = update.effective_user.username if update.effective_user else "Unknown" + + logger.info(f"User {user_id} (@{username}) requested help") + + # Проверка прав доступа + if user_id not in ADMIN_USER_IDS and isinstance(user_id, int): + if update.message: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + help_text = ( + "🤖 Synology Power Control Bot\n\n" + "БАЗОВЫЕ КОМАНДЫ:\n" + "/status - Проверить состояние NAS\n" + "/power - Управление питанием NAS (меню)\n" + "/reboot - Перезагрузка NAS (с подтверждением)\n" + "/sleep - Перевод NAS в спящий режим (с подтверждением)\n\n" + + "ИНФОРМАЦИОННЫЕ КОМАНДЫ:\n" + "/system - Информация о системе\n" + "/storage - Состояние хранилища\n" + "/shares - Список общих папок\n" + "/load - Нагрузка на систему\n" + "/security - Информация о безопасности\n" + "/temperature - Температура устройства\n" + "/processes - Список активных процессов\n" + "/network - Сетевая информация\n\n" + + "РАСШИРЕННЫЕ КОМАНДЫ:\n" + "/schedule - Расписание питания\n" + "/browse - Просмотр файлов\n" + "/search <запрос> - Поиск файлов\n" + "/updates - Проверка обновлений\n" + "/backup - Статус резервного копирования\n" + "/quota - Квоты пользователей\n\n" + + "БЫСТРЫЕ КОМАНДЫ:\n" + "/quickreboot - Быстрая перезагрузка\n" + "/wakeup - Пробуждение NAS (WOL)\n\n" + + "СЛУЖЕБНЫЕ КОМАНДЫ:\n" + "/checkapi - Проверка API\n" + "/help - Эта справка\n" + ) + + if update.message: + await update.message.reply_text(help_text, parse_mode=ParseMode.HTML) + elif update.callback_query: + await update.callback_query.answer() + if update.callback_query.message: + try: + await update.callback_query.message.edit_text(help_text, parse_mode=ParseMode.HTML) + except Exception as e: + logger.error(f"Failed to edit message: {e}") + # Отправляем новое сообщение в текущий чат + await context.bot.send_message( + chat_id=update.callback_query.message.chat_id, + text=help_text, + parse_mode=ParseMode.HTML + ) + +async def start_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """ + Обработчик команды /start - приветствие и краткая информация + """ + user_id = update.effective_user.id if update.effective_user else "Unknown" + username = update.effective_user.username if update.effective_user else "Unknown" + + logger.info(f"User {user_id} (@{username}) started the bot") + + # Проверка прав доступа + if user_id not in ADMIN_USER_IDS and isinstance(user_id, int): + if update.message: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + welcome_text = ( + "👋 Добро пожаловать в Synology Power Control Bot!\n\n" + "С помощью этого бота вы можете управлять питанием вашего Synology NAS " + "и получать различную информацию о его состоянии.\n\n" + "Для просмотра списка доступных команд используйте /help\n\n" + "Базовые команды:\n" + "• /status - Проверить состояние NAS\n" + "• /power - Управление питанием\n" + "• /system - Информация о системе\n" + "• /storage - Состояние хранилища" + ) + + if update.message: + await update.message.reply_text(welcome_text, parse_mode=ParseMode.HTML) diff --git a/.history/src/healthcheck_20250830102839.py b/.history/src/healthcheck_20250830102839.py new file mode 100644 index 0000000..0ec9b8e --- /dev/null +++ b/.history/src/healthcheck_20250830102839.py @@ -0,0 +1,71 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Простой HTTP-сервер для healthcheck Docker-контейнера. +Запускается параллельно с основным ботом и отвечает на запросы /health. +""" + +import os +import http.server +import socketserver +import threading +import logging +from time import sleep + +# Настройка логирования +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' +) +logger = logging.getLogger('healthcheck') + +# Порт для healthcheck +PORT = int(os.getenv('HEALTHCHECK_PORT', 8080)) + +class HealthCheckHandler(http.server.SimpleHTTPRequestHandler): + """Обработчик для healthcheck запросов""" + + def do_GET(self): + """Обработка GET-запросов""" + if self.path == '/health': + self.send_response(200) + self.send_header('Content-Type', 'text/plain') + self.end_headers() + self.wfile.write(b"OK") + else: + self.send_response(404) + self.send_header('Content-Type', 'text/plain') + self.end_headers() + self.wfile.write(b"Not Found") + + def log_message(self, format, *args): + """Переопределяем метод логирования для вывода в наш logger""" + logger.info("%s - - [%s] %s", self.address_string(), self.log_date_time_string(), format % args) + +def run_health_server(): + """Запуск HTTP-сервера для healthcheck""" + with socketserver.TCPServer(("", PORT), HealthCheckHandler) as httpd: + logger.info(f"Starting healthcheck server on port {PORT}") + httpd.serve_forever() + +def start_health_server(): + """Запуск сервера в отдельном потоке""" + # Даем основному приложению время на инициализацию + sleep(5) + + # Запускаем HTTP-сервер в отдельном потоке + thread = threading.Thread(target=run_health_server, daemon=True) + thread.start() + logger.info("Healthcheck server thread started") + return thread + +if __name__ == "__main__": + # Этот код выполняется только если файл запускается напрямую, а не импортируется + thread = start_health_server() + try: + # Держим основной поток живым + while True: + sleep(60) + except KeyboardInterrupt: + logger.info("Healthcheck server shutting down") diff --git a/.history/src/healthcheck_20250830103154.py b/.history/src/healthcheck_20250830103154.py new file mode 100644 index 0000000..0ec9b8e --- /dev/null +++ b/.history/src/healthcheck_20250830103154.py @@ -0,0 +1,71 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Простой HTTP-сервер для healthcheck Docker-контейнера. +Запускается параллельно с основным ботом и отвечает на запросы /health. +""" + +import os +import http.server +import socketserver +import threading +import logging +from time import sleep + +# Настройка логирования +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' +) +logger = logging.getLogger('healthcheck') + +# Порт для healthcheck +PORT = int(os.getenv('HEALTHCHECK_PORT', 8080)) + +class HealthCheckHandler(http.server.SimpleHTTPRequestHandler): + """Обработчик для healthcheck запросов""" + + def do_GET(self): + """Обработка GET-запросов""" + if self.path == '/health': + self.send_response(200) + self.send_header('Content-Type', 'text/plain') + self.end_headers() + self.wfile.write(b"OK") + else: + self.send_response(404) + self.send_header('Content-Type', 'text/plain') + self.end_headers() + self.wfile.write(b"Not Found") + + def log_message(self, format, *args): + """Переопределяем метод логирования для вывода в наш logger""" + logger.info("%s - - [%s] %s", self.address_string(), self.log_date_time_string(), format % args) + +def run_health_server(): + """Запуск HTTP-сервера для healthcheck""" + with socketserver.TCPServer(("", PORT), HealthCheckHandler) as httpd: + logger.info(f"Starting healthcheck server on port {PORT}") + httpd.serve_forever() + +def start_health_server(): + """Запуск сервера в отдельном потоке""" + # Даем основному приложению время на инициализацию + sleep(5) + + # Запускаем HTTP-сервер в отдельном потоке + thread = threading.Thread(target=run_health_server, daemon=True) + thread.start() + logger.info("Healthcheck server thread started") + return thread + +if __name__ == "__main__": + # Этот код выполняется только если файл запускается напрямую, а не импортируется + thread = start_health_server() + try: + # Держим основной поток живым + while True: + sleep(60) + except KeyboardInterrupt: + logger.info("Healthcheck server shutting down") diff --git a/.history/src/utils/logger_20250830063702.py b/.history/src/utils/logger_20250830063702.py new file mode 100644 index 0000000..3ecbddd --- /dev/null +++ b/.history/src/utils/logger_20250830063702.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Модуль для настройки логирования +""" + +import os +import logging +from logging.handlers import RotatingFileHandler + +def setup_logging(log_level=logging.INFO) -> None: + """ + Настройка логирования с ротацией файлов + + Args: + log_level: Уровень логирования (по умолчанию INFO) + """ + # Создание директории для логов, если её нет + logs_dir = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), "logs") + os.makedirs(logs_dir, exist_ok=True) + + # Путь к файлу лога + log_file = os.path.join(logs_dir, "synology_bot.log") + + # Базовая настройка логгера + logging.basicConfig( + level=log_level, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', + handlers=[ + RotatingFileHandler( + log_file, + maxBytes=10485760, # 10MB + backupCount=3 + ), + logging.StreamHandler() + ] + ) + + # Снижаем уровень логирования для некоторых модулей + logging.getLogger("httpx").setLevel(logging.WARNING) + logging.getLogger("telegram").setLevel(logging.WARNING) + + # Логирование старта системы + logging.info("Logging system initialized") diff --git a/.history/src/utils/logger_20250830063839.py b/.history/src/utils/logger_20250830063839.py new file mode 100644 index 0000000..3ecbddd --- /dev/null +++ b/.history/src/utils/logger_20250830063839.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Модуль для настройки логирования +""" + +import os +import logging +from logging.handlers import RotatingFileHandler + +def setup_logging(log_level=logging.INFO) -> None: + """ + Настройка логирования с ротацией файлов + + Args: + log_level: Уровень логирования (по умолчанию INFO) + """ + # Создание директории для логов, если её нет + logs_dir = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), "logs") + os.makedirs(logs_dir, exist_ok=True) + + # Путь к файлу лога + log_file = os.path.join(logs_dir, "synology_bot.log") + + # Базовая настройка логгера + logging.basicConfig( + level=log_level, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', + handlers=[ + RotatingFileHandler( + log_file, + maxBytes=10485760, # 10MB + backupCount=3 + ), + logging.StreamHandler() + ] + ) + + # Снижаем уровень логирования для некоторых модулей + logging.getLogger("httpx").setLevel(logging.WARNING) + logging.getLogger("telegram").setLevel(logging.WARNING) + + # Логирование старта системы + logging.info("Logging system initialized") diff --git a/.history/test_api_headers_20250830084440.py b/.history/test_api_headers_20250830084440.py new file mode 100644 index 0000000..ad073d6 --- /dev/null +++ b/.history/test_api_headers_20250830084440.py @@ -0,0 +1,391 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Тестовый скрипт для диагностики проблемы с использованием специальных заголовков +""" + +import requests +import logging +import json +import sys +import os +import urllib3 +import time +import socket +from requests.adapters import HTTPAdapter +from urllib3.util import Retry + +# Отключение предупреждений о небезопасных SSL-соединениях +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + +# Настройка логирования +logging.basicConfig( + level=logging.DEBUG, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' +) +logger = logging.getLogger(__name__) + +# Тестовые учетные данные (для примера) +SYNOLOGY_HOST = "192.168.0.102" +SYNOLOGY_PORT = 5000 +SYNOLOGY_USERNAME = "superadmin" +SYNOLOGY_PASSWORD = "Cl0ud_1985!" +SYNOLOGY_SECURE = False +SYNOLOGY_TIMEOUT = 10 + +def test_api_with_headers(): + """Тестирование API с использованием специальных заголовков для решения проблемы 119""" + + # Создаем сессию + session = requests.Session() + session.verify = False # Отключаем проверку SSL + + # Настройки повторных попыток + retry_strategy = Retry( + total=3, + status_forcelist=[429, 500, 502, 503, 504], + allowed_methods=["GET", "POST"], + backoff_factor=1.0 + ) + adapter = HTTPAdapter(max_retries=retry_strategy) + session.mount("http://", adapter) + session.mount("https://", adapter) + + # Формируем базовый URL + protocol = "https" if SYNOLOGY_SECURE else "http" + base_url = f"{protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi" + + logger.info(f"Тестирование API с заголовками для {base_url}") + + # Тест 1: Получение SID с настройкой cookie и user-agent + logger.info("Тест 1: Авторизация с настройкой cookie и user-agent") + + # Добавление пользовательских заголовков + custom_headers = { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36', + 'Accept': 'application/json, text/javascript, */*; q=0.01', + 'Accept-Language': 'ru-RU,ru;q=0.9,en-US;q=0.8,en;q=0.7', + 'X-Requested-With': 'XMLHttpRequest', + 'Connection': 'keep-alive', + 'Referer': f'{protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/' + } + + session.headers.update(custom_headers) + + try: + # Определяем путь для авторизации + auth_info_url = f"{base_url}/entry.cgi" + auth_info_params = { + "api": "SYNO.API.Info", + "version": "1", + "method": "query", + "query": "SYNO.API.Auth" + } + + auth_info_response = session.get(auth_info_url, params=auth_info_params) + auth_info_data = auth_info_response.json() + + if auth_info_data.get("success"): + auth_info = auth_info_data.get("data", {}).get("SYNO.API.Auth", {}) + auth_path = auth_info.get("path", "auth.cgi") + auth_max_version = auth_info.get("maxVersion", 6) + + logger.info(f"API авторизации: путь={auth_path}, макс. версия={auth_max_version}") + + # Используем версию 3 вместо 6 - тестирование на возможное решение проблемы + auth_version = min(3, auth_max_version) # Пробуем более старую версию API + + # Выполняем авторизацию + auth_url = f"{base_url}/{auth_path}" + auth_params = { + "api": "SYNO.API.Auth", + "version": str(auth_version), + "method": "login", + "account": SYNOLOGY_USERNAME, + "passwd": SYNOLOGY_PASSWORD, + "session": "DirectHeaderTest", + "format": "cookie" + } + + if auth_version >= 6: + auth_params["enable_syno_token"] = "yes" + + logger.info(f"Авторизация с использованием SYNO.API.Auth v{auth_version}") + auth_response = session.get(auth_url, params=auth_params) + auth_data = auth_response.json() + + if auth_data.get("success"): + sid = auth_data.get("data", {}).get("sid") + logger.info(f"Авторизация успешна! SID: {sid[:10]}...") + + # Теперь проверим, работает ли получение информации о системе + # с настройкой cookie и заголовков + + # Сначала настроим куки для сохранения SID + cookies = { + 'id': sid, + 'sid': sid + } + session.cookies.update(cookies) + + # Определяем путь для SYNO.DSM.Info + info_info_url = f"{base_url}/entry.cgi" + info_info_params = { + "api": "SYNO.API.Info", + "version": "1", + "method": "query", + "query": "SYNO.DSM.Info" + } + + info_info_response = session.get(info_info_url, params=info_info_params) + info_info_data = info_info_response.json() + + if info_info_data.get("success"): + info_info = info_info_data.get("data", {}).get("SYNO.DSM.Info", {}) + info_path = info_info.get("path", "entry.cgi") + info_max_version = info_info.get("maxVersion", 1) + info_min_version = info_info.get("minVersion", 1) + + logger.info(f"API SYNO.DSM.Info: путь={info_path}, версия={info_min_version}-{info_max_version}") + + # Используем правильную версию API + info_version = min(2, info_max_version) + + # Делаем запрос для получения информации о системе + # с использованием sid как параметр запроса + logger.info("Тест 2: Получение информации о системе с SID как параметром") + info_url = f"{base_url}/{info_path}" + info_params = { + "api": "SYNO.DSM.Info", + "version": str(info_version), + "method": "getinfo", + "_sid": sid + } + + logger.info(f"Запрос информации с использованием SYNO.DSM.Info v{info_version}") + info_response = session.get(info_url, params=info_params) + info_data = info_response.json() + + if info_data.get("success"): + logger.info("Успешно получена информация о системе!") + logger.info(f"Данные: {json.dumps(info_data.get('data', {}), indent=2)}") + else: + error_code = info_data.get("error", {}).get("code", -1) + logger.error(f"Не удалось получить информацию о системе. Ошибка: {error_code}") + + # Пробуем альтернативный способ + logger.info("Тест 3: Попытка получить базовую информацию через SYNO.Core.System") + + # Определяем путь для SYNO.Core.System + system_info_url = f"{base_url}/entry.cgi" + system_info_params = { + "api": "SYNO.API.Info", + "version": "1", + "method": "query", + "query": "SYNO.Core.System" + } + + system_info_response = session.get(system_info_url, params=system_info_params) + system_info_data = system_info_response.json() + + if system_info_data.get("success"): + system_info = system_info_data.get("data", {}).get("SYNO.Core.System", {}) + system_path = system_info.get("path", "entry.cgi") + system_max_version = system_info.get("maxVersion", 1) + system_min_version = system_info.get("minVersion", 1) + + logger.info(f"API SYNO.Core.System: путь={system_path}, версия={system_min_version}-{system_max_version}") + + # Используем правильную версию API + system_version = 1 + + # Пробуем альтернативную стратегию с X-SYNO-TOKEN + # Некоторые API Synology требуют специальный токен в заголовках + token = auth_data.get("data", {}).get("synotoken") + if token: + session.headers.update({'X-SYNO-TOKEN': token}) + logger.info(f"Добавлен X-SYNO-TOKEN: {token}") + + # Делаем запрос для получения информации о системе + system_url = f"{base_url}/{system_path}" + system_params = { + "api": "SYNO.Core.System", + "version": str(system_version), + "method": "info", + "_sid": sid + } + + logger.info(f"Запрос информации с использованием SYNO.Core.System v{system_version}") + system_response = session.get(system_url, params=system_params) + system_data = system_response.json() + + if system_data.get("success"): + logger.info("Успешно получена информация о системе через SYNO.Core.System!") + logger.info(f"Данные: {json.dumps(system_data.get('data', {}), indent=2)}") + else: + error_code = system_data.get("error", {}).get("code", -1) + logger.error(f"Не удалось получить информацию через SYNO.Core.System. Ошибка: {error_code}") + + # Пробуем другие методы для диагностики + logger.info("Тест 4: Попытка использовать различные методы и заголовки") + + # Пробуем создать полностью новую сессию + new_session = requests.Session() + new_session.verify = False + new_session.headers.update(custom_headers) + + # Пробуем версию 1 для авторизации + auth_version = 1 + auth_params["version"] = str(auth_version) + + auth_response = new_session.get(auth_url, params=auth_params) + auth_data = auth_response.json() + + if auth_data.get("success"): + sid = auth_data.get("data", {}).get("sid") + logger.info(f"Новая авторизация (v{auth_version}) успешна! SID: {sid[:10]}...") + + # Добавляем SID как куки + cookies = { + 'id': sid, + 'sid': sid + } + new_session.cookies.update(cookies) + + # Пробуем другой подход: разделение запросов во времени + logger.info("Тест 5: Разделение запросов во времени") + + # Даем некоторое время для инициализации сессии на сервере + time.sleep(2) + + # Пробуем получить список файлов + filestation_info_url = f"{base_url}/entry.cgi" + filestation_info_params = { + "api": "SYNO.API.Info", + "version": "1", + "method": "query", + "query": "SYNO.FileStation.List" + } + + filestation_info_response = new_session.get(filestation_info_url, params=filestation_info_params) + filestation_info_data = filestation_info_response.json() + + if filestation_info_data.get("success"): + filestation_info = filestation_info_data.get("data", {}).get("SYNO.FileStation.List", {}) + filestation_path = filestation_info.get("path", "entry.cgi") + filestation_max_version = filestation_info.get("maxVersion", 1) + + logger.info(f"API SYNO.FileStation.List: путь={filestation_path}, макс. версия={filestation_max_version}") + + # Используем правильную версию API + filestation_version = min(2, filestation_max_version) + + # Делаем запрос для получения списка общих папок + filestation_url = f"{base_url}/{filestation_path}" + filestation_params = { + "api": "SYNO.FileStation.List", + "version": str(filestation_version), + "method": "list_share", + "_sid": sid + } + + logger.info(f"Запрос списка общих папок с использованием SYNO.FileStation.List v{filestation_version}") + filestation_response = new_session.get(filestation_url, params=filestation_params) + filestation_data = filestation_response.json() + + if filestation_data.get("success"): + logger.info("Успешно получен список общих папок!") + shares = filestation_data.get("data", {}).get("shares", []) + logger.info(f"Общие папки: {json.dumps(shares, indent=2)[:200]}...") + else: + error_code = filestation_data.get("error", {}).get("code", -1) + logger.error(f"Не удалось получить список общих папок. Ошибка: {error_code}") + else: + error_code = auth_data.get("error", {}).get("code", -1) + logger.error(f"Новая авторизация не удалась! Код ошибки: {error_code}") + else: + logger.error("Не удалось получить информацию о SYNO.Core.System API") + else: + logger.error("Не удалось получить информацию о SYNO.DSM.Info API") + else: + error_code = auth_data.get("error", {}).get("code", -1) + logger.error(f"Авторизация не удалась! Код ошибки: {error_code}") + else: + logger.error("Не удалось получить информацию об API авторизации") + + except Exception as e: + logger.error(f"Произошла ошибка: {str(e)}") + + # Тест 6: Проверка сетевой доступности + logger.info("Тест 6: Проверка сетевой доступности") + + try: + # Проверка базового TCP-соединения + socket_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + socket_obj.settimeout(SYNOLOGY_TIMEOUT) + result = socket_obj.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT)) + socket_obj.close() + + if result == 0: + logger.info("TCP-соединение успешно установлено") + else: + logger.error(f"Не удалось установить TCP-соединение, код ошибки: {result}") + except Exception as e: + logger.error(f"Ошибка при проверке TCP-соединения: {str(e)}") + + # Тест 7: Запрос без аутентификации для проверки доступности API + logger.info("Тест 7: Запрос без аутентификации для проверки доступности API") + + try: + # Создаем новую сессию без аутентификации + simple_session = requests.Session() + simple_session.verify = False + + # Запрос к SYNO.API.Info не требует аутентификации + api_info_url = f"{base_url}/entry.cgi" + api_info_params = { + "api": "SYNO.API.Info", + "version": "1", + "method": "query", + "query": "all" + } + + logger.info("Запрос информации о всех API без аутентификации") + api_info_response = simple_session.get(api_info_url, params=api_info_params) + + if api_info_response.status_code == 200: + logger.info("API доступно без аутентификации") + api_info_data = api_info_response.json() + + if api_info_data.get("success"): + logger.info("Успешно получена информация о всех API") + api_count = len(api_info_data.get("data", {})) + logger.info(f"Количество доступных API: {api_count}") + + # Поиск API для управления питанием + power_apis = [] + for api_name, api_info in api_info_data.get("data", {}).items(): + if "power" in api_name.lower() or "reboot" in api_name.lower() or "shutdown" in api_name.lower(): + power_apis.append(f"{api_name}: {api_info}") + + logger.info(f"Найдены API для управления питанием: {power_apis}") + + # Поиск API для получения информации о системе + info_apis = [] + for api_name, api_info in api_info_data.get("data", {}).items(): + if "info" in api_name.lower() or "system" in api_name.lower() or "status" in api_name.lower(): + info_apis.append(f"{api_name}: {api_info}") + + logger.info(f"Найдены API для информации о системе: {info_apis[:5]} и еще {len(info_apis)-5}") + else: + error_code = api_info_data.get("error", {}).get("code", -1) + logger.error(f"Запрос к API без аутентификации не удался! Код ошибки: {error_code}") + else: + logger.error(f"API не доступно без аутентификации. HTTP статус: {api_info_response.status_code}") + except Exception as e: + logger.error(f"Ошибка при проверке доступности API: {str(e)}") + +if __name__ == "__main__": + logger.info("Запуск теста API с заголовками") + test_api_with_headers() diff --git a/.history/test_api_headers_20250830084500.py b/.history/test_api_headers_20250830084500.py new file mode 100644 index 0000000..ad073d6 --- /dev/null +++ b/.history/test_api_headers_20250830084500.py @@ -0,0 +1,391 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Тестовый скрипт для диагностики проблемы с использованием специальных заголовков +""" + +import requests +import logging +import json +import sys +import os +import urllib3 +import time +import socket +from requests.adapters import HTTPAdapter +from urllib3.util import Retry + +# Отключение предупреждений о небезопасных SSL-соединениях +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + +# Настройка логирования +logging.basicConfig( + level=logging.DEBUG, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' +) +logger = logging.getLogger(__name__) + +# Тестовые учетные данные (для примера) +SYNOLOGY_HOST = "192.168.0.102" +SYNOLOGY_PORT = 5000 +SYNOLOGY_USERNAME = "superadmin" +SYNOLOGY_PASSWORD = "Cl0ud_1985!" +SYNOLOGY_SECURE = False +SYNOLOGY_TIMEOUT = 10 + +def test_api_with_headers(): + """Тестирование API с использованием специальных заголовков для решения проблемы 119""" + + # Создаем сессию + session = requests.Session() + session.verify = False # Отключаем проверку SSL + + # Настройки повторных попыток + retry_strategy = Retry( + total=3, + status_forcelist=[429, 500, 502, 503, 504], + allowed_methods=["GET", "POST"], + backoff_factor=1.0 + ) + adapter = HTTPAdapter(max_retries=retry_strategy) + session.mount("http://", adapter) + session.mount("https://", adapter) + + # Формируем базовый URL + protocol = "https" if SYNOLOGY_SECURE else "http" + base_url = f"{protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi" + + logger.info(f"Тестирование API с заголовками для {base_url}") + + # Тест 1: Получение SID с настройкой cookie и user-agent + logger.info("Тест 1: Авторизация с настройкой cookie и user-agent") + + # Добавление пользовательских заголовков + custom_headers = { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36', + 'Accept': 'application/json, text/javascript, */*; q=0.01', + 'Accept-Language': 'ru-RU,ru;q=0.9,en-US;q=0.8,en;q=0.7', + 'X-Requested-With': 'XMLHttpRequest', + 'Connection': 'keep-alive', + 'Referer': f'{protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/' + } + + session.headers.update(custom_headers) + + try: + # Определяем путь для авторизации + auth_info_url = f"{base_url}/entry.cgi" + auth_info_params = { + "api": "SYNO.API.Info", + "version": "1", + "method": "query", + "query": "SYNO.API.Auth" + } + + auth_info_response = session.get(auth_info_url, params=auth_info_params) + auth_info_data = auth_info_response.json() + + if auth_info_data.get("success"): + auth_info = auth_info_data.get("data", {}).get("SYNO.API.Auth", {}) + auth_path = auth_info.get("path", "auth.cgi") + auth_max_version = auth_info.get("maxVersion", 6) + + logger.info(f"API авторизации: путь={auth_path}, макс. версия={auth_max_version}") + + # Используем версию 3 вместо 6 - тестирование на возможное решение проблемы + auth_version = min(3, auth_max_version) # Пробуем более старую версию API + + # Выполняем авторизацию + auth_url = f"{base_url}/{auth_path}" + auth_params = { + "api": "SYNO.API.Auth", + "version": str(auth_version), + "method": "login", + "account": SYNOLOGY_USERNAME, + "passwd": SYNOLOGY_PASSWORD, + "session": "DirectHeaderTest", + "format": "cookie" + } + + if auth_version >= 6: + auth_params["enable_syno_token"] = "yes" + + logger.info(f"Авторизация с использованием SYNO.API.Auth v{auth_version}") + auth_response = session.get(auth_url, params=auth_params) + auth_data = auth_response.json() + + if auth_data.get("success"): + sid = auth_data.get("data", {}).get("sid") + logger.info(f"Авторизация успешна! SID: {sid[:10]}...") + + # Теперь проверим, работает ли получение информации о системе + # с настройкой cookie и заголовков + + # Сначала настроим куки для сохранения SID + cookies = { + 'id': sid, + 'sid': sid + } + session.cookies.update(cookies) + + # Определяем путь для SYNO.DSM.Info + info_info_url = f"{base_url}/entry.cgi" + info_info_params = { + "api": "SYNO.API.Info", + "version": "1", + "method": "query", + "query": "SYNO.DSM.Info" + } + + info_info_response = session.get(info_info_url, params=info_info_params) + info_info_data = info_info_response.json() + + if info_info_data.get("success"): + info_info = info_info_data.get("data", {}).get("SYNO.DSM.Info", {}) + info_path = info_info.get("path", "entry.cgi") + info_max_version = info_info.get("maxVersion", 1) + info_min_version = info_info.get("minVersion", 1) + + logger.info(f"API SYNO.DSM.Info: путь={info_path}, версия={info_min_version}-{info_max_version}") + + # Используем правильную версию API + info_version = min(2, info_max_version) + + # Делаем запрос для получения информации о системе + # с использованием sid как параметр запроса + logger.info("Тест 2: Получение информации о системе с SID как параметром") + info_url = f"{base_url}/{info_path}" + info_params = { + "api": "SYNO.DSM.Info", + "version": str(info_version), + "method": "getinfo", + "_sid": sid + } + + logger.info(f"Запрос информации с использованием SYNO.DSM.Info v{info_version}") + info_response = session.get(info_url, params=info_params) + info_data = info_response.json() + + if info_data.get("success"): + logger.info("Успешно получена информация о системе!") + logger.info(f"Данные: {json.dumps(info_data.get('data', {}), indent=2)}") + else: + error_code = info_data.get("error", {}).get("code", -1) + logger.error(f"Не удалось получить информацию о системе. Ошибка: {error_code}") + + # Пробуем альтернативный способ + logger.info("Тест 3: Попытка получить базовую информацию через SYNO.Core.System") + + # Определяем путь для SYNO.Core.System + system_info_url = f"{base_url}/entry.cgi" + system_info_params = { + "api": "SYNO.API.Info", + "version": "1", + "method": "query", + "query": "SYNO.Core.System" + } + + system_info_response = session.get(system_info_url, params=system_info_params) + system_info_data = system_info_response.json() + + if system_info_data.get("success"): + system_info = system_info_data.get("data", {}).get("SYNO.Core.System", {}) + system_path = system_info.get("path", "entry.cgi") + system_max_version = system_info.get("maxVersion", 1) + system_min_version = system_info.get("minVersion", 1) + + logger.info(f"API SYNO.Core.System: путь={system_path}, версия={system_min_version}-{system_max_version}") + + # Используем правильную версию API + system_version = 1 + + # Пробуем альтернативную стратегию с X-SYNO-TOKEN + # Некоторые API Synology требуют специальный токен в заголовках + token = auth_data.get("data", {}).get("synotoken") + if token: + session.headers.update({'X-SYNO-TOKEN': token}) + logger.info(f"Добавлен X-SYNO-TOKEN: {token}") + + # Делаем запрос для получения информации о системе + system_url = f"{base_url}/{system_path}" + system_params = { + "api": "SYNO.Core.System", + "version": str(system_version), + "method": "info", + "_sid": sid + } + + logger.info(f"Запрос информации с использованием SYNO.Core.System v{system_version}") + system_response = session.get(system_url, params=system_params) + system_data = system_response.json() + + if system_data.get("success"): + logger.info("Успешно получена информация о системе через SYNO.Core.System!") + logger.info(f"Данные: {json.dumps(system_data.get('data', {}), indent=2)}") + else: + error_code = system_data.get("error", {}).get("code", -1) + logger.error(f"Не удалось получить информацию через SYNO.Core.System. Ошибка: {error_code}") + + # Пробуем другие методы для диагностики + logger.info("Тест 4: Попытка использовать различные методы и заголовки") + + # Пробуем создать полностью новую сессию + new_session = requests.Session() + new_session.verify = False + new_session.headers.update(custom_headers) + + # Пробуем версию 1 для авторизации + auth_version = 1 + auth_params["version"] = str(auth_version) + + auth_response = new_session.get(auth_url, params=auth_params) + auth_data = auth_response.json() + + if auth_data.get("success"): + sid = auth_data.get("data", {}).get("sid") + logger.info(f"Новая авторизация (v{auth_version}) успешна! SID: {sid[:10]}...") + + # Добавляем SID как куки + cookies = { + 'id': sid, + 'sid': sid + } + new_session.cookies.update(cookies) + + # Пробуем другой подход: разделение запросов во времени + logger.info("Тест 5: Разделение запросов во времени") + + # Даем некоторое время для инициализации сессии на сервере + time.sleep(2) + + # Пробуем получить список файлов + filestation_info_url = f"{base_url}/entry.cgi" + filestation_info_params = { + "api": "SYNO.API.Info", + "version": "1", + "method": "query", + "query": "SYNO.FileStation.List" + } + + filestation_info_response = new_session.get(filestation_info_url, params=filestation_info_params) + filestation_info_data = filestation_info_response.json() + + if filestation_info_data.get("success"): + filestation_info = filestation_info_data.get("data", {}).get("SYNO.FileStation.List", {}) + filestation_path = filestation_info.get("path", "entry.cgi") + filestation_max_version = filestation_info.get("maxVersion", 1) + + logger.info(f"API SYNO.FileStation.List: путь={filestation_path}, макс. версия={filestation_max_version}") + + # Используем правильную версию API + filestation_version = min(2, filestation_max_version) + + # Делаем запрос для получения списка общих папок + filestation_url = f"{base_url}/{filestation_path}" + filestation_params = { + "api": "SYNO.FileStation.List", + "version": str(filestation_version), + "method": "list_share", + "_sid": sid + } + + logger.info(f"Запрос списка общих папок с использованием SYNO.FileStation.List v{filestation_version}") + filestation_response = new_session.get(filestation_url, params=filestation_params) + filestation_data = filestation_response.json() + + if filestation_data.get("success"): + logger.info("Успешно получен список общих папок!") + shares = filestation_data.get("data", {}).get("shares", []) + logger.info(f"Общие папки: {json.dumps(shares, indent=2)[:200]}...") + else: + error_code = filestation_data.get("error", {}).get("code", -1) + logger.error(f"Не удалось получить список общих папок. Ошибка: {error_code}") + else: + error_code = auth_data.get("error", {}).get("code", -1) + logger.error(f"Новая авторизация не удалась! Код ошибки: {error_code}") + else: + logger.error("Не удалось получить информацию о SYNO.Core.System API") + else: + logger.error("Не удалось получить информацию о SYNO.DSM.Info API") + else: + error_code = auth_data.get("error", {}).get("code", -1) + logger.error(f"Авторизация не удалась! Код ошибки: {error_code}") + else: + logger.error("Не удалось получить информацию об API авторизации") + + except Exception as e: + logger.error(f"Произошла ошибка: {str(e)}") + + # Тест 6: Проверка сетевой доступности + logger.info("Тест 6: Проверка сетевой доступности") + + try: + # Проверка базового TCP-соединения + socket_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + socket_obj.settimeout(SYNOLOGY_TIMEOUT) + result = socket_obj.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT)) + socket_obj.close() + + if result == 0: + logger.info("TCP-соединение успешно установлено") + else: + logger.error(f"Не удалось установить TCP-соединение, код ошибки: {result}") + except Exception as e: + logger.error(f"Ошибка при проверке TCP-соединения: {str(e)}") + + # Тест 7: Запрос без аутентификации для проверки доступности API + logger.info("Тест 7: Запрос без аутентификации для проверки доступности API") + + try: + # Создаем новую сессию без аутентификации + simple_session = requests.Session() + simple_session.verify = False + + # Запрос к SYNO.API.Info не требует аутентификации + api_info_url = f"{base_url}/entry.cgi" + api_info_params = { + "api": "SYNO.API.Info", + "version": "1", + "method": "query", + "query": "all" + } + + logger.info("Запрос информации о всех API без аутентификации") + api_info_response = simple_session.get(api_info_url, params=api_info_params) + + if api_info_response.status_code == 200: + logger.info("API доступно без аутентификации") + api_info_data = api_info_response.json() + + if api_info_data.get("success"): + logger.info("Успешно получена информация о всех API") + api_count = len(api_info_data.get("data", {})) + logger.info(f"Количество доступных API: {api_count}") + + # Поиск API для управления питанием + power_apis = [] + for api_name, api_info in api_info_data.get("data", {}).items(): + if "power" in api_name.lower() or "reboot" in api_name.lower() or "shutdown" in api_name.lower(): + power_apis.append(f"{api_name}: {api_info}") + + logger.info(f"Найдены API для управления питанием: {power_apis}") + + # Поиск API для получения информации о системе + info_apis = [] + for api_name, api_info in api_info_data.get("data", {}).items(): + if "info" in api_name.lower() or "system" in api_name.lower() or "status" in api_name.lower(): + info_apis.append(f"{api_name}: {api_info}") + + logger.info(f"Найдены API для информации о системе: {info_apis[:5]} и еще {len(info_apis)-5}") + else: + error_code = api_info_data.get("error", {}).get("code", -1) + logger.error(f"Запрос к API без аутентификации не удался! Код ошибки: {error_code}") + else: + logger.error(f"API не доступно без аутентификации. HTTP статус: {api_info_response.status_code}") + except Exception as e: + logger.error(f"Ошибка при проверке доступности API: {str(e)}") + +if __name__ == "__main__": + logger.info("Запуск теста API с заголовками") + test_api_with_headers() diff --git a/.history/test_reboot_20250830083539.py b/.history/test_reboot_20250830083539.py new file mode 100644 index 0000000..90e896c --- /dev/null +++ b/.history/test_reboot_20250830083539.py @@ -0,0 +1,73 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Скрипт для тестирования функции перезагрузки Synology NAS +""" + +import os +import sys +import logging +from pathlib import Path + +# Добавляем путь проекта в sys.path +project_dir = str(Path(__file__).resolve().parent) +if project_dir not in sys.path: + sys.path.insert(0, project_dir) + +from src.api.synology import SynologyAPI +from src.config.config import SYNOLOGY_POWER_API, SYNOLOGY_INFO_API, SYNOLOGY_API_VERSION + +# Настройка логирования +logging.basicConfig( + level=logging.DEBUG, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' +) +logger = logging.getLogger(__name__) + +def main(): + """Основная функция для тестирования перезагрузки""" + logger.info("Тестирование функции перезагрузки Synology NAS") + + logger.info(f"Используемые API: POWER={SYNOLOGY_POWER_API}, INFO={SYNOLOGY_INFO_API}, VERSION={SYNOLOGY_API_VERSION}") + + # Инициализация API + synology = SynologyAPI() + + # Проверка онлайн статуса + logger.info("Проверка онлайн статуса...") + is_online = synology.is_online(force_check=True) + logger.info(f"Статус: {'Онлайн' if is_online else 'Оффлайн'}") + + if not is_online: + logger.error("NAS недоступен. Невозможно выполнить перезагрузку.") + return + + # Вывод информации о системе + logger.info("Получение информации о системе...") + system_info = synology.get_system_status() + + if system_info.get("status") == "error": + logger.error(f"Ошибка получения информации о системе: {system_info.get('error')}") + else: + logger.info(f"Информация о системе: {system_info}") + + # Запрос на подтверждение действия + confirm = input("Вы действительно хотите перезагрузить Synology NAS? (y/n): ") + if confirm.lower() != 'y': + logger.info("Операция отменена пользователем.") + return + + # Выполнение перезагрузки + logger.info("Выполнение перезагрузки...") + try: + result = synology.reboot_system() + if result: + logger.info("Перезагрузка выполнена успешно.") + else: + logger.error("Не удалось выполнить перезагрузку.") + except Exception as e: + logger.error(f"Ошибка при выполнении перезагрузки: {str(e)}") + +if __name__ == "__main__": + main() diff --git a/.history/test_reboot_20250830083624.py b/.history/test_reboot_20250830083624.py new file mode 100644 index 0000000..90e896c --- /dev/null +++ b/.history/test_reboot_20250830083624.py @@ -0,0 +1,73 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Скрипт для тестирования функции перезагрузки Synology NAS +""" + +import os +import sys +import logging +from pathlib import Path + +# Добавляем путь проекта в sys.path +project_dir = str(Path(__file__).resolve().parent) +if project_dir not in sys.path: + sys.path.insert(0, project_dir) + +from src.api.synology import SynologyAPI +from src.config.config import SYNOLOGY_POWER_API, SYNOLOGY_INFO_API, SYNOLOGY_API_VERSION + +# Настройка логирования +logging.basicConfig( + level=logging.DEBUG, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' +) +logger = logging.getLogger(__name__) + +def main(): + """Основная функция для тестирования перезагрузки""" + logger.info("Тестирование функции перезагрузки Synology NAS") + + logger.info(f"Используемые API: POWER={SYNOLOGY_POWER_API}, INFO={SYNOLOGY_INFO_API}, VERSION={SYNOLOGY_API_VERSION}") + + # Инициализация API + synology = SynologyAPI() + + # Проверка онлайн статуса + logger.info("Проверка онлайн статуса...") + is_online = synology.is_online(force_check=True) + logger.info(f"Статус: {'Онлайн' if is_online else 'Оффлайн'}") + + if not is_online: + logger.error("NAS недоступен. Невозможно выполнить перезагрузку.") + return + + # Вывод информации о системе + logger.info("Получение информации о системе...") + system_info = synology.get_system_status() + + if system_info.get("status") == "error": + logger.error(f"Ошибка получения информации о системе: {system_info.get('error')}") + else: + logger.info(f"Информация о системе: {system_info}") + + # Запрос на подтверждение действия + confirm = input("Вы действительно хотите перезагрузить Synology NAS? (y/n): ") + if confirm.lower() != 'y': + logger.info("Операция отменена пользователем.") + return + + # Выполнение перезагрузки + logger.info("Выполнение перезагрузки...") + try: + result = synology.reboot_system() + if result: + logger.info("Перезагрузка выполнена успешно.") + else: + logger.error("Не удалось выполнить перезагрузку.") + except Exception as e: + logger.error(f"Ошибка при выполнении перезагрузки: {str(e)}") + +if __name__ == "__main__": + main() diff --git a/.history/test_system_info_20250830083606.py b/.history/test_system_info_20250830083606.py new file mode 100644 index 0000000..95a9bf3 --- /dev/null +++ b/.history/test_system_info_20250830083606.py @@ -0,0 +1,87 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Скрипт для тестирования получения информации о системе Synology NAS +""" + +import os +import sys +import logging +from pathlib import Path + +# Добавляем путь проекта в sys.path +project_dir = str(Path(__file__).resolve().parent) +if project_dir not in sys.path: + sys.path.insert(0, project_dir) + +from src.api.synology import SynologyAPI +from src.config.config import SYNOLOGY_POWER_API, SYNOLOGY_INFO_API, SYNOLOGY_API_VERSION + +# Настройка логирования +logging.basicConfig( + level=logging.DEBUG, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' +) +logger = logging.getLogger(__name__) + +def main(): + """Основная функция для тестирования получения информации о системе""" + logger.info("Тестирование получения информации о системе Synology NAS") + + logger.info(f"Используемые API: POWER={SYNOLOGY_POWER_API}, INFO={SYNOLOGY_INFO_API}, VERSION={SYNOLOGY_API_VERSION}") + + # Инициализация API + synology = SynologyAPI() + + # Проверка онлайн статуса + logger.info("Проверка онлайн статуса...") + is_online = synology.is_online(force_check=True) + logger.info(f"Статус: {'Онлайн' if is_online else 'Оффлайн'}") + + if not is_online: + logger.error("NAS недоступен. Невозможно получить информацию о системе.") + return + + # Получение списка доступных API + logger.info("Получение списка доступных API...") + from src.api.api_discovery import discover_available_apis + from src.config.config import SYNOLOGY_HOST, SYNOLOGY_PORT, SYNOLOGY_SECURE + + protocol = "https" if SYNOLOGY_SECURE else "http" + base_url = f"{protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi" + + apis = discover_available_apis(base_url) + if apis: + logger.info(f"Найдено {len(apis)} API") + + # Фильтрация API для управления питанием + power_apis = [name for name in apis.keys() if "power" in name.lower()] + system_apis = [name for name in apis.keys() if "system" in name.lower() or "dsm.info" in name.lower()] + + logger.info(f"API для управления питанием: {power_apis}") + logger.info(f"API для системной информации: {system_apis}") + + # Проверка конкретных API + for api_name in [SYNOLOGY_POWER_API, SYNOLOGY_INFO_API]: + if api_name in apis: + api_info = apis[api_name] + logger.info(f"API {api_name}: versions={api_info.get('minVersion')}-{api_info.get('maxVersion')}, path={api_info.get('path')}") + else: + logger.warning(f"API {api_name} не найден в списке доступных API") + else: + logger.error("Не удалось получить список доступных API") + + # Вывод информации о системе + logger.info("Получение информации о системе...") + system_info = synology.get_system_status() + + if system_info.get("status") == "error": + logger.error(f"Ошибка получения информации о системе: {system_info.get('error')}") + else: + logger.info(f"Информация о системе: {system_info}") + + logger.info("Тестирование завершено.") + +if __name__ == "__main__": + main() diff --git a/.history/test_system_info_20250830083624.py b/.history/test_system_info_20250830083624.py new file mode 100644 index 0000000..95a9bf3 --- /dev/null +++ b/.history/test_system_info_20250830083624.py @@ -0,0 +1,87 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Скрипт для тестирования получения информации о системе Synology NAS +""" + +import os +import sys +import logging +from pathlib import Path + +# Добавляем путь проекта в sys.path +project_dir = str(Path(__file__).resolve().parent) +if project_dir not in sys.path: + sys.path.insert(0, project_dir) + +from src.api.synology import SynologyAPI +from src.config.config import SYNOLOGY_POWER_API, SYNOLOGY_INFO_API, SYNOLOGY_API_VERSION + +# Настройка логирования +logging.basicConfig( + level=logging.DEBUG, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' +) +logger = logging.getLogger(__name__) + +def main(): + """Основная функция для тестирования получения информации о системе""" + logger.info("Тестирование получения информации о системе Synology NAS") + + logger.info(f"Используемые API: POWER={SYNOLOGY_POWER_API}, INFO={SYNOLOGY_INFO_API}, VERSION={SYNOLOGY_API_VERSION}") + + # Инициализация API + synology = SynologyAPI() + + # Проверка онлайн статуса + logger.info("Проверка онлайн статуса...") + is_online = synology.is_online(force_check=True) + logger.info(f"Статус: {'Онлайн' if is_online else 'Оффлайн'}") + + if not is_online: + logger.error("NAS недоступен. Невозможно получить информацию о системе.") + return + + # Получение списка доступных API + logger.info("Получение списка доступных API...") + from src.api.api_discovery import discover_available_apis + from src.config.config import SYNOLOGY_HOST, SYNOLOGY_PORT, SYNOLOGY_SECURE + + protocol = "https" if SYNOLOGY_SECURE else "http" + base_url = f"{protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi" + + apis = discover_available_apis(base_url) + if apis: + logger.info(f"Найдено {len(apis)} API") + + # Фильтрация API для управления питанием + power_apis = [name for name in apis.keys() if "power" in name.lower()] + system_apis = [name for name in apis.keys() if "system" in name.lower() or "dsm.info" in name.lower()] + + logger.info(f"API для управления питанием: {power_apis}") + logger.info(f"API для системной информации: {system_apis}") + + # Проверка конкретных API + for api_name in [SYNOLOGY_POWER_API, SYNOLOGY_INFO_API]: + if api_name in apis: + api_info = apis[api_name] + logger.info(f"API {api_name}: versions={api_info.get('minVersion')}-{api_info.get('maxVersion')}, path={api_info.get('path')}") + else: + logger.warning(f"API {api_name} не найден в списке доступных API") + else: + logger.error("Не удалось получить список доступных API") + + # Вывод информации о системе + logger.info("Получение информации о системе...") + system_info = synology.get_system_status() + + if system_info.get("status") == "error": + logger.error(f"Ошибка получения информации о системе: {system_info.get('error')}") + else: + logger.info(f"Информация о системе: {system_info}") + + logger.info("Тестирование завершено.") + +if __name__ == "__main__": + main() diff --git a/.history/ОТЧЕТ_ПО_API_20250830090431.md b/.history/ОТЧЕТ_ПО_API_20250830090431.md new file mode 100644 index 0000000..68eba65 --- /dev/null +++ b/.history/ОТЧЕТ_ПО_API_20250830090431.md @@ -0,0 +1,185 @@ +# Отчет по доступным API и возможностям управления Synology NAS + +## 1. Доступные API Synology NAS + +На вашем Synology NAS (модель DS223j с DSM 7.2.2) обнаружено **572** различных API. Ниже перечислены ключевые категории API, которые можно использовать для расширения функциональности бота: + +### 1.1. API для управления питанием +- **SYNO.Core.Hardware.PowerRecovery** (v1) - основной API для управления питанием +- **SYNO.Core.Hardware.PowerSchedule** (v1) - API для настройки расписания включения/выключения +- **SYNO.Core.Hardware.NeedReboot** (v1) - API для перезагрузки системы + +### 1.2. API для системной информации +- **SYNO.DSM.Info** (v2) - основной API для получения общей информации о системе +- **SYNO.Core.System** (v1) - API для получения расширенной системной информации +- **SYNO.Core.System.Status** (v1) - API для получения статуса системы +- **SYNO.Core.System.Utilization** (v1) - API для получения сведений о загрузке системы + +### 1.3. API для хранилища и файлов +- **SYNO.Storage.CGI.Storage** - информация о хранилище и дисках +- **SYNO.FileStation.List** - получение списка файлов и папок + +### 1.4. API для мониторинга +- **SYNO.Core.System.Process** - информация о запущенных процессах +- **SYNO.Core.System.SystemHealth** - состояние здоровья системы + +## 2. Расширенные команды управления NAS + +Бот уже поддерживает следующие базовые команды: +- `/start` - Начало работы с ботом +- `/status` - Проверка текущего статуса NAS +- `/power` - Управление питанием NAS +- `/help` - Вывод справки + +Также реализованы расширенные команды: +- `/system` - Подробная информация о системе +- `/storage` - Информация о хранилище и дисках +- `/shares` - Список общих папок +- `/load` - Текущая нагрузка на систему +- `/security` - Статус безопасности системы +- `/checkapi` - Проверка доступных API Synology + +### 2.1. Рекомендуемые дополнительные команды + +На основе анализа доступных API предлагаю добавить следующие команды: + +#### 2.1.1. Управление питанием и расписанием +- `/schedule` - Управление расписанием включения/выключения NAS +- `/wakeup` - Немедленное включение NAS через Wake-on-LAN +- `/quickreboot` - Быстрая перезагрузка без запроса подтверждения + +#### 2.1.2. Мониторинг системы +- `/processes` - Просмотр активных процессов и их загрузки +- `/network` - Детальная информация о сетевых подключениях +- `/temperature` - Мониторинг температуры системы и дисков +- `/updates` - Проверка доступных обновлений для DSM + +#### 2.1.3. Управление файлами +- `/browse [путь]` - Просмотр файлов в указанной директории +- `/search [шаблон]` - Поиск файлов по шаблону +- `/quota` - Просмотр информации о квотах пользователей + +#### 2.1.4. Резервное копирование +- `/backup` - Управление задачами резервного копирования +- `/backupstatus` - Проверка статуса резервного копирования + +## 3. Возможности использования API + +### 3.1. Оптимизация текущего кода +- Обнаружено, что для успешного взаимодействия с API необходимы специальные HTTP-заголовки, имитирующие браузер +- API версии 3 показывает лучшую стабильность для базовых операций, чем версия 6 +- Для аутентификации рекомендуется использовать куки и форматы, совместимые с веб-интерфейсом + +### 3.2. Рекомендуемые настройки API +- **SYNOLOGY_POWER_API = SYNO.Core.Hardware.PowerRecovery** +- **SYNOLOGY_INFO_API = SYNO.DSM.Info** +- **SYNOLOGY_API_VERSION = 2** (вместо текущего значения 6) + +### 3.3. Новые функциональные возможности + +#### 3.3.1. Мониторинг производительности +```python +def get_performance_stats(): + """Получение детальной статистики производительности""" + result = api._make_api_request("SYNO.Core.System.Utilization", "get", version=1) + return result +``` + +#### 3.3.2. Управление сервисами +```python +def manage_services(service_name, action="status"): + """Управление системными сервисами (start/stop/restart/status)""" + result = api._make_api_request("SYNO.Core.Service", action, version=1, + params={"service": service_name}) + return result +``` + +#### 3.3.3. Просмотр журналов +```python +def get_system_logs(limit=20): + """Получение системных журналов""" + result = api._make_api_request("SYNO.Core.SyslogClient.Log", "list", version=1, + params={"limit": limit}) + return result +``` + +#### 3.3.4. Настройка расписания питания +```python +def set_power_schedule(days, time, action="boot"): + """Настройка расписания питания (boot/shutdown)""" + result = api._make_api_request("SYNO.Core.Hardware.PowerSchedule", "set", version=1, + params={"day": days, "time": time, "action": action}) + return result +``` + +## 4. Решение текущих проблем API + +### 4.1. Проблема с получением информации о хранилище +Текущий код использует заглушку для `get_storage_status()`. Рекомендуемая реализация: + +```python +def get_storage_status(self) -> Dict[str, Any]: + """Получение информации о хранилище""" + # Попробуем получить информацию о дисках + disk_result = self._make_api_request("SYNO.Storage.CGI.Storage", "load_info", version=1) + + if not disk_result: + # Пробуем альтернативный API + disk_result = self._make_api_request("SYNO.Core.Storage", "info", version=1) + + # Собираем результат + volumes = disk_result.get("volumes", []) + disks = disk_result.get("disks", []) + + total_size = sum(vol.get("size", {}).get("total", 0) for vol in volumes) + total_used = sum(vol.get("size", {}).get("used", 0) for vol in volumes) + + return { + "volumes": volumes, + "disks": disks, + "total_size": total_size, + "total_used": total_used + } +``` + +### 4.2. Проблема с получением списка общих папок + +```python +def get_shared_folders(self) -> List[Dict[str, Any]]: + """Получение списка общих папок""" + result = self._make_api_request("SYNO.FileStation.List", "list_share", version=2) + + if not result: + return [] + + return result.get("shares", []) +``` + +### 4.3. Проблема с получением информации о нагрузке + +```python +def get_system_load(self) -> Dict[str, Any]: + """Получение информации о нагрузке системы""" + result = self._make_api_request("SYNO.Core.System.Utilization", "get", version=1) + + if not result: + return {} + + return { + "cpu_load": result.get("cpu", {}).get("user", 0) + result.get("cpu", {}).get("system", 0), + "memory": result.get("memory", {}), + "network": result.get("network", {}) + } +``` + +## 5. Заключение + +Synology NAS предоставляет обширный набор API, которые можно использовать для создания полноценного Telegram-бота для управления и мониторинга. Основные рекомендации: + +1. Обновить версию API до более стабильной (v2-3 вместо v6) +2. Добавить специальные HTTP-заголовки для имитации веб-браузера +3. Использовать куки для сохранения сессии +4. Реализовать новые функции управления на основе доступных API +5. Добавить обработку ошибок для нестабильных API (особенно хранилища и общих папок) + +Дополнительно рекомендуется реализовать функции автоматического мониторинга и уведомления о важных событиях, таких как высокая температура, заканчивающееся место на дисках или необходимость обновления системы. diff --git a/.history/ОТЧЕТ_ПО_API_20250830090511.md b/.history/ОТЧЕТ_ПО_API_20250830090511.md new file mode 100644 index 0000000..68eba65 --- /dev/null +++ b/.history/ОТЧЕТ_ПО_API_20250830090511.md @@ -0,0 +1,185 @@ +# Отчет по доступным API и возможностям управления Synology NAS + +## 1. Доступные API Synology NAS + +На вашем Synology NAS (модель DS223j с DSM 7.2.2) обнаружено **572** различных API. Ниже перечислены ключевые категории API, которые можно использовать для расширения функциональности бота: + +### 1.1. API для управления питанием +- **SYNO.Core.Hardware.PowerRecovery** (v1) - основной API для управления питанием +- **SYNO.Core.Hardware.PowerSchedule** (v1) - API для настройки расписания включения/выключения +- **SYNO.Core.Hardware.NeedReboot** (v1) - API для перезагрузки системы + +### 1.2. API для системной информации +- **SYNO.DSM.Info** (v2) - основной API для получения общей информации о системе +- **SYNO.Core.System** (v1) - API для получения расширенной системной информации +- **SYNO.Core.System.Status** (v1) - API для получения статуса системы +- **SYNO.Core.System.Utilization** (v1) - API для получения сведений о загрузке системы + +### 1.3. API для хранилища и файлов +- **SYNO.Storage.CGI.Storage** - информация о хранилище и дисках +- **SYNO.FileStation.List** - получение списка файлов и папок + +### 1.4. API для мониторинга +- **SYNO.Core.System.Process** - информация о запущенных процессах +- **SYNO.Core.System.SystemHealth** - состояние здоровья системы + +## 2. Расширенные команды управления NAS + +Бот уже поддерживает следующие базовые команды: +- `/start` - Начало работы с ботом +- `/status` - Проверка текущего статуса NAS +- `/power` - Управление питанием NAS +- `/help` - Вывод справки + +Также реализованы расширенные команды: +- `/system` - Подробная информация о системе +- `/storage` - Информация о хранилище и дисках +- `/shares` - Список общих папок +- `/load` - Текущая нагрузка на систему +- `/security` - Статус безопасности системы +- `/checkapi` - Проверка доступных API Synology + +### 2.1. Рекомендуемые дополнительные команды + +На основе анализа доступных API предлагаю добавить следующие команды: + +#### 2.1.1. Управление питанием и расписанием +- `/schedule` - Управление расписанием включения/выключения NAS +- `/wakeup` - Немедленное включение NAS через Wake-on-LAN +- `/quickreboot` - Быстрая перезагрузка без запроса подтверждения + +#### 2.1.2. Мониторинг системы +- `/processes` - Просмотр активных процессов и их загрузки +- `/network` - Детальная информация о сетевых подключениях +- `/temperature` - Мониторинг температуры системы и дисков +- `/updates` - Проверка доступных обновлений для DSM + +#### 2.1.3. Управление файлами +- `/browse [путь]` - Просмотр файлов в указанной директории +- `/search [шаблон]` - Поиск файлов по шаблону +- `/quota` - Просмотр информации о квотах пользователей + +#### 2.1.4. Резервное копирование +- `/backup` - Управление задачами резервного копирования +- `/backupstatus` - Проверка статуса резервного копирования + +## 3. Возможности использования API + +### 3.1. Оптимизация текущего кода +- Обнаружено, что для успешного взаимодействия с API необходимы специальные HTTP-заголовки, имитирующие браузер +- API версии 3 показывает лучшую стабильность для базовых операций, чем версия 6 +- Для аутентификации рекомендуется использовать куки и форматы, совместимые с веб-интерфейсом + +### 3.2. Рекомендуемые настройки API +- **SYNOLOGY_POWER_API = SYNO.Core.Hardware.PowerRecovery** +- **SYNOLOGY_INFO_API = SYNO.DSM.Info** +- **SYNOLOGY_API_VERSION = 2** (вместо текущего значения 6) + +### 3.3. Новые функциональные возможности + +#### 3.3.1. Мониторинг производительности +```python +def get_performance_stats(): + """Получение детальной статистики производительности""" + result = api._make_api_request("SYNO.Core.System.Utilization", "get", version=1) + return result +``` + +#### 3.3.2. Управление сервисами +```python +def manage_services(service_name, action="status"): + """Управление системными сервисами (start/stop/restart/status)""" + result = api._make_api_request("SYNO.Core.Service", action, version=1, + params={"service": service_name}) + return result +``` + +#### 3.3.3. Просмотр журналов +```python +def get_system_logs(limit=20): + """Получение системных журналов""" + result = api._make_api_request("SYNO.Core.SyslogClient.Log", "list", version=1, + params={"limit": limit}) + return result +``` + +#### 3.3.4. Настройка расписания питания +```python +def set_power_schedule(days, time, action="boot"): + """Настройка расписания питания (boot/shutdown)""" + result = api._make_api_request("SYNO.Core.Hardware.PowerSchedule", "set", version=1, + params={"day": days, "time": time, "action": action}) + return result +``` + +## 4. Решение текущих проблем API + +### 4.1. Проблема с получением информации о хранилище +Текущий код использует заглушку для `get_storage_status()`. Рекомендуемая реализация: + +```python +def get_storage_status(self) -> Dict[str, Any]: + """Получение информации о хранилище""" + # Попробуем получить информацию о дисках + disk_result = self._make_api_request("SYNO.Storage.CGI.Storage", "load_info", version=1) + + if not disk_result: + # Пробуем альтернативный API + disk_result = self._make_api_request("SYNO.Core.Storage", "info", version=1) + + # Собираем результат + volumes = disk_result.get("volumes", []) + disks = disk_result.get("disks", []) + + total_size = sum(vol.get("size", {}).get("total", 0) for vol in volumes) + total_used = sum(vol.get("size", {}).get("used", 0) for vol in volumes) + + return { + "volumes": volumes, + "disks": disks, + "total_size": total_size, + "total_used": total_used + } +``` + +### 4.2. Проблема с получением списка общих папок + +```python +def get_shared_folders(self) -> List[Dict[str, Any]]: + """Получение списка общих папок""" + result = self._make_api_request("SYNO.FileStation.List", "list_share", version=2) + + if not result: + return [] + + return result.get("shares", []) +``` + +### 4.3. Проблема с получением информации о нагрузке + +```python +def get_system_load(self) -> Dict[str, Any]: + """Получение информации о нагрузке системы""" + result = self._make_api_request("SYNO.Core.System.Utilization", "get", version=1) + + if not result: + return {} + + return { + "cpu_load": result.get("cpu", {}).get("user", 0) + result.get("cpu", {}).get("system", 0), + "memory": result.get("memory", {}), + "network": result.get("network", {}) + } +``` + +## 5. Заключение + +Synology NAS предоставляет обширный набор API, которые можно использовать для создания полноценного Telegram-бота для управления и мониторинга. Основные рекомендации: + +1. Обновить версию API до более стабильной (v2-3 вместо v6) +2. Добавить специальные HTTP-заголовки для имитации веб-браузера +3. Использовать куки для сохранения сессии +4. Реализовать новые функции управления на основе доступных API +5. Добавить обработку ошибок для нестабильных API (особенно хранилища и общих папок) + +Дополнительно рекомендуется реализовать функции автоматического мониторинга и уведомления о важных событиях, таких как высокая температура, заканчивающееся место на дисках или необходимость обновления системы. diff --git a/DOCKER_DEPLOYMENT.md b/DOCKER_DEPLOYMENT.md new file mode 100644 index 0000000..4e5b666 --- /dev/null +++ b/DOCKER_DEPLOYMENT.md @@ -0,0 +1,218 @@ +# Synology Power Control Bot - Руководство по развертыванию в Docker + +## Подготовка к развертыванию + +Это руководство поможет вам развернуть бота для управления питанием Synology NAS в Docker-контейнере. Развертывание в Docker имеет следующие преимущества: +- Изоляция приложения и его зависимостей +- Простота управления и обновления +- Автоматический перезапуск при сбоях +- Возможность легкого переноса между системами + +## Предварительные требования + +1. **Установка Docker и Docker Compose**: + + **Для Ubuntu/Debian**: + ```bash + # Установка Docker + sudo apt update + sudo apt install apt-transport-https ca-certificates curl software-properties-common + curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg + echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null + sudo apt update + sudo apt install docker-ce docker-ce-cli containerd.io + + # Установка Docker Compose + sudo curl -L "https://github.com/docker/compose/releases/download/v2.15.1/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose + sudo chmod +x /usr/local/bin/docker-compose + ``` + + **Для CentOS/RHEL**: + ```bash + # Установка Docker + sudo yum install -y yum-utils + sudo yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo + sudo yum install docker-ce docker-ce-cli containerd.io + sudo systemctl start docker + sudo systemctl enable docker + + # Установка Docker Compose + sudo curl -L "https://github.com/docker/compose/releases/download/v2.15.1/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose + sudo chmod +x /usr/local/bin/docker-compose + ``` + + **Для Windows**: + - Скачайте и установите Docker Desktop с [официального сайта Docker](https://www.docker.com/products/docker-desktop/) + +2. **Настройка проекта**: + ```bash + # Клонирование репозитория (если используете Git) + git clone https://github.com/yourusername/synology_power_control_bot.git + cd synology_power_control_bot + + # Или распакуйте архив с исходным кодом + ``` + +## Конфигурация + +1. **Создайте файл .env**: + Создайте файл `.env` на основе `.env-example` и настройте его с вашими параметрами: + + ```bash + cp .env-example .env + nano .env # или любой другой текстовый редактор + ``` + + Заполните следующие параметры: + - `TELEGRAM_TOKEN`: Токен вашего Telegram-бота от @BotFather + - `ADMIN_USER_IDS`: ID пользователей Telegram с доступом к боту + - `SYNOLOGY_HOST`: IP-адрес вашего Synology NAS + - `SYNOLOGY_USERNAME` и `SYNOLOGY_PASSWORD`: Учетные данные для DSM + - `MAC_ADDRESS`: MAC-адрес Synology NAS для Wake-on-LAN + +## Развертывание + +### С использованием скриптов: + +**Linux**: +```bash +chmod +x deploy.sh +./deploy.sh +``` + +**Windows**: +``` +deploy.cmd +``` + +### Вручную с Docker Compose: + +1. **Сборка и запуск**: + ```bash + docker-compose up -d --build + ``` + +2. **Проверка статуса**: + ```bash + docker-compose ps + ``` + +3. **Просмотр логов**: + ```bash + docker-compose logs -f + ``` + +4. **Остановка**: + ```bash + docker-compose down + ``` + +## Управление контейнером + +### Перезапуск бота: +```bash +docker-compose restart +``` + +### Обновление: +```bash +# Остановка +docker-compose down + +# Обновление (если используете Git) +git pull + +# Пересборка и запуск +docker-compose up -d --build +``` + +### Резервное копирование данных: +Важные данные хранятся в томе `logs`, который можно скопировать: +```bash +# Создание бэкапа логов +tar -czvf synology_bot_logs_backup.tar.gz ./logs +``` + +## Проверка работоспособности + +После развертывания можно проверить состояние бота с помощью следующих команд: + +1. **Проверка статуса контейнера**: + ```bash + docker-compose ps + ``` + +2. **Проверка health-check**: + ```bash + curl http://localhost:8080/health + ``` + Должен вернуть `OK`. + +3. **Проверка логов**: + ```bash + docker-compose logs -f + ``` + Ищите строки с успешной инициализацией бота. + +## Решение проблем + +### Контейнер не запускается или сразу завершает работу +- Проверьте логи: `docker-compose logs -f` +- Проверьте файл `.env` на наличие всех необходимых параметров +- Убедитесь, что порт 8080 не занят другим приложением + +### Проблемы с подключением к Synology NAS +- Проверьте доступность NAS из контейнера: + ```bash + docker-compose exec synology-bot ping $SYNOLOGY_HOST + ``` +- Проверьте правильность учетных данных +- Убедитесь, что API DSM включено в настройках NAS + +### Telegram-бот не отвечает +- Проверьте корректность TELEGRAM_TOKEN +- Убедитесь, что бот запущен: `/start` в чате с ботом +- Проверьте, что ваш Telegram ID указан в ADMIN_USER_IDS + +## Автоматический запуск при перезагрузке сервера + +Docker и Docker Compose по умолчанию настроены на автоматический запуск контейнеров при перезагрузке системы благодаря параметру `restart: unless-stopped` в docker-compose.yml. + +Если эта опция не работает, вы можете настроить systemd: + +1. **Создайте файл сервиса**: + ```bash + sudo nano /etc/systemd/system/synology-bot.service + ``` + +2. **Добавьте следующее содержимое**: + ``` + [Unit] + Description=Synology Power Control Bot + Requires=docker.service + After=docker.service + + [Service] + Type=oneshot + RemainAfterExit=yes + WorkingDirectory=/path/to/synology_power_control_bot + ExecStart=/usr/local/bin/docker-compose up -d + ExecStop=/usr/local/bin/docker-compose down + TimeoutStartSec=0 + + [Install] + WantedBy=multi-user.target + ``` + +3. **Активируйте и запустите сервис**: + ```bash + sudo systemctl enable synology-bot.service + sudo systemctl start synology-bot.service + ``` + +## Безопасность + +- Не передавайте файл `.env` с учетными данными третьим лицам +- Регулярно меняйте пароль от DSM +- Ограничьте доступ к боту только доверенным пользователям +- Рассмотрите возможность запуска на выделенной сети или с дополнительными ограничениями доступа diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..e08df4b --- /dev/null +++ b/Dockerfile @@ -0,0 +1,23 @@ +FROM python:3.11-slim + +# Устанавливаем рабочую директорию +WORKDIR /app + +# Копируем файлы зависимостей +COPY requirements.txt . + +# Устанавливаем зависимости +RUN pip install --no-cache-dir -r requirements.txt + +# Копируем исходный код +COPY . . + +# Делаем entrypoint исполняемым +RUN chmod +x /app/entrypoint.sh + +# Указываем переменные окружения +ENV PYTHONPATH=/app +ENV PYTHONUNBUFFERED=1 + +# Используем entrypoint.sh +ENTRYPOINT ["/app/entrypoint.sh"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..473468e --- /dev/null +++ b/README.md @@ -0,0 +1,236 @@ +# Synology Power Control Bot + +Telegram-бот для удаленного управления питанием сетевого хранилища Synology NAS (DS223j и другие модели). + +## Возможности + +### Управление питанием +- ✅ Включение питания через Wake-on-LAN +- ✅ Выключение питания через API DSM +- ✅ Перезагрузка системы с отслеживанием статуса + +### Мониторинг системы +- ✅ Проверка онлайн статуса NAS +- ✅ Информация о системе (модель, версия DSM, время работы) +- ✅ Мониторинг загрузки CPU и памяти +- ✅ Данные о температуре и сетевой активности +- ✅ Статус хранилища и дисков +- ✅ Информация о безопасности системы +- ✅ Список активных процессов +- ✅ Мониторинг сетевых подключений + +### Управление данными +- ✅ Просмотр списка общих папок +- ✅ Информация о томах и дисках +- ✅ Статистика использования дисков +- ✅ Просмотр файлов и папок +- ✅ Поиск файлов +- ✅ Мониторинг квот пользователей + +### Безопасность +- ✅ Ограничение доступа по ID пользователей Telegram +- ✅ Безопасное хранение учетных данных + +### Дополнительные функции +- ✅ Мониторинг обновлений DSM и пакетов +- ✅ Управление расписанием питания +- ✅ Проверка статуса резервного копирования + +## Требования + +- Python 3.8+ +- Synology NAS с включенным WoL +- Учетная запись администратора DSM +- Telegram Bot API Token +- Доступ к порту API Synology DSM (обычно 5000 или 5001) + +## Установка и настройка + +### Метод 1: Локальный запуск + +1. Клонируйте репозиторий: + ```bash + git clone https://github.com/yourusername/synology_power_control_bot.git + cd synology_power_control_bot + ``` + +2. Установите зависимости: + ```bash + pip install -r requirements.txt + ``` + +3. Настройте параметры в файле `.env`: + ``` + # Telegram Bot Configuration + TELEGRAM_TOKEN=your_telegram_bot_token + ADMIN_USER_IDS=123456789,987654321 + + # Synology NAS Configuration + SYNOLOGY_HOST=192.168.1.100 + SYNOLOGY_PORT=5000 + SYNOLOGY_USERNAME=admin + SYNOLOGY_PASSWORD=your_password + SYNOLOGY_SECURE=False + SYNOLOGY_TIMEOUT=10 + + # Wake-on-LAN Configuration + SYNOLOGY_MAC=00:11:22:33:44:55 + WOL_PORT=9 + ``` + +4. Запустите бота: + ```bash + python run.py + ``` + +### Метод 2: Docker + +1. Убедитесь, что Docker и Docker Compose установлены в вашей системе. + +2. Клонируйте репозиторий: + ```bash + git clone https://github.com/yourusername/synology_power_control_bot.git + cd synology_power_control_bot + ``` + +3. Создайте файл `.env` на основе `.env-example` и заполните необходимыми значениями. + +4. Запустите скрипт развертывания: + ```bash + # Linux/macOS + chmod +x deploy.sh + ./deploy.sh + + # Windows + deploy.cmd + ``` + + Или запустите вручную: + ```bash + docker-compose up -d --build + ``` + +5. Проверьте статус: + ```bash + docker-compose ps + ``` + +6. Просмотр логов: + ```bash + docker-compose logs -f + ``` + +Дополнительная информация о Docker-развертывании доступна в файле [README_DOCKER.md](README_DOCKER.md). + +## Подготовка Synology NAS + +1. Включите Wake-on-LAN в настройках DSM: + - Панель управления > Сеть > Общие > Wake-on-LAN + +2. Убедитесь, что API DSM включено: + - Панель управления > Службы терминала и SNMP > Включить DSM API + +3. Узнайте MAC-адрес вашего NAS: + - Панель управления > Сеть > Сетевой интерфейс + +## Команды бота + +### Основные команды +- `/start` - Начало работы с ботом +- `/status` - Проверка текущего статуса NAS +- `/power` - Управление питанием NAS (включение, выключение, перезагрузка) +- `/help` - Вывод справочной информации + +### Информационные команды +- `/system` - Подробная информация о системе +- `/storage` - Информация о хранилище и дисках +- `/shares` - Список общих папок +- `/load` - Текущая нагрузка на систему +- `/security` - Статус безопасности системы +- `/temperature` - Температура устройства +- `/processes` - Список активных процессов +- `/network` - Сетевая информация + +### Расширенные команды +- `/schedule` - Расписание питания +- `/browse` - Просмотр файлов +- `/search <запрос>` - Поиск файлов +- `/updates` - Проверка обновлений +- `/backup` - Статус резервного копирования +- `/quota` - Квоты пользователей + +### Быстрые команды +- `/quickreboot` - Быстрая перезагрузка +- `/wakeup` - Пробуждение NAS (WOL) + +## Структура проекта + +``` +synology_power_control_bot/ +├── logs/ # Директория для логов +├── src/ # Исходный код +│ ├── api/ # Модули для работы с API +│ │ └── synology.py # API для работы с Synology NAS +│ ├── config/ # Модули конфигурации +│ │ └── config.py # Основная конфигурация +│ ├── handlers/ # Обработчики команд бота +│ │ └── command_handlers.py +│ ├── utils/ # Вспомогательные утилиты +│ │ └── logger.py # Настройка логирования +│ └── bot.py # Основной файл запуска бота +├── .env # Файл с переменными окружения +├── .env-example # Пример файла переменных окружения +├── .gitignore # Файл игнорирования Git +├── .dockerignore # Файлы, игнорируемые при сборке Docker-образа +├── Dockerfile # Инструкции для сборки Docker-образа +├── docker-compose.yml # Конфигурация Docker Compose +├── deploy.sh # Скрипт развёртывания для Linux +├── deploy.cmd # Скрипт развёртывания для Windows +├── entrypoint.sh # Скрипт для запуска в Docker +├── README.md # Основная документация +├── README_DOCKER.md # Документация по Docker-развёртыванию +└── requirements.txt # Зависимости проекта +``` + +## Безопасность + +Бот имеет систему авторизации на основе ID пользователей Telegram. Убедитесь, что указали правильные ID в переменной `ADMIN_USER_IDS` в файле `.env`. + +## Устранение неисправностей + +### Проблемы с подключением к Synology NAS + +1. Убедитесь, что NAS доступен по сети (можно проверить с помощью команды `ping`). +2. Проверьте правильность логина и пароля в `.env`. +3. Убедитесь, что DSM API включено в настройках NAS. + +### Проблемы с Docker + +1. Проверьте статус контейнера: `docker-compose ps` +2. Просмотрите логи: `docker-compose logs -f` +3. Перезапустите контейнер: `docker-compose restart` +4. Проверьте состояние здоровья: `docker inspect --format="{{json .State.Health}}" synology-power-control-bot` +5. Проверьте, что все переменные окружения корректно переданы в контейнер. + +### Обновление в Docker + +Для обновления бота в Docker: + +1. Остановите контейнеры: + ```bash + docker-compose down + ``` + +2. Загрузите обновления (если используете Git): + ```bash + git pull + ``` + +3. Запустите контейнеры заново: + ```bash + docker-compose up -d --build + ``` + +## Лицензия + +MIT diff --git a/README_DOCKER.md b/README_DOCKER.md new file mode 100644 index 0000000..6bd6f3f --- /dev/null +++ b/README_DOCKER.md @@ -0,0 +1,106 @@ +# Synology Power Control Bot - Docker Deployment + +## Подготовка к развертыванию + +Перед развертыванием в Docker убедитесь, что: + +1. Docker и Docker Compose установлены в вашей системе. +2. Файл `.env` настроен с правильными значениями. + +## Структура проекта для Docker + +``` +synology_power_control_bot/ +├── src/ # Исходный код бота +├── logs/ # Папка для логов (будет смонтирована как том) +├── .env # Файл с переменными окружения +├── requirements.txt # Зависимости Python +├── Dockerfile # Инструкции для сборки образа +├── docker-compose.yml # Конфигурация Docker Compose +└── run.py # Точка входа +``` + +## Настройка переменных окружения + +Убедитесь, что файл `.env` содержит все необходимые переменные: + +``` +# Telegram Bot API +TELEGRAM_TOKEN=your_telegram_bot_token +ADMIN_USER_IDS=123456789,987654321 # ID пользователей-администраторов через запятую + +# Synology NAS +SYNOLOGY_HOST=192.168.1.100 +SYNOLOGY_PORT=5000 # Обычно 5000 для HTTP и 5001 для HTTPS +SYNOLOGY_USERNAME=your_username +SYNOLOGY_PASSWORD=your_password +SYNOLOGY_SECURE=True # Использовать HTTPS +SYNOLOGY_VERIFY_SSL=False # Проверка SSL-сертификата +SYNOLOGY_TIMEOUT=10 # Таймаут для API запросов в секундах +SYNOLOGY_API_VERSION=1 # Версия API +SYNOLOGY_POWER_API=SYNO.Core.System # API для управления питанием + +# WOL (Wake-on-LAN) +MAC_ADDRESS=00:11:22:33:44:55 # MAC-адрес Synology NAS +WOL_BROADCAST=255.255.255.255 # Broadcast-адрес для WOL +WOL_PORT=9 # Порт для WOL (обычно 7 или 9) + +# Logging +LOG_LEVEL=INFO +``` + +## Сборка и запуск + +### Сборка и запуск контейнеров + +```bash +docker-compose up -d --build +``` + +### Просмотр логов + +```bash +docker-compose logs -f +``` + +### Остановка контейнеров + +```bash +docker-compose down +``` + +## Обновление + +Для обновления бота: + +1. Остановите контейнеры: + ```bash + docker-compose down + ``` + +2. Скачайте последние изменения (если используете Git): + ```bash + git pull + ``` + +3. Соберите и запустите контейнеры заново: + ```bash + docker-compose up -d --build + ``` + +## Устранение неполадок + +### Проверка статуса контейнера +```bash +docker-compose ps +``` + +### Проверка логов контейнера +```bash +docker-compose logs -f synology-bot +``` + +### Подключение к контейнеру +```bash +docker-compose exec synology-bot bash +``` diff --git a/deploy.cmd b/deploy.cmd new file mode 100644 index 0000000..6ded549 --- /dev/null +++ b/deploy.cmd @@ -0,0 +1,45 @@ +@echo off +REM deploy.cmd - Скрипт для развертывания Synology Power Control Bot на Windows + +echo Проверка наличия Docker... +where docker >nul 2>&1 +if %ERRORLEVEL% NEQ 0 ( + echo Ошибка: Docker не установлен. + echo Установите Docker Desktop, следуя инструкциям: https://docs.docker.com/desktop/windows/install/ + exit /b 1 +) + +echo Проверка наличия Docker Compose... +where docker-compose >nul 2>&1 +if %ERRORLEVEL% NEQ 0 ( + docker compose version >nul 2>&1 + if %ERRORLEVEL% NEQ 0 ( + echo Ошибка: Docker Compose не установлен. + echo Установите Docker Desktop, следуя инструкциям: https://docs.docker.com/desktop/windows/install/ + exit /b 1 + ) +) + +echo Проверка файла .env... +if not exist .env ( + echo Ошибка: Файл .env не найден. + echo Создайте файл .env с необходимыми переменными окружения. + exit /b 1 +) + +echo Создание директории для логов... +if not exist logs mkdir logs + +echo Сборка и запуск Docker контейнеров... +docker-compose down +docker-compose up -d --build + +echo Проверка статуса контейнеров... +docker-compose ps + +echo. +echo Развертывание завершено успешно! +echo Для просмотра логов: docker-compose logs -f +echo Для остановки: docker-compose down + +pause diff --git a/deploy.sh b/deploy.sh new file mode 100644 index 0000000..fc7e1aa --- /dev/null +++ b/deploy.sh @@ -0,0 +1,49 @@ +#!/bin/bash +# deploy.sh - Скрипт для развертывания Synology Power Control Bot + +# Цвета для вывода +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +RED='\033[0;31m' +NC='\033[0m' # No Color + +# Проверяем наличие Docker и Docker Compose +echo -e "${YELLOW}Проверка наличия Docker...${NC}" +if ! [ -x "$(command -v docker)" ]; then + echo -e "${RED}Ошибка: Docker не установлен.${NC}" >&2 + echo -e "Установите Docker, следуя инструкциям: https://docs.docker.com/get-docker/" + exit 1 +fi + +echo -e "${YELLOW}Проверка наличия Docker Compose...${NC}" +if ! [ -x "$(command -v docker-compose)" ] && ! [ -x "$(command -v docker compose)" ]; then + echo -e "${RED}Ошибка: Docker Compose не установлен.${NC}" >&2 + echo -e "Установите Docker Compose, следуя инструкциям: https://docs.docker.com/compose/install/" + exit 1 +fi + +# Проверяем наличие файла .env +echo -e "${YELLOW}Проверка файла .env...${NC}" +if [ ! -f ".env" ]; then + echo -e "${RED}Ошибка: Файл .env не найден.${NC}" >&2 + echo -e "Создайте файл .env с необходимыми переменными окружения." + exit 1 +fi + +# Создаем директорию для логов +echo -e "${YELLOW}Создание директории для логов...${NC}" +mkdir -p logs +chmod 777 logs + +# Сборка и запуск Docker контейнеров +echo -e "${YELLOW}Сборка и запуск Docker контейнеров...${NC}" +docker-compose down +docker-compose up -d --build + +# Проверка статуса контейнеров +echo -e "${YELLOW}Проверка статуса контейнеров...${NC}" +docker-compose ps + +echo -e "${GREEN}Развертывание завершено успешно!${NC}" +echo -e "Для просмотра логов: ${YELLOW}docker-compose logs -f${NC}" +echo -e "Для остановки: ${YELLOW}docker-compose down${NC}" diff --git a/diagnose_api.py b/diagnose_api.py new file mode 100644 index 0000000..2ae4b6a --- /dev/null +++ b/diagnose_api.py @@ -0,0 +1,91 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Диагностический скрипт для определения совместимых API +""" + +import os +import sys +import logging +import argparse +from pathlib import Path + +# Добавляем родительскую директорию в sys.path +parent_dir = str(Path(__file__).resolve().parent.parent) +if parent_dir not in sys.path: + sys.path.insert(0, parent_dir) + +from src.api.api_discovery import discover_available_apis +from src.config.config import SYNOLOGY_HOST, SYNOLOGY_PORT, SYNOLOGY_SECURE + +# Настройка логгера +logging.basicConfig( + level=logging.DEBUG, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' +) +logger = logging.getLogger(__name__) + +def main(): + """Точка входа для диагностического скрипта""" + parser = argparse.ArgumentParser(description='Synology API Diagnostic Tool') + parser.add_argument('--host', help='Synology host address', default=SYNOLOGY_HOST) + parser.add_argument('--port', type=int, help='Synology host port', default=SYNOLOGY_PORT) + parser.add_argument('--secure', action='store_true', help='Use HTTPS', default=SYNOLOGY_SECURE) + + args = parser.parse_args() + + protocol = "https" if args.secure else "http" + base_url = f"{protocol}://{args.host}:{args.port}/webapi" + + print(f"Scanning APIs at {base_url}...") + + apis = discover_available_apis(base_url) + + if not apis: + print("No APIs were discovered. Check connection parameters.") + return + + print(f"Discovered {len(apis)} APIs") + + # Анализ результатов + + # 1. Ищем API для управления питанием + print("\nPower Management APIs:") + power_apis = [name for name in apis.keys() if "power" in name.lower()] + for api in power_apis: + info = apis[api] + print(f" - {api} (v{info.get('minVersion', 1)}-{info.get('maxVersion', 1)}), path: {info.get('path', 'entry.cgi')}") + + # 2. Ищем API для информации о системе + print("\nSystem Information APIs:") + system_info_apis = [name for name in apis.keys() if ("system" in name.lower() or "dsm" in name.lower()) and "info" in name.lower()] + for api in system_info_apis: + info = apis[api] + print(f" - {api} (v{info.get('minVersion', 1)}-{info.get('maxVersion', 1)}), path: {info.get('path', 'entry.cgi')}") + + # 3. Ищем API для перезагрузки + print("\nReboot/Restart APIs:") + reboot_apis = [name for name in apis.keys() if any(word in name.lower() for word in ["restart", "reboot", "power"])] + for api in reboot_apis: + info = apis[api] + print(f" - {api} (v{info.get('minVersion', 1)}-{info.get('maxVersion', 1)}), path: {info.get('path', 'entry.cgi')}") + + print("\nRecommended API Settings:") + + if power_apis: + recommended_power_api = max(power_apis, key=lambda x: apis[x].get('maxVersion', 1)) + print(f"Power API: {recommended_power_api}, version: {apis[recommended_power_api].get('maxVersion', 1)}") + else: + print("Power API: Not found, falling back to SYNO.Core.System") + + if system_info_apis: + recommended_info_api = max(system_info_apis, key=lambda x: apis[x].get('maxVersion', 1)) + print(f"System Info API: {recommended_info_api}, version: {apis[recommended_info_api].get('maxVersion', 1)}") + else: + print("System Info API: Not found, falling back to SYNO.DSM.Info") + + print("\nThese settings should be added to your .env file.") + +if __name__ == "__main__": + main() diff --git a/direct_api_test.py b/direct_api_test.py new file mode 100644 index 0000000..ca9c245 --- /dev/null +++ b/direct_api_test.py @@ -0,0 +1,293 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Тестовый скрипт для прямого доступа к API Synology для получения информации о системе. +Используется для отладки и определения совместимых API. +""" + +import requests +import logging +import json +import sys +import os +import urllib3 +from requests.adapters import HTTPAdapter +from urllib3.util import Retry + +# Добавляем корневой каталог в путь для импорта +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +from src.config.config import ( + SYNOLOGY_HOST, + SYNOLOGY_PORT, + SYNOLOGY_USERNAME, + SYNOLOGY_PASSWORD, + SYNOLOGY_SECURE +) + +# Отключение предупреждений о небезопасных SSL-соединениях +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + +# Настройка логирования +logging.basicConfig( + level=logging.DEBUG, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' +) +logger = logging.getLogger(__name__) + +def direct_api_test(): + """Прямой тест API без использования классов для определения проблемы""" + # Создаем базовую сессию + session = requests.Session() + session.verify = False # Отключаем проверку SSL + + # Добавляем повторные попытки для HTTP-запросов + retry_strategy = Retry( + total=3, + status_forcelist=[429, 500, 502, 503, 504], + allowed_methods=["GET", "POST"], + backoff_factor=1.0 + ) + adapter = HTTPAdapter(max_retries=retry_strategy) + session.mount("http://", adapter) + session.mount("https://", adapter) + + # Формируем базовый URL + protocol = "https" if SYNOLOGY_SECURE else "http" + base_url = f"{protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi" + logger.info(f"Тестирование прямого API доступа к {base_url}") + + # Шаг 1: Авторизация + logger.info("Шаг 1: Попытка авторизации...") + + # Сначала получаем информацию об API авторизации + api_info_url = f"{base_url}/entry.cgi" + api_info_params = { + "api": "SYNO.API.Info", + "version": "1", + "method": "query", + "query": "SYNO.API.Auth" + } + + try: + auth_info_response = session.get(api_info_url, params=api_info_params, timeout=10) + auth_info_data = auth_info_response.json() + + if auth_info_data.get("success"): + auth_info = auth_info_data.get("data", {}).get("SYNO.API.Auth", {}) + auth_path = auth_info.get("path", "auth.cgi") + auth_max_version = auth_info.get("maxVersion", 6) + + logger.info(f"API авторизации: путь={auth_path}, макс. версия={auth_max_version}") + + # Пробуем версию 6 или максимальную доступную + auth_version = min(6, auth_max_version) + + # Выполняем авторизацию + auth_url = f"{base_url}/{auth_path}" + auth_params = { + "api": "SYNO.API.Auth", + "version": str(auth_version), + "method": "login", + "account": SYNOLOGY_USERNAME, + "passwd": SYNOLOGY_PASSWORD, + "session": "DirectApiTest", + "format": "cookie" + } + + # Для версии 6+ используем немного другой формат + if auth_version >= 6: + auth_params["enable_syno_token"] = "yes" + + logger.info(f"Авторизация с использованием SYNO.API.Auth v{auth_version}") + auth_response = session.get(auth_url, params=auth_params, timeout=10) + auth_data = auth_response.json() + + if auth_data.get("success"): + sid = auth_data.get("data", {}).get("sid") + logger.info(f"Авторизация успешна! SID: {sid[:10]}...") + + # Шаг 2: Тестирование различных API для получения информации о системе + logger.info("Шаг 2: Тестирование различных API для получения информации о системе") + + # Создаем список API для тестирования + api_to_test = [ + {"name": "SYNO.DSM.Info", "method": "getinfo", "version": 1}, + {"name": "SYNO.DSM.Info", "method": "getinfo", "version": 2}, + {"name": "SYNO.Core.System", "method": "info", "version": 1}, + {"name": "SYNO.Core.System", "method": "info", "version": 2}, + {"name": "SYNO.Core.System.Status", "method": "get", "version": 1}, + {"name": "SYNO.Core.System.Status", "method": "get", "version": 2}, + {"name": "SYNO.Core.System.Utilization", "method": "get", "version": 1}, + {"name": "SYNO.Core.CurrentConnection", "method": "list", "version": 1} + ] + + # Перебираем все API и тестируем их + for api in api_to_test: + # Сначала получаем информацию о конкретном API + try: + api_info_params = { + "api": "SYNO.API.Info", + "version": "1", + "method": "query", + "query": api["name"] + } + + api_info_resp = session.get(api_info_url, params=api_info_params, timeout=10) + api_info_data = api_info_resp.json() + + if api_info_data.get("success") and api["name"] in api_info_data.get("data", {}): + api_details = api_info_data["data"][api["name"]] + api_path = api_details.get("path", "entry.cgi") + api_min_version = api_details.get("minVersion", 1) + api_max_version = api_details.get("maxVersion", 1) + + # Проверяем, поддерживается ли указанная версия + if api["version"] < api_min_version: + logger.warning(f"{api['name']} v{api['version']} ниже минимальной {api_min_version}, используем {api_min_version}") + test_version = api_min_version + elif api["version"] > api_max_version: + logger.warning(f"{api['name']} v{api['version']} выше максимальной {api_max_version}, используем {api_max_version}") + test_version = api_max_version + else: + test_version = api["version"] + + # Выполняем запрос API + test_url = f"{base_url}/{api_path}" + test_params = { + "api": api["name"], + "version": str(test_version), + "method": api["method"], + "_sid": sid # Используем sid для аутентификации + } + + logger.info(f"Тестирование {api['name']}.{api['method']} v{test_version}") + test_response = session.get(test_url, params=test_params, timeout=10) + test_data = test_response.json() + + if test_data.get("success"): + logger.info(f"API {api['name']}.{api['method']} v{test_version} РАБОТАЕТ!") + logger.info(f"Результат: {json.dumps(test_data.get('data', {}), indent=2)[:200]}...") + else: + error_code = test_data.get("error", {}).get("code", -1) + logger.error(f"API {api['name']}.{api['method']} v{test_version} ОШИБКА: {error_code}") + + # Если ошибка связана с сессией, попробуем еще раз авторизоваться + if error_code == 119: # Session timeout + logger.info("Повторная авторизация из-за ошибки 119...") + + # Создаем новую сессию + new_session = requests.Session() + new_session.verify = False + + auth_response = new_session.get(auth_url, params=auth_params, timeout=10) + auth_data = auth_response.json() + + if auth_data.get("success"): + new_sid = auth_data.get("data", {}).get("sid") + logger.info(f"Повторная авторизация успешна! Новый SID: {new_sid[:10]}...") + + # Пробуем запрос с новым SID + test_params["_sid"] = new_sid + logger.info(f"Повторное тестирование {api['name']}.{api['method']} v{test_version}") + test_response = new_session.get(test_url, params=test_params, timeout=10) + test_data = test_response.json() + + if test_data.get("success"): + logger.info(f"API {api['name']}.{api['method']} v{test_version} теперь РАБОТАЕТ!") + logger.info(f"Результат с новой сессией: {json.dumps(test_data.get('data', {}), indent=2)[:200]}...") + else: + error_code = test_data.get("error", {}).get("code", -1) + logger.error(f"API {api['name']}.{api['method']} v{test_version} ВСЕ ЕЩЕ С ОШИБКОЙ: {error_code}") + else: + logger.warning(f"API {api['name']} не найден в информации API") + + except Exception as e: + logger.error(f"Ошибка при тестировании {api['name']}.{api['method']} v{api['version']}: {str(e)}") + + # Шаг 3: Тестирование комбинации запросов для решения проблемы + logger.info("Шаг 3: Тестирование комбинации запросов для решения проблемы") + + # Создаем новую сессию для каждого запроса + for api in [{"name": "SYNO.DSM.Info", "method": "getinfo", "version": 1}]: + try: + fresh_session = requests.Session() + fresh_session.verify = False + + # Авторизуемся + auth_response = fresh_session.get(auth_url, params=auth_params, timeout=10) + auth_data = auth_response.json() + + if auth_data.get("success"): + fresh_sid = auth_data.get("data", {}).get("sid") + logger.info(f"Авторизация в новой сессии успешна! SID: {fresh_sid[:10]}...") + + # Сразу же делаем запрос для получения информации в той же сессии + test_params = { + "api": api["name"], + "version": str(api["version"]), + "method": api["method"], + "_sid": fresh_sid + } + + test_url = f"{base_url}/entry.cgi" # Используем entry.cgi по умолчанию + logger.info(f"Тест в свежей сессии: {api['name']}.{api['method']} v{api['version']}") + test_response = fresh_session.get(test_url, params=test_params, timeout=10) + test_data = test_response.json() + + if test_data.get("success"): + logger.info(f"API в свежей сессии РАБОТАЕТ!") + logger.info(f"Результат: {json.dumps(test_data.get('data', {}), indent=2)[:200]}...") + else: + error_code = test_data.get("error", {}).get("code", -1) + logger.error(f"API в свежей сессии ОШИБКА: {error_code}") + except Exception as e: + logger.error(f"Ошибка при тестировании свежей сессии: {str(e)}") + + # Шаг 4: Получаем информацию об остальных API + logger.info("Шаг 4: Получаем информацию о доступных API для уточнения проблемы") + + # Запрашиваем все API из SYNO.API.Info + try: + all_api_params = { + "api": "SYNO.API.Info", + "version": "1", + "method": "query", + "query": "all" + } + + all_api_response = session.get(api_info_url, params=all_api_params, timeout=15) # Больший таймаут для большого ответа + all_api_data = all_api_response.json() + + if all_api_data.get("success"): + api_list = all_api_data.get("data", {}) + logger.info(f"Получен список всех API. Найдено {len(api_list)} API.") + + # Ищем интересующие нас API для отладки + interested_in = ["SYNO.DSM.Info", "SYNO.Core.System", "SYNO.Core.Hardware", + "SYNO.Core.System.Status", "SYNO.API.Auth"] + + logger.info("Информация о важных API:") + for api_name in interested_in: + if api_name in api_list: + logger.info(f"{api_name}: {api_list[api_name]}") + else: + logger.warning(f"API {api_name} не найден") + else: + logger.error("Не удалось получить список всех API") + except Exception as e: + logger.error(f"Ошибка при получении списка API: {str(e)}") + + else: + error_code = auth_data.get("error", {}).get("code", -1) + logger.error(f"Авторизация не удалась! Код ошибки: {error_code}") + else: + logger.error("Не удалось получить информацию об API авторизации") + + except Exception as e: + logger.error(f"Произошла ошибка при выполнении теста: {str(e)}") + +if __name__ == "__main__": + logger.info("Запуск прямого теста API Synology") + direct_api_test() diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..c35284c --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,38 @@ +version: '3.8' + +services: + synology-bot: + build: + context: . + dockerfile: Dockerfile + container_name: synology-power-control-bot + restart: unless-stopped + env_file: + - .env + environment: + - DOCKER_ENV=true + volumes: + - ./logs:/app/logs + # Если у вас есть файлы конфигурации или данные, которые нужно сохранять: + # - ./data:/app/data + healthcheck: + test: ["CMD", "python", "-c", "import requests; requests.get('http://localhost:8080/health', timeout=5)"] + interval: 60s + timeout: 10s + retries: 3 + start_period: 20s + networks: + - bot-network + # Для ограничения ресурсов (раскомментируйте и настройте при необходимости): + # deploy: + # resources: + # limits: + # cpus: '0.50' + # memory: 512M + # reservations: + # cpus: '0.25' + # memory: 256M + +networks: + bot-network: + driver: bridge diff --git a/entrypoint.sh b/entrypoint.sh new file mode 100644 index 0000000..b4de286 --- /dev/null +++ b/entrypoint.sh @@ -0,0 +1,7 @@ +#!/bin/sh + +# Создаем директорию для логов, если она не существует +mkdir -p /app/logs + +# Запускаем бота +exec python /app/run.py diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..4703994 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,6 @@ +python-telegram-bot>=20.0 +requests>=2.28.0 +python-dotenv>=1.0.0 +urllib3>=2.0.0 +aiohttp>=3.8.4 +async-timeout>=4.0.2 diff --git a/run.py b/run.py new file mode 100644 index 0000000..b695a8d --- /dev/null +++ b/run.py @@ -0,0 +1,18 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Точка входа для запуска телеграм-бота +""" + +import os +from src.bot import main +from src.healthcheck import start_health_server + +if __name__ == "__main__": + # Запускаем healthcheck сервер в Docker-окружении + if os.environ.get("DOCKER_ENV", "False").lower() == "true": + start_health_server() + + # Запускаем основной бот + main() diff --git a/run_bot.py b/run_bot.py new file mode 100644 index 0000000..737f58d --- /dev/null +++ b/run_bot.py @@ -0,0 +1,11 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Файл-обёртка для запуска бота из корневой директории +""" + +from src.bot import main + +if __name__ == "__main__": + main() diff --git a/src/api/api_discovery.py b/src/api/api_discovery.py new file mode 100644 index 0000000..641dc8c --- /dev/null +++ b/src/api/api_discovery.py @@ -0,0 +1,113 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Модуль для обнаружения доступных API Synology NAS +""" + +import logging +import requests +import urllib3 +from typing import Dict, Any, List, Optional + +# Отключение предупреждений о небезопасных SSL-соединениях +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + +logger = logging.getLogger(__name__) + +def discover_available_apis(base_url: str, timeout=(10, 20), verify=False) -> Dict[str, Any]: + """ + Получение списка доступных API на Synology NAS + + Args: + base_url: базовый URL для API (например, 'http://192.168.0.100:5000/webapi') + timeout: таймаут для запроса + verify: проверять ли SSL-сертификат + + Returns: + Словарь с информацией о доступных API + """ + logger.info("Discovering available Synology APIs") + + try: + # Делаем базовый запрос для получения всех доступных API + api_info_url = f"{base_url}/entry.cgi" + api_info_params = { + "api": "SYNO.API.Info", + "version": "1", + "method": "query", + "query": "all" + } + + response = requests.get( + api_info_url, + params=api_info_params, + timeout=timeout, + verify=verify + ) + + if response.status_code == 200: + data = response.json() + if data.get("success"): + apis = data.get("data", {}) + logger.info(f"Discovered {len(apis)} APIs") + + # Выводим список найденных API + api_list = list(apis.keys()) + logger.debug(f"Available APIs: {', '.join(api_list[:10])}... and {len(api_list) - 10} more") + + # Группируем API по категориям + power_apis = [api for api in api_list if "power" in api.lower()] + system_apis = [api for api in api_list if "system" in api.lower()] + info_apis = [api for api in api_list if "info" in api.lower()] + + logger.info(f"Power related APIs: {power_apis}") + logger.info(f"System related APIs: {system_apis[:10]}") + logger.info(f"Info related APIs: {info_apis[:10]}") + + return apis + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to discover APIs. Error code: {error_code}") + return {} + else: + logger.error(f"API discovery request failed with HTTP status: {response.status_code}") + return {} + except Exception as e: + logger.error(f"Error during API discovery: {str(e)}") + return {} + +def find_compatible_api(apis: Dict[str, Any], api_category: str, method: str) -> List[Dict[str, Any]]: + """ + Поиск совместимых API заданной категории + + Args: + apis: словарь с доступными API + api_category: категория API (например, 'system', 'power', 'info') + method: искомый метод API + + Returns: + Список подходящих API с версиями + """ + compatible_apis = [] + + for api_name, api_info in apis.items(): + if api_category.lower() in api_name.lower(): + compatible_apis.append({ + "name": api_name, + "path": api_info.get("path", "entry.cgi"), + "min_version": api_info.get("minVersion", 1), + "max_version": api_info.get("maxVersion", 1), + "method": method, + "version": api_info.get("maxVersion", 1) # Используем максимальную версию по умолчанию + }) + + # Сортируем по приоритету + compatible_apis.sort(key=lambda x: ( + # Приоритет по точности совпадения категории + 0 if api_category.upper() in x["name"] else 1, + # Приоритет по версии (от большей к меньшей) + -x["max_version"] + )) + + return compatible_apis diff --git a/src/api/api_version_resolver.py b/src/api/api_version_resolver.py new file mode 100644 index 0000000..64a4003 --- /dev/null +++ b/src/api/api_version_resolver.py @@ -0,0 +1,234 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Модуль для разрешения проблем с API Synology и автоматического выбора совместимых версий +""" + +import logging +import requests +from typing import Dict, Any, Optional, List, Tuple + +logger = logging.getLogger(__name__) + +class ApiVersionResolver: + """Класс для определения совместимых версий API и правильных методов""" + + def __init__(self, base_url: str, session: requests.Session, timeout: tuple = (10, 20)): + """Инициализация класса ApiVersionResolver + + Args: + base_url: Базовый URL API Synology NAS (например, http://192.168.0.102:5000/webapi) + session: Сессия requests для повторного использования соединений + timeout: Таймауты для запросов (connect_timeout, read_timeout) + """ + self.base_url = base_url + self.session = session + self.timeout = timeout + self.api_info_cache = {} + + def get_api_info(self, api_name: str) -> Dict[str, Any]: + """Получает информацию об API из SYNO.API.Info + + Args: + api_name: Имя API для запроса (например, SYNO.DSM.Info) + + Returns: + Dict с информацией об API или пустой словарь в случае ошибки + """ + # Проверяем наличие данных в кэше + if api_name in self.api_info_cache: + return self.api_info_cache[api_name] + + try: + # Запрос информации об API + api_info_url = f"{self.base_url}/entry.cgi" + api_info_params = { + "api": "SYNO.API.Info", + "version": "1", + "method": "query", + "query": api_name + } + + logger.debug(f"Querying API info for {api_name}") + response = self.session.get( + api_info_url, + params=api_info_params, + timeout=self.timeout, + verify=False + ) + + if response.status_code != 200: + logger.warning(f"API info request failed with status {response.status_code}") + return {} + + data = response.json() + if not data.get("success"): + logger.warning(f"API info request unsuccessful for {api_name}") + return {} + + # Извлекаем информацию о запрошенном API + api_info = data.get("data", {}).get(api_name, {}) + if not api_info: + logger.warning(f"API {api_name} not found in API info response") + return {} + + # Кэшируем результат + self.api_info_cache[api_name] = api_info + logger.debug(f"API info for {api_name}: {api_info}") + return api_info + + except Exception as e: + logger.error(f"Error querying API info for {api_name}: {str(e)}") + return {} + + def resolve_api_path(self, api_name: str) -> str: + """Определяет путь для API + + Args: + api_name: Имя API + + Returns: + Путь к API или 'entry.cgi' по умолчанию + """ + api_info = self.get_api_info(api_name) + return api_info.get("path", "entry.cgi") + + def resolve_api_version(self, api_name: str, requested_version: int) -> int: + """Определяет совместимую версию API + + Args: + api_name: Имя API + requested_version: Запрошенная версия API + + Returns: + Совместимая версия API, которая будет работать + """ + api_info = self.get_api_info(api_name) + if not api_info: + # Если нет информации, возвращаем запрошенную версию + return requested_version + + min_version = api_info.get("minVersion", 1) + max_version = api_info.get("maxVersion", requested_version) + + # Проверка, поддерживается ли запрошенная версия + if requested_version < min_version: + logger.warning(f"API version {requested_version} for {api_name} is below minimum {min_version}, using {min_version}") + return min_version + elif requested_version > max_version: + logger.warning(f"API version {requested_version} for {api_name} exceeds maximum {max_version}, using {max_version}") + return max_version + + return requested_version + + def resolve_api_method(self, api_name: str) -> Dict[str, str]: + """Определяет доступные методы для API + + Args: + api_name: Имя API + + Returns: + Словарь с типами методов и их правильными именами для данного API + """ + # Возможные методы для разных типов API + api_methods = { + # Методы для информации о системе + "SYNO.DSM.Info": {"info": "getinfo", "get": "getinfo"}, + "SYNO.Core.System": {"info": "info", "get": "info"}, + "SYNO.Core.System.Status": {"info": "get", "get": "get"}, + "SYNO.Core.System.Info": {"info": "get", "get": "get"}, + + # Методы для управления питанием + "SYNO.Core.Hardware.PowerRecovery": { + "restart": "setPowerOnState", + "reboot": "setPowerOnState", + "shutdown": "setPowerOnState", + "poweroff": "setPowerOnState" + }, + "SYNO.Core.System.Power": { + "restart": "restart", + "reboot": "restart", + "shutdown": "shutdown", + "poweroff": "shutdown" + }, + "SYNO.DSM.Power": { + "restart": "reboot", + "reboot": "reboot", + "shutdown": "shutdown", + "poweroff": "shutdown" + }, + "SYNO.Core.Hardware.NeedReboot": { + "restart": "reboot", + "reboot": "reboot" + } + } + + return api_methods.get(api_name, {}) + + def get_api_special_params(self, api_name: str, method: str) -> Dict[str, Any]: + """Возвращает специальные параметры, которые требуются для определенного API + + Args: + api_name: Имя API + method: Метод API + + Returns: + Словарь с параметрами для метода или пустой словарь + """ + # Специфические параметры для определенных API + special_params = { + # Параметры для управления питанием + "SYNO.Core.Hardware.PowerRecovery": { + "setPowerOnState": { + "restart": {"reboot": "true"}, + "reboot": {"reboot": "true"}, + "shutdown": {"state": "powerbtn"}, + "poweroff": {"state": "powerbtn"} + } + }, + # Другие специальные параметры для других API + } + + api_params = special_params.get(api_name, {}) + method_params = api_params.get(method, {}) + + # Если это метод управления питанием, возвращаем соответствующие параметры + if isinstance(method_params, dict) and method in method_params: + return method_params[method] + + return method_params + + def find_compatible_api_for_function(self, function_type: str) -> List[Tuple[str, str, int]]: + """Находит совместимые API для определенного типа функций + + Args: + function_type: Тип функции ('info', 'power', 'status', etc.) + + Returns: + Список кортежей (api_name, method, version) в порядке приоритета + """ + # Определяем API для каждого типа функции + function_apis = { + "info": [ + ("SYNO.DSM.Info", "getinfo", 2), + ("SYNO.Core.System", "info", 1), + ("SYNO.Core.System.Status", "get", 1), + ("SYNO.Core.System.Info", "get", 1) + ], + "power_restart": [ + ("SYNO.Core.Hardware.PowerRecovery", "setPowerOnState", 1), + ("SYNO.Core.Hardware.NeedReboot", "reboot", 1), + ("SYNO.Core.System.Power", "restart", 1), + ("SYNO.DSM.Power", "reboot", 1), + ("SYNO.Core.System", "reboot", 3) + ], + "power_shutdown": [ + ("SYNO.Core.Hardware.PowerRecovery", "setPowerOnState", 1), + ("SYNO.Core.System.Power", "shutdown", 1), + ("SYNO.DSM.Power", "shutdown", 1), + ("SYNO.Core.System", "shutdown", 3) + ] + } + + return function_apis.get(function_type, []) diff --git a/src/api/synology.py b/src/api/synology.py new file mode 100644 index 0000000..6d5a673 --- /dev/null +++ b/src/api/synology.py @@ -0,0 +1,1861 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Модуль для взаимодействия с API Synology NAS +""" + +import requests +from requests.adapters import HTTPAdapter +import json +import logging +import time +import urllib3 +from urllib3.util import Retry +from typing import Dict, Any, Optional, List +import socket +import struct +from time import sleep + +from src.config.config import ( + SYNOLOGY_HOST, + SYNOLOGY_PORT, + SYNOLOGY_USERNAME, + SYNOLOGY_PASSWORD, + SYNOLOGY_SECURE, + SYNOLOGY_TIMEOUT, + SYNOLOGY_MAC, + WOL_PORT, + SYNOLOGY_API_VERSION, + SYNOLOGY_POWER_API, + SYNOLOGY_INFO_API +) +from src.api.api_discovery import discover_available_apis, find_compatible_api + +# Отключение предупреждений о небезопасных SSL-соединениях +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + +logger = logging.getLogger(__name__) + +class SynologyAPI: + """Класс для взаимодействия с API Synology NAS""" + + def __init__(self): + """Инициализация класса SynologyAPI""" + logger.info("Creating API with auto-retry and connection pool") + logger.debug(f"Connection details: Host={SYNOLOGY_HOST}, Port={SYNOLOGY_PORT}, Secure={SYNOLOGY_SECURE}") + + self.protocol = "https" if SYNOLOGY_SECURE else "http" + self.base_url = f"{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi" + self.sid = None + self.session = requests.Session() + + # Настройка SSL + if self.protocol == "https": + logger.debug("SSL enabled, disabling certificate verification for internal network") + self.session.verify = False # Отключаем проверку SSL для внутренней сети + + # Добавляем пользовательские заголовки для улучшения совместимости с API + custom_headers = { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36', + 'Accept': 'application/json, text/javascript, */*; q=0.01', + 'Accept-Language': 'ru-RU,ru;q=0.9,en-US;q=0.8,en;q=0.7', + 'X-Requested-With': 'XMLHttpRequest', + 'Connection': 'keep-alive', + 'Referer': f'{self.protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/' + } + self.session.headers.update(custom_headers) + logger.debug("Added browser-like headers for API compatibility") + + # Добавляем повторные попытки для HTTP-запросов + retry_strategy = Retry( + total=5, # Увеличиваем количество попыток + status_forcelist=[429, 500, 502, 503, 504, 404], + allowed_methods=["GET", "POST"], + backoff_factor=1.5, # Увеличиваем задержку между попытками + respect_retry_after_header=True + ) + adapter = HTTPAdapter( + max_retries=retry_strategy, + pool_connections=3, + pool_maxsize=10 + ) + self.session.mount("http://", adapter) + self.session.mount("https://", adapter) + + # Таймауты будут указаны в запросах + self.default_timeout = (SYNOLOGY_TIMEOUT, SYNOLOGY_TIMEOUT*2) + logger.debug(f"Setting default request timeout: {self.default_timeout}") + + # Кэш для хранения результатов запросов + self._cache = {} + self._cache_ttl = {} + self._last_online_check = 0 + self._last_online_status = False + self._online_check_interval = 30 # Интервал для кэширования проверок онлайн-статуса + + # Время последней успешной аутентификации и срок действия сессии + self._last_auth_time = 0 + self._auth_expiry = 3600 # По умолчанию 1 час + + # Информация о доступных API + self._available_apis = {} + self._api_info_ttl = 0 + + # Инициализируем API version resolver для автоматического определения совместимых API + self.api_resolver = None # Будет создан при необходимости + + def login(self) -> bool: + """Авторизация в API Synology NAS""" + # Сбрасываем SID для новой сессии + self.sid = None + + logger.info("Attempting to authenticate with Synology NAS...") + logger.debug(f"Base URL: {self.base_url}") + + # Проверяем доступность NAS перед запросом с помощью прямой TCP-проверки + # Избегаем вызова is_online(), чтобы не создавать рекурсию + online_status = self._check_tcp_connection() + if not online_status: + logger.error("Cannot login: Synology NAS is not reachable") + return False + + # Пробуем различные версии API для аутентификации + # Начинаем с версии 3, которая показала лучшую совместимость в тестах + auth_versions_to_try = [3, 2, 1, 6] # Пробуем сначала более стабильные версии + + for auth_version in auth_versions_to_try: + try: + # Определяем путь к API аутентификации + auth_path = "entry.cgi" # Используем entry.cgi вместо auth.cgi по умолчанию + + # Проверка информации API для определения доступных версий API + api_info_url = f"{self.base_url}/entry.cgi" + api_info_params = { + "api": "SYNO.API.Info", + "version": "1", + "method": "query", + "query": "SYNO.API.Auth" + } + + logger.debug(f"Querying API info for auth version {auth_version}") + try: + api_info_response = self.session.get( + api_info_url, + params=api_info_params, + timeout=self.default_timeout, + verify=False + ) + + if api_info_response.status_code == 200: + api_info_data = api_info_response.json() + if api_info_data.get("success"): + auth_info = api_info_data.get("data", {}).get("SYNO.API.Auth", {}) + max_version = auth_info.get("maxVersion", 6) + min_version = auth_info.get("minVersion", 1) + auth_path = auth_info.get("path", "entry.cgi") + logger.debug(f"Auth API versions: min={min_version}, max={max_version}, path={auth_path}") + + # Проверяем поддержку текущей версии + if auth_version < min_version or auth_version > max_version: + logger.warning(f"Auth API version {auth_version} not supported (min={min_version}, max={max_version}), trying next version") + continue + else: + logger.warning("Failed to query API info, using default auth path") + else: + logger.warning(f"API info request failed with status {api_info_response.status_code}, using default auth path") + except Exception as e: + logger.warning(f"Error querying API info: {str(e)}, using default auth path") + + # Основной запрос авторизации + url = f"{self.base_url}/{auth_path}" + params = { + "api": "SYNO.API.Auth", + "version": str(auth_version), + "method": "login", + "account": SYNOLOGY_USERNAME, + "passwd": SYNOLOGY_PASSWORD, + "session": "SynologyPowerControlBot", + "format": "cookie" + } + + # Для версии 6+ используем немного другой формат + if auth_version >= 6: + params["enable_syno_token"] = "yes" + + logger.debug(f"Sending auth request to {url} with API version {auth_version}") + start_time = time.time() + response = self.session.get( + url, + params=params, + timeout=self.default_timeout, + verify=False + ) + elapsed_time = time.time() - start_time + logger.debug(f"Auth request completed in {elapsed_time:.2f}s with status code {response.status_code}") + + # Проверка статуса HTTP + if response.status_code != 200: + logger.error(f"HTTP error: {response.status_code}") + logger.debug(f"Response content: {response.text[:200]}") + continue # Пробуем следующую версию + + try: + data = response.json() + except json.JSONDecodeError: + logger.error("Failed to decode JSON response") + logger.debug(f"Response content: {response.text[:200]}") + continue # Пробуем следующую версию + + if data.get("success"): + self.sid = data.get("data", {}).get("sid") + self._last_auth_time = time.time() + logger.info(f"Successfully logged in to Synology NAS using API version {auth_version}") + logger.debug(f"Session ID obtained: {self.sid[:5] if self.sid else 'None'}...") + + # Получаем и сохраняем токен SYNO, если он есть + syno_token = data.get("data", {}).get("synotoken") + if syno_token: + self.session.headers.update({'X-SYNO-TOKEN': syno_token}) + logger.debug("Added X-SYNO-TOKEN header for improved API compatibility") + + # Также добавляем SID в cookies для улучшения совместимости + self.session.cookies.update({ + 'id': self.sid, + 'sid': self.sid + }) + logger.debug("Added SID to session cookies for improved compatibility") + + # Проверка валидности полученной сессии с помощью простого запроса + # Будем использовать SYNO.API.Info без проверки сложных методов + + # Даем системе немного времени для инициализации сессии + time.sleep(0.5) + + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to log in with version {auth_version}: Error code {error_code}") + + # Если ошибка связана с версией API, пробуем следующую версию + if error_code in [104, 105]: + logger.warning(f"Auth version {auth_version} not supported, trying next version") + continue + + # Дополнительная диагностика + if error_code == 400: + logger.error("Authentication error: Invalid credentials") + elif error_code == 401: + logger.error("Authentication error: Account disabled") + elif error_code == 402: + logger.error("Authentication error: Permission denied") + elif error_code == 403: + logger.error("Authentication error: 2-factor authentication required") + elif error_code == 404: + logger.error("Authentication error: Failed to authenticate with 2-factor authentication") + + # Если ошибка связана с неверными учетными данными, нет смысла пробовать другие версии API + if error_code in [400, 401, 402, 403, 404]: + return False + + except requests.exceptions.Timeout: + logger.error(f"Connection timeout during auth with version {auth_version}") + continue # Пробуем следующую версию + except requests.exceptions.ConnectionError as e: + logger.error(f"Connection error during auth with version {auth_version}: {str(e)}") + continue # Пробуем следующую версию + except requests.RequestException as e: + logger.error(f"Request error during auth with version {auth_version}: {str(e)}") + continue # Пробуем следующую версию + except Exception as e: + logger.error(f"Unexpected error during login with version {auth_version}: {str(e)}", exc_info=True) + continue # Пробуем следующую версию + + # Если все версии не сработали + logger.error("Failed to authenticate with any API version") + return False + + def _validate_session(self) -> bool: + """Проверяет валидность сессии после авторизации""" + if not self.sid: + return False + + # Попробуем сделать простой запрос для проверки сессии + test_apis = [ + {"api": "SYNO.Core.System", "method": "info", "version": 1}, + {"api": "SYNO.DSM.Info", "method": "getinfo", "version": 1} + ] + + for test_api in test_apis: + try: + url = f"{self.base_url}/entry.cgi" + params = { + "api": test_api["api"], + "version": str(test_api["version"]), + "method": test_api["method"], + "_sid": self.sid + } + + response = self.session.get(url, params=params, timeout=self.default_timeout, verify=False) + + if response.status_code == 200: + data = response.json() + if data.get("success"): + logger.debug(f"Session validation successful using {test_api['api']}") + return True + else: + error_code = data.get("error", {}).get("code", -1) + if error_code != 119: # Не сессия истекла + logger.debug(f"Session validation with {test_api['api']} returned error code {error_code}") + return True # Считаем сессию валидной, если ошибка не связана с истечением сессии + except Exception as e: + logger.warning(f"Error during session validation with {test_api['api']}: {str(e)}") + + logger.warning("Session validation failed with all test APIs") + return False + + def logout(self) -> bool: + """Выход из API Synology NAS""" + if not self.sid: + return True + + try: + url = f"{self.base_url}/auth.cgi" + params = { + "api": "SYNO.API.Auth", + "version": "1", + "method": "logout", + "session": "SynologyPowerControlBot", + "_sid": self.sid + } + + response = self.session.get(url, params=params, timeout=SYNOLOGY_TIMEOUT, verify=False) + data = response.json() + + if data.get("success"): + self.sid = None + logger.info("Successfully logged out from Synology NAS") + return True + else: + error_code = data.get("error", {}).get("code", -1) + logger.error(f"Failed to log out from Synology NAS: Error code {error_code}") + return False + + except requests.RequestException as e: + logger.error(f"Connection error: {str(e)}") + return False + + def _make_api_request(self, api_name: str, method: str, version: int = 1, + params: Optional[Dict[str, Any]] = None, retry_auth: bool = True, retry_count: int = 0) -> Optional[Dict[str, Any]]: + """Обобщенный метод для выполнения API запросов с обработкой ошибок""" + # Ограничение на количество повторных попыток + if retry_count >= 3: + logger.error(f"Too many retries for {api_name}.{method}, giving up") + return None + + # Проверка наличия авторизации + if not self.sid and not self.login(): + logger.error(f"Not authenticated for API request: {api_name}.{method}") + return None + + # Проверка информации API для определения пути и поддерживаемой версии + api_info_url = f"{self.base_url}/entry.cgi" + api_info_params = { + "api": "SYNO.API.Info", + "version": "1", + "method": "query", + "query": api_name + } + + api_path = "entry.cgi" + try: + logger.debug(f"Querying API info for {api_name}") + api_info_response = self.session.get( + api_info_url, + params=api_info_params, + timeout=self.default_timeout, + verify=False + ) + + if api_info_response.status_code == 200: + api_info_data = api_info_response.json() + if api_info_data.get("success"): + api_info = api_info_data.get("data", {}).get(api_name, {}) + if api_info: + max_version = api_info.get("maxVersion", version) + min_version = api_info.get("minVersion", version) + api_path = api_info.get("path", "entry.cgi") + + # Проверка, поддерживается ли запрошенная версия + if version < min_version: + logger.warning(f"API version {version} for {api_name} is below minimum {min_version}, using {min_version}") + version = min_version + elif version > max_version: + logger.warning(f"API version {version} for {api_name} exceeds maximum {max_version}, using {max_version}") + version = max_version + + logger.debug(f"Using API path: {api_path}, version: {version}") + else: + logger.warning(f"API {api_name} not found in API info, using defaults") + except Exception as e: + logger.warning(f"Error querying API info for {api_name}: {str(e)}, using defaults") + + # Подготовка базовых параметров запроса + base_params = { + "api": api_name, + "version": str(version), + "method": method, + "_sid": self.sid # Используем _sid вместо sid для лучшей совместимости + } + + # Добавление дополнительных параметров, если они заданы + if params: + base_params.update(params) + + url = f"{self.base_url}/{api_path}" + logger.debug(f"API request: {api_name}.{method} v{version} to {url}") + logger.debug(f"Full request params: {base_params}") + + try: + start_time = time.time() + response = self.session.get( + url, + params=base_params, + timeout=self.default_timeout, + verify=False + ) + elapsed_time = time.time() - start_time + logger.debug(f"API request completed in {elapsed_time:.2f}s with status {response.status_code}") + + # Проверка статуса HTTP + if response.status_code != 200: + logger.error(f"HTTP error: {response.status_code} for {api_name}.{method}") + + # Повторная попытка при ошибках соединения + if response.status_code in [500, 502, 503, 504]: + logger.info(f"Server error, retrying request for {api_name}.{method}") + sleep(2) + return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1) + + return None + + try: + data = response.json() + except json.JSONDecodeError: + logger.error(f"Failed to decode JSON response for {api_name}.{method}") + logger.debug(f"Response content: {response.text[:200]}") + + # Повторная попытка при ошибках декодирования + logger.info(f"JSON decode error, retrying request for {api_name}.{method}") + sleep(2) + return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1) + + if data.get("success"): + logger.info(f"API request successful for {api_name}.{method}") + return data.get("data", {}) + else: + error_code = data.get("error", {}).get("code", -1) + error_desc = self._get_error_description(error_code) + logger.error(f"API error for {api_name}.{method}: {error_code} - {error_desc}") + + # Ошибки доступа или прав часто встречаются, но они не критичные + # Например, ошибка 102 означает, что нет прав, но NAS доступен + if error_code in [102, 103, 104, 105]: + logger.warning(f"Permission-related error for {api_name}.{method}: {error_code}") + # Возвращаем пустой словарь вместо None, + # чтобы вызывающий код мог понять, что запрос выполнен + return {} + + # Если ошибка связана с авторизацией и нам разрешено повторить попытку + if error_code in [106, 107, 119] and retry_auth: + logger.info(f"Session error (code {error_code}), creating fresh session...") + self.sid = None # Сбрасываем SID + + # Для ошибки 119 (Session timeout) дадим системе немного времени + if error_code == 119: + logger.info("Session timeout detected, waiting before retry...") + sleep(3) + + if self.login(): + logger.info("Re-authenticated with fresh session, retrying API request...") + # Рекурсивный вызов, но со счетчиком повторов + return self._make_api_request(api_name, method, version, params, False, retry_count + 1) + + # Для некоторых ошибок можно автоматически повторить запрос + if error_code in [408, 429, 500, 502, 503, 504]: + logger.info(f"Temporary error {error_code}, retrying request for {api_name}.{method}") + sleep(2) + return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1) + + return None + + except requests.exceptions.Timeout: + logger.error(f"Request timeout for {api_name}.{method}") + + # Повторная попытка при таймауте + if retry_count < 2: + logger.info(f"Timeout, retrying request for {api_name}.{method}") + sleep(2) + return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1) + + return None + except requests.exceptions.ConnectionError as e: + logger.error(f"Connection error for {api_name}.{method}: {str(e)}") + + # Повторная попытка при ошибке соединения + if retry_count < 2: + logger.info(f"Connection error, retrying request for {api_name}.{method}") + sleep(2) + return self._make_api_request(api_name, method, version, params, retry_auth, retry_count + 1) + + return None + except Exception as e: + logger.error(f"Unexpected error during {api_name}.{method}: {str(e)}") + return None + + def get_system_status(self) -> Dict[str, Any]: + """Получение статуса системы""" + # Проверяем доступность системы + if not self.is_online(): + logger.info("Device is offline, skipping API request") + return {"status": "offline"} + + # Проверяем, есть ли кэшированный результат + cache_key = "system_status" + current_time = time.time() + if cache_key in self._cache and current_time - self._cache_ttl.get(cache_key, 0) < 60: + logger.debug("Using cached system status") + return self._cache[cache_key] + + # Используем рекомендованный API для получения информации о системе + logger.info(f"Getting system info using {SYNOLOGY_INFO_API} v{SYNOLOGY_API_VERSION}") + + # Для SYNO.DSM.Info метод getinfo, для других обычно используется get или info + if SYNOLOGY_INFO_API == "SYNO.DSM.Info": + method = "getinfo" + else: + method = "get" + + result = self._make_api_request(SYNOLOGY_INFO_API, method, version=SYNOLOGY_API_VERSION) + + if result: + logger.info(f"Successfully retrieved system info using {SYNOLOGY_INFO_API}") + + # Формируем расширенный ответ с дополнительной информацией + system_info = { + "status": "online", + "hostname": result.get("hostname", "unknown"), + "model": result.get("model", "unknown"), + "version": result.get("version", "unknown"), + "uptime": result.get("uptime", 0), + "time": current_time, + "is_online": True, + "api_used": SYNOLOGY_INFO_API + } + + # Сохраняем в кэше + self._cache[cache_key] = system_info + self._cache_ttl[cache_key] = current_time + + return system_info + + # Если основной API не сработал, пробуем резервные варианты + logger.warning(f"Failed to retrieve system info with {SYNOLOGY_INFO_API}, trying fallback APIs") + + # Пробуем резервные API + apis_to_try = [ + {"name": "SYNO.DSM.Info", "method": "getinfo", "version": 2}, + {"name": "SYNO.Core.System", "method": "info", "version": 1}, + {"name": "SYNO.Core.System.Status", "method": "get", "version": 1}, + {"name": "SYNO.Core.System.Info", "method": "get", "version": 1}, + ] + + for api in apis_to_try: + if api["name"] == SYNOLOGY_INFO_API: + continue # Пропускаем уже проверенный API + + logger.info(f"Trying to get system info using {api['name']}.{api['method']} v{api['version']}") + result = self._make_api_request(api["name"], api["method"], version=api["version"]) + + if result: + logger.info(f"Successfully retrieved system info using {api['name']}") + + # Формируем расширенный ответ с дополнительной информацией + system_info = { + "status": "online", + "hostname": result.get("hostname", "unknown"), + "model": result.get("model", "unknown"), + "version": result.get("version", "unknown"), + "uptime": result.get("uptime", 0), + "time": current_time, + "is_online": True, + "api_used": api["name"] + } + + # Сохраняем в кэше + self._cache[cache_key] = system_info + self._cache_ttl[cache_key] = current_time + + return system_info + + # Если все запросы не удались, но система онлайн, возвращаем базовую информацию + logger.warning("Failed to retrieve system info with all API methods") + return { + "status": "error", + "error": "Failed to fetch system information", + "is_online": True, + "time": current_time + } + + def shutdown_system(self) -> bool: + """Выключение системы""" + # Проверяем, включено ли устройство перед попыткой его выключить + if not self.is_online(force_check=True): + logger.info("Synology NAS is already offline, no need to shut down") + return True + + logger.info("Attempting to shutdown Synology NAS...") + + # Попробуем сначала использовать предпочтительный API для управления питанием + logger.info(f"Trying shutdown with {SYNOLOGY_POWER_API} v{SYNOLOGY_API_VERSION}") + + # Для SYNO.Core.Hardware.PowerRecovery используется метод setPowerOnState + # Для других API обычно используется метод shutdown или reboot + if SYNOLOGY_POWER_API == "SYNO.Core.Hardware.PowerRecovery": + # Для этого API нужны специальные параметры + params = {"state": "powerbtn"} # powerbtn имитирует нажатие кнопки питания + result = self._make_api_request(SYNOLOGY_POWER_API, "setPowerOnState", version=SYNOLOGY_API_VERSION, params=params) + else: + # Пробуем стандартный метод + result = self._make_api_request(SYNOLOGY_POWER_API, "shutdown", version=SYNOLOGY_API_VERSION) + + if result is not None: + logger.info(f"Successfully initiated system shutdown using {SYNOLOGY_POWER_API}") + + # Даем системе время начать процесс выключения + logger.info("Waiting for shutdown to initialize...") + sleep(5) + + # Проверяем статус + if not self.is_online(force_check=True): + logger.info("System is now offline. Shutdown confirmed successful.") + return True + else: + logger.info("System still appears to be online, but shutdown may be in progress.") + return True + else: + logger.warning(f"Failed to shutdown using {SYNOLOGY_POWER_API}, trying alternative methods") + + # Если не сработал основной метод, пробуем резервные варианты + # Проверка всех доступных методов API для выключения + apis_to_try = [ + {"name": "SYNO.Core.System", "method": "shutdown", "version": 3}, + {"name": "SYNO.Core.System.Power", "method": "shutdown", "version": 1}, + {"name": "SYNO.DSM.Power", "method": "shutdown", "version": 1}, + {"name": "SYNO.System.Power", "method": "shutdown", "version": 1}, + {"name": "SYNO.DSM.System", "method": "shutdown", "version": 1} + ] + + # Проверяем доступные API + try: + api_info_url = f"{self.base_url}/entry.cgi" + api_info_params = { + "api": "SYNO.API.Info", + "version": "1", + "method": "query", + "query": "SYNO.Core.System.Power,SYNO.DSM.Power,SYNO.Core.System,SYNO.System.Power,SYNO.DSM.System" + } + + logger.debug("Checking available shutdown APIs") + api_info_response = self.session.get( + api_info_url, + params=api_info_params, + timeout=self.default_timeout, + verify=False + ) + + if api_info_response.status_code == 200: + api_info_data = api_info_response.json() + if api_info_data.get("success"): + available_apis = api_info_data.get("data", {}) + logger.debug(f"Available APIs: {list(available_apis.keys())}") + + # Фильтруем только доступные API + filtered_apis = [] + for api in apis_to_try: + if api["name"] in available_apis: + api_info = available_apis[api["name"]] + # Проверка версии API + if api_info.get("minVersion", 1) <= api["version"] <= api_info.get("maxVersion", 1): + filtered_apis.append(api) + logger.debug(f"Adding {api['name']} to available shutdown APIs") + else: + logger.debug(f"API {api['name']} version {api['version']} not supported. Supported: {api_info.get('minVersion')}-{api_info.get('maxVersion')}") + + if filtered_apis: + apis_to_try = filtered_apis + else: + logger.warning("No compatible APIs found, trying all methods as fallback") + else: + logger.warning("Failed to query API info, using default methods") + else: + logger.warning(f"API info request failed with status {api_info_response.status_code}, using default methods") + except Exception as e: + logger.warning(f"Error querying API info: {str(e)}, using default methods") + + # Пробуем все доступные методы по порядку + for api in apis_to_try: + logger.info(f"Trying shutdown with {api['name']}.{api['method']} v{api['version']}") + result = self._make_api_request(api["name"], api["method"], version=api["version"]) + + if result is not None: + logger.info(f"Successfully initiated system shutdown using {api['name']}") + + # Даем системе время начать процесс выключения + logger.info("Waiting for shutdown to initialize...") + sleep(5) + + # Проверяем статус + if not self.is_online(force_check=True): + logger.info("System is now offline. Shutdown confirmed successful.") + return True + else: + logger.info("System still appears to be online, but shutdown may be in progress.") + return True + else: + logger.warning(f"Failed to shutdown using {api['name']}.{api['method']} v{api['version']}") + + # Если ни один метод не сработал, но система стала недоступна + if not self.is_online(force_check=True): + logger.info("System appears to be shutting down despite API errors") + return True + + logger.error("Failed to shutdown system after trying multiple APIs") + return False + + def reboot_system(self) -> bool: + """Перезагрузка системы""" + # Проверяем, включена ли система + if not self.is_online(force_check=True): + logger.error("Cannot reboot: System is offline") + return False + + logger.info("Attempting to reboot Synology NAS...") + + # Список API и методов для попытки перезагрузки + apis_to_try = [ + {"api": "SYNO.Core.System", "method": "reboot", "version": 1}, + {"api": "SYNO.DSM.System", "method": "reboot", "version": 1}, + {"api": "SYNO.Core.System.Power", "method": "restart", "version": 1}, + {"api": "SYNO.DSM.Power", "method": "reboot", "version": 1}, + {"api": "SYNO.Core.System", "method": "reboot", "version": 3}, + {"api": "SYNO.System.Power", "method": "reboot", "version": 1}, + {"api": "SYNO.Core.Hardware.NeedReboot", "method": "reboot", "version": 1} + ] + + # Добавляем настроенный в конфигурации API, если он отличается от уже добавленных + already_added = [item["api"] for item in apis_to_try] + if SYNOLOGY_POWER_API not in already_added: + for method in ["restart", "reboot"]: + apis_to_try.append({ + "api": SYNOLOGY_POWER_API, + "method": method, + "version": SYNOLOGY_API_VERSION + }) + + # Перебираем все возможные API и методы + for api_info in apis_to_try: + try: + logger.info(f"Trying reboot with {api_info['api']} API using method {api_info['method']}") + result = self._make_api_request(api_info['api'], api_info['method'], version=api_info['version']) + if result is not None: + logger.info(f"Successfully initiated system reboot using {api_info['api']} API") + + # Даем системе время начать процесс перезагрузки + logger.info("Waiting for reboot to initialize...") + sleep(5) + + # Ждем, пока система станет недоступна (признак перезагрузки) + reboot_started = False + for i in range(12): # Проверяем в течение 60 секунд (12 * 5сек) + if not self.is_online(force_check=True): + logger.info(f"System went offline after {i*5} seconds, reboot in progress") + reboot_started = True + break + logger.debug(f"System still online, waiting... ({i+1}/12)") + sleep(5) + + if reboot_started: + # Успешный вызов API и система ушла оффлайн - это признак успешной перезагрузки + return True + else: + # Успешный вызов API, но система не ушла оффлайн + logger.warning("System did not go offline after reboot command, but command was accepted") + # Даже если система не ушла оффлайн, команда могла быть принята + return True + except Exception as e: + logger.error(f"Error during reboot with {api_info['api']}: {str(e)}") + + # Если все попытки не удались, возвращаем False + logger.error("All reboot attempts failed") + return False + + def _get_error_description(self, error_code: int) -> str: + """Получение описания ошибки по коду""" + error_descriptions = { + 100: "Unknown error", + 101: "Invalid parameter", + 102: "API does not exist", + 103: "Method does not exist", + 104: "Version does not support", + 105: "Permission denied", + 106: "Session timeout", + 107: "Session interrupted by duplicate login", + 400: "Invalid credentials", + 401: "Account disabled", + 402: "Permission denied", + 403: "2FA required", + 404: "Failed to authenticate with 2FA" + } + return error_descriptions.get(error_code, "Unknown error code") + + def _check_tcp_connection(self) -> bool: + """Проверка базового TCP-соединения с Synology NAS""" + try: + socket_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + socket_obj.settimeout(SYNOLOGY_TIMEOUT) + result = socket_obj.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT)) + socket_obj.close() + + return result == 0 + except socket.error as e: + logger.error(f"Socket error during connection check: {str(e)}") + return False + except Exception as e: + logger.error(f"Unexpected error during connection check: {str(e)}") + return False + + def is_online(self, force_check=False) -> bool: + """Проверка онлайн-статуса Synology NAS""" + # Используем кэшированное значение, если доступно и не устарело + current_time = time.time() + if not force_check and (current_time - self._last_online_check) < self._online_check_interval: + logger.debug(f"Using cached online status: {self._last_online_status}") + return self._last_online_status + + logger.info("Checking if NAS is online...") + + # Проверяем TCP-соединение + online_status = self._check_tcp_connection() + logger.info(f"Detected Synology NAS online status: {online_status}") + + # Если TCP-соединение успешно и у нас есть действующий SID, + # попробуем более детальную проверку через API + if online_status and self.sid: + logger.info("Trying to fetch more detailed online status through API...") + + # Пробуем разные API для проверки онлайн-статуса + api_checks = [ + {"api": "SYNO.DSM.Info", "version": "2", "method": "getinfo"}, + {"api": "SYNO.Core.System", "version": "1", "method": "info"}, + {"api": "SYNO.Core.System.Status", "version": "1", "method": "get"} + ] + + api_success = False + for api_check in api_checks: + try: + url = f"{self.base_url}/entry.cgi" + params = { + "api": api_check["api"], + "version": api_check["version"], + "method": api_check["method"], + "sid": self.sid + } + + logger.debug(f"Trying online status check with {api_check['api']}") + response = self.session.get( + url, + params=params, + timeout=self.default_timeout, + verify=False + ) + + if response.status_code == 200: + data = response.json() + if data.get("success"): + logger.info(f"API request successful for {api_check['api']}") + logger.info("Synology NAS is online with API access") + api_success = True + break + else: + error_code = data.get("error", {}).get("code", -1) + logger.warning(f"API response indicates an error: {error_code}, but NAS is reachable") + else: + logger.warning(f"API returned status code {response.status_code}") + except Exception as e: + logger.warning(f"API check failed for {api_check['api']}, but TCP connection succeeded: {str(e)}") + + if not api_success: + logger.warning("All API checks failed, but TCP connection is successful") + + # Обновляем кэшированное значение + self._last_online_check = current_time + self._last_online_status = online_status + + return online_status + + def wake_on_lan(self) -> bool: + """Отправка Wake-on-LAN пакета для включения Synology NAS""" + if not SYNOLOGY_MAC: + logger.error("MAC address not configured") + return False + + try: + # Преобразование MAC-адреса в байты + mac_address = SYNOLOGY_MAC.replace(':', '').replace('-', '') + if len(mac_address) != 12: + logger.error(f"Invalid MAC address format: {SYNOLOGY_MAC}") + return False + + try: + mac_bytes = bytes.fromhex(mac_address) + except ValueError as e: + logger.error(f"Failed to parse MAC address: {str(e)}") + logger.error(f"MAC address must contain only hex characters: {SYNOLOGY_MAC}") + return False + + # Создание Magic Packet + magic_packet = b'\xff' * 6 + mac_bytes * 16 + + # Отправка пакета на конкретный адрес + try: + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) + sock.sendto(magic_packet, (SYNOLOGY_HOST, WOL_PORT)) + sock.close() + logger.info(f"Wake-on-LAN packet sent to {SYNOLOGY_MAC} at {SYNOLOGY_HOST}:{WOL_PORT}") + except Exception as e: + logger.error(f"Error sending directed WoL packet: {str(e)}") + return False + + # Для надежности отправляем также широковещательный пакет + try: + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) + + # Используем стандартный широковещательный адрес + broadcast_addr = "255.255.255.255" + sock.sendto(magic_packet, (broadcast_addr, WOL_PORT)) + sock.close() + logger.info(f"Wake-on-LAN broadcast packet sent to {broadcast_addr}:{WOL_PORT}") + except Exception as e: + logger.warning(f"Error sending broadcast WoL packet (non-critical): {str(e)}") + # Не считаем ошибкой, т.к. основной пакет уже отправлен + + return True + + except Exception as e: + logger.error(f"Unexpected error in wake_on_lan: {str(e)}") + return False + + def wait_for_boot(self, max_attempts: int = 30, delay: int = 10) -> bool: + """Ожидание загрузки Synology NAS""" + logger.info(f"Waiting for Synology NAS to boot (timeout: {max_attempts * delay}s)...") + + for attempt in range(max_attempts): + # Принудительно проверяем статус без использования кэша + if self.is_online(force_check=True): + logger.info(f"Synology NAS is online after {attempt + 1} attempts ({(attempt + 1) * delay}s)") + + # Проверяем, что не только сеть доступна, но и API загрузился + api_ready = False + logger.info("Waiting for API services to initialize...") + + for api_check in range(5): # Даем еще до 50 секунд для загрузки API + if self.sid or self.login(): + api_ready = True + logger.info(f"API services are ready after {api_check + 1} attempts") + break + logger.debug(f"API not ready yet, waiting... ({api_check + 1}/5)") + sleep(10) + + if not api_ready: + logger.warning("System is online but API services may not be fully initialized") + + # Дадим дополнительное время для полной загрузки всех сервисов + sleep(delay) + return True + + sleep(delay) + logger.info(f"Waiting for boot... Attempt {attempt + 1}/{max_attempts}") + + logger.error(f"Synology NAS didn't come online after {max_attempts * delay} seconds") + return False + + def power_on(self) -> bool: + """Включение Synology NAS""" + # Принудительная проверка статуса + if self.is_online(force_check=True): + logger.info("Synology NAS is already online") + return True + + logger.info("Powering on Synology NAS via Wake-on-LAN...") + + # Проверяем, настроен ли MAC-адрес + if not SYNOLOGY_MAC: + logger.error("Cannot power on: MAC address not configured in settings") + return False + + # Пробуем отправить несколько WoL пакетов для надежности + success = False + for attempt in range(3): + logger.debug(f"Sending WoL packet, attempt {attempt + 1}/3") + if self.wake_on_lan(): + success = True + break + sleep(1) + + if not success: + logger.error("Failed to send Wake-on-LAN packets") + return False + + # Ожидание загрузки + logger.info("WoL packets sent successfully, waiting for system to boot...") + boot_result = self.wait_for_boot(max_attempts=30, delay=10) + + if boot_result: + # Проверяем доступность API после загрузки + system_status = self.get_system_status() + if system_status.get("status") == "online": + logger.info("System booted successfully with API access") + return True + else: + logger.warning("System appears to be online but API may not be fully ready") + return True + else: + logger.error("System did not come online after WoL") + return False + + def power_off(self) -> bool: + """Выключение Synology NAS""" + if not self.is_online(force_check=True): + logger.info("Synology NAS is already offline") + return True + + logger.info("Powering off Synology NAS...") + + # Список API и методов для попытки выключения + apis_to_try = [ + {"api": "SYNO.Core.System", "method": "shutdown", "version": 1}, + {"api": "SYNO.DSM.System", "method": "shutdown", "version": 1}, + {"api": SYNOLOGY_POWER_API, "method": "shutdown", "version": SYNOLOGY_API_VERSION} + ] + + # Перебираем все возможные API и методы + for api_info in apis_to_try: + try: + logger.info(f"Trying shutdown with {api_info['api']} API using method {api_info['method']}") + api_result = self._make_api_request(api_info['api'], api_info['method'], version=api_info['version']) + if api_result is not None: + logger.info(f"Successfully initiated system shutdown using {api_info['api']} API") + + # Даем системе время начать процесс выключения + logger.info("Waiting for shutdown to initialize...") + sleep(5) + + return True + except Exception as e: + logger.error(f"Error during shutdown with {api_info['api']}: {str(e)}") + + # Если все попытки не удались, возвращаем False + logger.error("All shutdown attempts failed") + return False + + # Если все еще не сработало, используем оригинальный метод shutdown_system + if not result: + result = self.shutdown_system() + + if result: + # Дополнительная проверка, что система действительно выключилась + logger.info("Verifying system is offline...") + for attempt in range(12): # Проверяем в течение 2 минут (12 * 10 сек) + if not self.is_online(force_check=True): + logger.info(f"System confirmed offline after {attempt * 10} seconds") + return True + logger.debug(f"System still shutting down, checking again... ({attempt + 1}/12)") + sleep(10) + + logger.warning("System still appears to be online after shutdown command") + return False + else: + logger.error("Failed to initiate shutdown") + return False + + # Заглушки для расширенных методов + def get_shared_folders(self) -> List[Dict[str, Any]]: + """Получение списка общих папок""" + logger.info("Getting list of shared folders") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for shared folders request") + return [] + + try: + # Запрашиваем список общих папок через FileStation API + result = self._make_api_request("SYNO.FileStation.List", "list_share", version=2) + + if not result: + # Пробуем альтернативный API + logger.info("Trying alternative API for shared folders") + alt_result = self._make_api_request("SYNO.FileStation.List", "list_share", version=1) + if alt_result: + return alt_result.get("shares", []) + return [] + + return result.get("shares", []) + + except Exception as e: + logger.error(f"Error getting shared folders: {str(e)}") + return [] + + def get_system_load(self) -> Dict[str, Any]: + """Получение информации о загрузке системы""" + logger.info("Getting system load information") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for system load request") + return {} + + try: + # Запрашиваем информацию о загрузке системы + result = self._make_api_request("SYNO.Core.System.Utilization", "get", version=1) + + if not result: + # Пробуем альтернативный API + logger.info("Trying alternative API for system load") + alt_result = self._make_api_request("SYNO.Core.System", "info", version=3) + + if not alt_result: + return {} + + # Формируем из частичных данных + return { + "cpu_load": alt_result.get("cpu_usage", 0), + "memory": { + "total": alt_result.get("memory_size", 0), + "used": alt_result.get("memory_usage", 0), + "usage_percent": alt_result.get("memory_usage_percent", 0) + } + } + + # Формируем структурированный результат + return { + "cpu_load": result.get("cpu", {}).get("user", 0) + result.get("cpu", {}).get("system", 0), + "memory": result.get("memory", {}), + "network": result.get("network", {}) + } + + except Exception as e: + logger.error(f"Error getting system load: {str(e)}") + return {} + + def is_online_api(self) -> bool: + """Проверка онлайн-статуса Synology NAS с использованием API""" + if not self.is_online(): + return False + + # Проверяем доступность API через авторизацию + if not self.sid and not self.login(): + return False + + return True + + def get_storage_status(self) -> Dict[str, Any]: + """Получение подробной информации о хранилище""" + logger.info("Getting storage status information") + + # Проверяем доступность NAS и API + if not self.is_online_api(): + logger.error("Cannot get storage status: NAS is not online or API is not accessible") + return {"error": "authentication_failed"} + + try: + # Пробуем получить информацию о хранилище через SYNO.Storage.CGI.Storage API + result = self._make_api_request("SYNO.Storage.CGI.Storage", "load_info", version=1) + + if not result: + # Пробуем альтернативный API + logger.info("Trying alternative API for storage info") + alt_result = self._make_api_request("SYNO.Core.Storage", "info", version=1) + + if not alt_result: + # Пробуем еще один альтернативный API + logger.info("Trying SYNO.Core.System API for storage info") + sys_result = self._make_api_request("SYNO.Core.System", "info", version=3) + + if not sys_result: + return { + "volumes": [], + "disks": [], + "total_size": 0, + "total_used": 0, + "error": "no_data" + } + + # Извлекаем базовую информацию о хранилище из системной информации + return { + "volumes": [], + "disks": [], + "total_size": sys_result.get("hdd_total_size", 0) * 1024**2, # МБ в байты + "total_used": sys_result.get("hdd_total_size", 0) * sys_result.get("hdd_usage", 0) / 100 * 1024**2, + } + + # Обрабатываем данные из альтернативного API + volumes = alt_result.get("volumes", []) + disks = alt_result.get("disks", []) + + else: + # Обрабатываем данные из основного API + volumes = result.get("volumes", []) + disks = result.get("disks", []) + + # Рассчитываем общие размеры + total_size = 0 + total_used = 0 + + for volume in volumes: + volume_size = volume.get("size", {}).get("total", 0) + volume_used = volume.get("size", {}).get("used", 0) + + total_size += volume_size + total_used += volume_used + + return { + "volumes": volumes, + "disks": disks, + "total_size": total_size, + "total_used": total_used + } + + except Exception as e: + logger.error(f"Error in get_storage_status: {str(e)}") + return {"error": str(e)} + + def get_security_status(self) -> Dict[str, Any]: + """Получение информации о состоянии безопасности""" + logger.info("Getting security status information") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for security status request") + return {"success": False, "error": "authentication_failed"} + + try: + # Пробуем получить информацию о безопасности через API Security Scan + result = self._make_api_request("SYNO.Core.SecurityScan.Status", "get", version=1) + + if not result: + # Пробуем альтернативный API + logger.info("Trying alternative API for security status") + # Проверяем статус брандмауэра + firewall_result = self._make_api_request("SYNO.Core.Security.Firewall.Status", "get", version=1) + + # Проверяем статус автоматических обновлений + update_result = self._make_api_request("SYNO.Core.Upgrade.Server.Setting", "get", version=1) + + # Если ни один из API не отвечает + if not firewall_result and not update_result: + # Получаем общую информацию о системе для базовой проверки безопасности + sys_result = self._make_api_request("SYNO.DSM.Info", "getinfo", version=2) + + if not sys_result: + return { + "success": False, + "status": "unknown", + "last_check": None, + "is_secure": False, + "error": "no_security_api" + } + + # Собираем базовые сведения из системной информации + return { + "success": True, + "status": "basic", + "last_check": None, + "is_secure": True, # Предполагаем, что система в целом безопасна + "firewall_enabled": None, + "auto_update": None, + "version_latest": sys_result.get("version_string", "") + } + + # Собираем информацию из доступных результатов + firewall_enabled = firewall_result.get("enabled", False) if firewall_result else None + auto_update = update_result.get("auto_update", False) if update_result else None + + # Определяем, насколько система безопасна + is_secure = True # По умолчанию предполагаем, что система безопасна + if firewall_enabled is not None and not firewall_enabled: + is_secure = False + + return { + "success": True, + "status": "partial", + "last_check": None, + "is_secure": is_secure, + "firewall_enabled": firewall_enabled, + "auto_update": auto_update + } + + # Если основное API отвечает, возвращаем его данные + return { + "success": True, + "status": result.get("status", "unknown"), + "last_check": result.get("last_check", None), + "is_secure": result.get("is_secure", False), + "details": result.get("details", {}) + } + + except Exception as e: + logger.error(f"Error in get_security_status: {str(e)}") + return {"success": False, "error": str(e)} + + def get_processes(self, limit: int = 20) -> List[Dict[str, Any]]: + """Получение списка активных процессов""" + logger.info(f"Getting list of active processes (limit={limit})") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for processes request") + return [] + + try: + # Получаем список процессов через API + result = self._make_api_request("SYNO.Core.System.Process", "list", version=1, + params={"sort_by": "cpu", "order": "DESC", "limit": limit}) + + if not result: + logger.warning("Failed to get process list") + return [] + + return result.get("processes", []) + + except Exception as e: + logger.error(f"Error getting process list: {str(e)}") + return [] + + def get_network_status(self) -> Dict[str, Any]: + """Получение информации о сетевых подключениях""" + logger.info("Getting network status information") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for network status request") + return {} + + try: + # Получаем информацию о сетевых интерфейсах + interface_result = self._make_api_request("SYNO.Core.Network.Interface", "list", version=1) + + # Получаем статистику использования сети + utilization_result = self._make_api_request("SYNO.Core.System.Utilization", "get", version=1) + + interfaces = [] + if interface_result: + interfaces = interface_result.get("interfaces", []) + + network_stats = {} + if utilization_result and "network" in utilization_result: + network_stats = utilization_result.get("network", {}) + + # Объединяем данные + for interface in interfaces: + iface_id = interface.get("id", "") + if iface_id in network_stats: + interface["rx"] = network_stats[iface_id].get("rx", 0) + interface["tx"] = network_stats[iface_id].get("tx", 0) + + return { + "interfaces": interfaces, + "statistics": network_stats + } + + except Exception as e: + logger.error(f"Error getting network status: {str(e)}") + return {} + + def get_system_logs(self, limit: int = 20) -> List[Dict[str, Any]]: + """Получение журналов системы""" + logger.info(f"Getting system logs (limit={limit})") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for system logs request") + return [] + + try: + # Получаем журналы через API + result = self._make_api_request("SYNO.Core.SyslogClient.Log", "list", version=1, + params={"start": 0, "limit": limit, "sort": "time", "dir": "DESC"}) + + if not result: + # Пробуем альтернативный API + logger.info("Trying alternative API for system logs") + alt_result = self._make_api_request("SYNO.Core.Log", "list", version=1, + params={"start": 0, "limit": limit, "sort": "time", "dir": "DESC"}) + + if alt_result: + return alt_result.get("logs", []) + return [] + + return result.get("logs", []) + + except Exception as e: + logger.error(f"Error getting system logs: {str(e)}") + return [] + + def get_power_schedule(self) -> Dict[str, Any]: + """Получение расписания включения/выключения""" + logger.info("Getting power schedule") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for power schedule request") + return {} + + try: + # Получаем расписание через API + result = self._make_api_request("SYNO.Core.Hardware.PowerSchedule", "get", version=1) + + if not result: + return {} + + return result + + except Exception as e: + logger.error(f"Error getting power schedule: {str(e)}") + return {} + + def set_power_schedule(self, schedule_type: str, days: List[int], time: str, enabled: bool = True) -> bool: + """Настройка расписания включения/выключения + + Args: + schedule_type: Тип расписания ('boot' или 'shutdown') + days: Список дней недели (0-6, где 0 - понедельник) + time: Время в формате 'HH:MM' + enabled: Включить или выключить расписание + + Returns: + True если успешно, False в противном случае + """ + logger.info(f"Setting power schedule for {schedule_type} at {time} on days {days}") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for setting power schedule") + return False + + try: + # Получаем текущее расписание + current_schedule = self._make_api_request("SYNO.Core.Hardware.PowerSchedule", "get", version=1) + + if not current_schedule: + logger.error("Failed to get current power schedule") + return False + + # Подготавливаем новое расписание + params = { + "enabled": enabled, + "type": schedule_type, + "day": days, + "time": time + } + + # Устанавливаем новое расписание + result = self._make_api_request("SYNO.Core.Hardware.PowerSchedule", "set", version=1, params=params) + + if not result: + logger.error("Failed to set power schedule") + return False + + logger.info(f"Power schedule for {schedule_type} set successfully") + return True + + except Exception as e: + logger.error(f"Error setting power schedule: {str(e)}") + return False + + def get_temperature_status(self) -> Dict[str, Any]: + """Получение информации о температуре системы и дисков""" + logger.info("Getting temperature status") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for temperature status request") + return {} + + try: + # Получаем информацию о системе для общей температуры + system_info = self._make_api_request("SYNO.DSM.Info", "getinfo", version=2) + + # Получаем информацию о дисках для их температуры + storage_info = self._make_api_request("SYNO.Storage.CGI.Storage", "load_info", version=1) + + system_temp = None + disk_temps = [] + + if system_info: + system_temp = system_info.get("temperature") + + if storage_info: + disks = storage_info.get("disks", []) + for disk in disks: + name = disk.get("name", "unknown") + model = disk.get("model", "unknown") + temp = disk.get("temp", None) + if temp is not None: + disk_temps.append({ + "name": name, + "model": model, + "temperature": temp + }) + + return { + "system_temperature": system_temp, + "disk_temperatures": disk_temps, + "warning": system_info.get("temperature_warn", False) if system_info else False + } + + except Exception as e: + logger.error(f"Error getting temperature status: {str(e)}") + return {} + + def browse_files(self, folder_path: str = "", limit: int = 100) -> Dict[str, Any]: + """Просмотр файлов в указанной директории + + Args: + folder_path: Путь к папке (пустая строка для корневых общих папок) + limit: Максимальное количество элементов для возврата + + Returns: + Словарь с информацией о файлах и папках + """ + logger.info(f"Browsing files in {folder_path or 'root'}") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for file browsing") + return {"success": False, "error": "authentication_failed"} + + try: + if not folder_path: + # Если путь не указан, получаем список общих папок + result = self._make_api_request("SYNO.FileStation.List", "list_share", version=2) + + if not result: + logger.error("Failed to list shared folders") + return {"success": False, "error": "api_error"} + + return { + "success": True, + "items": result.get("shares", []), + "path": "", + "is_root": True + } + else: + # Получаем список файлов в указанной директории + params = { + "folder_path": folder_path, + "limit": limit, + "offset": 0, + "sort_by": "name", + "sort_direction": "ASC" + } + + result = self._make_api_request("SYNO.FileStation.List", "list", version=2, params=params) + + if not result: + logger.error(f"Failed to list files in {folder_path}") + return {"success": False, "error": "api_error"} + + return { + "success": True, + "items": result.get("files", []), + "path": folder_path, + "is_root": False, + "total": result.get("total", 0) + } + + except Exception as e: + logger.error(f"Error browsing files: {str(e)}") + return {"success": False, "error": str(e)} + + def manage_service(self, service_name: str, action: str = "status") -> Dict[str, Any]: + """Управление системным сервисом + + Args: + service_name: Имя сервиса + action: Действие (status/start/stop/restart) + + Returns: + Словарь с результатом операции + """ + logger.info(f"Managing service {service_name}, action: {action}") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for service management") + return {"success": False, "error": "authentication_failed"} + + try: + # Проверяем доступное API для управления сервисами + if action == "status": + result = self._make_api_request("SYNO.Core.Service", "get", version=1, + params={"service": service_name}) + else: + result = self._make_api_request("SYNO.Core.Service", action, version=1, + params={"service": service_name}) + + if not result: + logger.error(f"Failed to {action} service {service_name}") + return {"success": False, "error": "api_error"} + + return { + "success": True, + "service": service_name, + "action": action, + "result": result, + "status": result.get("status") if action == "status" else "completed" + } + + except Exception as e: + logger.error(f"Error managing service {service_name}: {str(e)}") + return {"success": False, "error": str(e)} + + def search_files(self, pattern: str, folder_path: str = "", limit: int = 100) -> Dict[str, Any]: + """Поиск файлов по шаблону + + Args: + pattern: Шаблон для поиска + folder_path: Путь к папке для поиска (пустая строка для всех общих папок) + limit: Максимальное количество результатов + + Returns: + Словарь с найденными файлами + """ + logger.info(f"Searching for {pattern} in {folder_path or 'all shared folders'}") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for file search") + return {"success": False, "error": "authentication_failed"} + + try: + if not folder_path: + # Получаем список всех общих папок для поиска + shares_result = self._make_api_request("SYNO.FileStation.List", "list_share", version=2) + + if not shares_result: + logger.error("Failed to list shared folders for search") + return {"success": False, "error": "api_error"} + + # Формируем список путей для поиска + folder_paths = [share.get("path") for share in shares_result.get("shares", [])] + else: + folder_paths = [folder_path] + + # Запускаем поиск + params = { + "folder_path": folder_paths, + "pattern": pattern, + "limit": limit, + "offset": 0 + } + + result = self._make_api_request("SYNO.FileStation.Search", "start", version=2, params=params) + + if not result: + logger.error(f"Failed to start search for {pattern}") + return {"success": False, "error": "api_error"} + + # Получаем taskid для проверки результатов + taskid = result.get("taskid") + if not taskid: + logger.error("No taskid received for search") + return {"success": False, "error": "no_task_id"} + + # Ожидаем завершения поиска + search_result = {"finished": False, "progress": 0} + for _ in range(10): # Максимум 10 попыток + search_status = self._make_api_request("SYNO.FileStation.Search", "status", version=2, + params={"taskid": taskid}) + + if not search_status: + break + + search_result["progress"] = search_status.get("progress", 0) + + if search_status.get("finished", False): + search_result["finished"] = True + break + + time.sleep(0.5) # Пауза между запросами + + # Получаем результаты поиска + if search_result["finished"]: + list_result = self._make_api_request("SYNO.FileStation.Search", "list", version=2, + params={"taskid": taskid, "limit": limit}) + + if list_result: + files = list_result.get("files", []) + return { + "success": True, + "pattern": pattern, + "results": files, + "total": list_result.get("total", len(files)) + } + + # Если не удалось получить результаты, останавливаем поиск + self._make_api_request("SYNO.FileStation.Search", "stop", version=2, + params={"taskid": taskid}) + + return { + "success": False, + "error": "search_timeout", + "progress": search_result["progress"] + } + + except Exception as e: + logger.error(f"Error searching files: {str(e)}") + return {"success": False, "error": str(e)} + + def get_backup_status(self) -> Dict[str, Any]: + """Получение информации о резервном копировании""" + logger.info("Getting backup status information") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for backup status request") + return {"success": False, "error": "authentication_failed"} + + try: + # Пробуем получить информацию о Hyper Backup + hyper_result = self._make_api_request("SYNO.Backup.Repository", "list", version=1) + + # Пробуем получить информацию о задачах Time Backup + time_result = self._make_api_request("SYNO.BackupService.Task", "list", version=1) + + # Проверяем статус резервного копирования USB + usb_result = self._make_api_request("SYNO.USBCopy", "status", version=1) + + backups = { + "hyper_backup": hyper_result.get("backups", []) if hyper_result else [], + "time_backup": time_result.get("tasks", []) if time_result else [], + "usb_copy": {"enabled": usb_result.get("enable", False) if usb_result else False} + } + + return { + "success": True, + "backups": backups, + "available_apis": { + "hyper_backup": hyper_result is not None, + "time_backup": time_result is not None, + "usb_copy": usb_result is not None + } + } + + except Exception as e: + logger.error(f"Error getting backup status: {str(e)}") + return {"success": False, "error": str(e)} + + def check_for_updates(self) -> Dict[str, Any]: + """Проверка наличия обновлений системы""" + logger.info("Checking for system updates") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for update check") + return {"success": False, "error": "authentication_failed"} + + try: + # Получаем текущую информацию о системе + system_info = self._make_api_request("SYNO.DSM.Info", "getinfo", version=2) + + if not system_info: + logger.error("Failed to get system info for update check") + return {"success": False, "error": "api_error"} + + # Проверяем наличие обновлений + update_result = self._make_api_request("SYNO.Core.Upgrade.Server", "check", version=1) + + # Получаем настройки автоматического обновления + settings_result = self._make_api_request("SYNO.Core.Upgrade.Server.Setting", "get", version=1) + + # Получаем информацию о доступных обновлениях + update_info = self._make_api_request("SYNO.Core.Upgrade.Server.Download", "list", version=1) + + current_version = system_info.get("version_string", "unknown") + auto_update_enabled = settings_result.get("auto_update", False) if settings_result else False + + updates = [] + if update_info and "updates" in update_info: + updates = update_info.get("updates", []) + + update_available = len(updates) > 0 + + return { + "success": True, + "current_version": current_version, + "update_available": update_available, + "auto_update_enabled": auto_update_enabled, + "updates": updates + } + + except Exception as e: + logger.error(f"Error checking for updates: {str(e)}") + return {"success": False, "error": str(e)} + + def get_quota_info(self) -> Dict[str, Any]: + """Получение информации о квотах пользователей""" + logger.info("Getting user quota information") + + # Аутентифицируемся перед запросом данных + if not self.sid and not self.login(): + logger.error("Failed to authenticate for quota info request") + return {"success": False, "error": "authentication_failed"} + + try: + # Получаем список пользователей + users_result = self._make_api_request("SYNO.Core.User", "list", version=1) + + if not users_result: + logger.error("Failed to get user list for quota info") + return {"success": False, "error": "api_error"} + + users = users_result.get("users", []) + user_quotas = [] + + # Получаем квоты для каждого пользователя + for user in users: + user_name = user.get("name") + if not user_name: + continue + + quota_result = self._make_api_request("SYNO.Core.Quota", "get", version=1, + params={"user_name": user_name}) + + if quota_result and "quotas" in quota_result: + user_quotas.append({ + "user": user_name, + "quotas": quota_result.get("quotas", []) + }) + + return { + "success": True, + "user_quotas": user_quotas + } + + except Exception as e: + logger.error(f"Error getting quota info: {str(e)}") + return {"success": False, "error": str(e)} diff --git a/src/bot.py b/src/bot.py new file mode 100644 index 0000000..54d7e0e --- /dev/null +++ b/src/bot.py @@ -0,0 +1,144 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Главный модуль запуска телеграм-бота для управления Synology NAS +""" + +import os +import signal +import sys +import asyncio +import logging +from telegram.ext import ( + Application, + CommandHandler, + CallbackQueryHandler, + MessageHandler, + filters +) + +from src.config.config import TELEGRAM_TOKEN +from src.handlers.help_handlers import ( + start_command, + help_command +) +from src.handlers.command_handlers import ( + status_command, + power_command, + power_callback +) +from src.handlers.extended_handlers import ( + storage_command, + shares_command, + system_command, + load_command, + security_command, + check_api_command +) +from src.handlers.advanced_handlers import ( + processes_command, + network_command, + temperature_command, + schedule_command, + browse_command, + search_command, + updates_command, + backup_command, + quickreboot_command, + reboot_command, + sleep_command, + wakeup_command, + quota_command, + schedule_callback, + browse_callback, + advanced_power_callback +) +from src.utils.logger import setup_logging + +async def shutdown(application: Application) -> None: + """Корректное завершение работы бота""" + logger = logging.getLogger(__name__) + logger.info("Stopping Synology Power Control Bot...") + + # Останавливаем прием обновлений + await application.stop() + logger.info("Bot stopped successfully") + +def signal_handler(sig, frame, application=None): + """Обработчик сигналов для корректного завершения""" + logger = logging.getLogger(__name__) + logger.info(f"Received signal {sig}, shutting down gracefully") + + if application: + # Создаем и запускаем задачу завершения в event loop + loop = asyncio.get_event_loop() + if loop.is_running(): + loop.create_task(shutdown(application)) + else: + loop.run_until_complete(shutdown(application)) + + sys.exit(0) + +def main() -> None: + """Основная функция запуска бота""" + # Настройка логирования + setup_logging() + logger = logging.getLogger(__name__) + + # Проверка наличия токена + if not TELEGRAM_TOKEN: + logger.error("Telegram token is not set! Please configure TELEGRAM_TOKEN in .env file.") + return + + # Создание и настройка приложения бота + logger.info("Starting Synology Power Control Bot") + application = Application.builder().token(TELEGRAM_TOKEN).build() + + # Регистрация обработчиков команд + application.add_handler(CommandHandler("start", start_command)) + application.add_handler(CommandHandler("help", help_command)) + application.add_handler(CommandHandler("status", status_command)) + application.add_handler(CommandHandler("power", power_command)) + + # Регистрация расширенных обработчиков команд + application.add_handler(CommandHandler("storage", storage_command)) + application.add_handler(CommandHandler("shares", shares_command)) + application.add_handler(CommandHandler("system", system_command)) + application.add_handler(CommandHandler("load", load_command)) + application.add_handler(CommandHandler("security", security_command)) + application.add_handler(CommandHandler("checkapi", check_api_command)) + + # Регистрация продвинутых обработчиков команд + application.add_handler(CommandHandler("processes", processes_command)) + application.add_handler(CommandHandler("network", network_command)) + application.add_handler(CommandHandler("temperature", temperature_command)) + application.add_handler(CommandHandler("schedule", schedule_command)) + application.add_handler(CommandHandler("browse", browse_command)) + application.add_handler(CommandHandler("search", search_command)) + application.add_handler(CommandHandler("updates", updates_command)) + application.add_handler(CommandHandler("backup", backup_command)) + application.add_handler(CommandHandler("quickreboot", quickreboot_command)) + application.add_handler(CommandHandler("reboot", reboot_command)) + application.add_handler(CommandHandler("sleep", sleep_command)) + application.add_handler(CommandHandler("wakeup", wakeup_command)) + application.add_handler(CommandHandler("quota", quota_command)) + + # Регистрация обработчиков callback-запросов + # Сначала обрабатываем более специфичные паттерны + application.add_handler(CallbackQueryHandler(advanced_power_callback, pattern="^(confirm|cancel)_(reboot|sleep)$")) # Для advanced_handlers.py + # Затем более общие паттерны + application.add_handler(CallbackQueryHandler(power_callback, pattern="^power_|^reboot$|^cancel$")) # Для command_handlers.py + application.add_handler(CallbackQueryHandler(schedule_callback, pattern="^schedule_")) + application.add_handler(CallbackQueryHandler(browse_callback, pattern="^browse_")) + + # Настройка обработчиков сигналов для корректного завершения + signal.signal(signal.SIGINT, lambda sig, frame: signal_handler(sig, frame, application)) + signal.signal(signal.SIGTERM, lambda sig, frame: signal_handler(sig, frame, application)) + + # Запуск бота + logger.info("Bot started. Press Ctrl+C to stop.") + application.run_polling(allowed_updates=["message", "callback_query"]) + +if __name__ == "__main__": + main() diff --git a/src/config/config.py b/src/config/config.py new file mode 100644 index 0000000..17840bf --- /dev/null +++ b/src/config/config.py @@ -0,0 +1,31 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Конфигурационный файл для телеграм-бота управления Synology NAS +""" + +import os +from dotenv import load_dotenv + +# Загрузка переменных окружения из .env файла +load_dotenv() + +# Конфигурация для Telegram бота +TELEGRAM_TOKEN = os.getenv("TELEGRAM_TOKEN") +ADMIN_USER_IDS = list(map(int, os.getenv("ADMIN_USER_IDS", "").split(","))) if os.getenv("ADMIN_USER_IDS") else [] + +# Конфигурация для Synology NAS +SYNOLOGY_HOST = os.getenv("SYNOLOGY_HOST") +SYNOLOGY_PORT = int(os.getenv("SYNOLOGY_PORT", 5000)) +SYNOLOGY_USERNAME = os.getenv("SYNOLOGY_USERNAME") +SYNOLOGY_PASSWORD = os.getenv("SYNOLOGY_PASSWORD") +SYNOLOGY_SECURE = os.getenv("SYNOLOGY_SECURE", "False").lower() == "true" +SYNOLOGY_TIMEOUT = int(os.getenv("SYNOLOGY_TIMEOUT", 10)) +SYNOLOGY_API_VERSION = int(os.getenv("SYNOLOGY_API_VERSION", 1)) +SYNOLOGY_POWER_API = os.getenv("SYNOLOGY_POWER_API", "SYNO.Core.System") +SYNOLOGY_INFO_API = os.getenv("SYNOLOGY_INFO_API", "SYNO.DSM.Info") + +# Конфигурация для Wake-on-LAN +SYNOLOGY_MAC = os.getenv("SYNOLOGY_MAC") +WOL_PORT = int(os.getenv("WOL_PORT", 9)) diff --git a/src/handlers/advanced_handlers.py b/src/handlers/advanced_handlers.py new file mode 100644 index 0000000..9cc9ee6 --- /dev/null +++ b/src/handlers/advanced_handlers.py @@ -0,0 +1,972 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Расширенные обработчики команд для управления Synology NAS +""" + +import logging +from datetime import datetime +from typing import List, Dict, Any +from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup +from telegram.ext import ContextTypes + +from src.config.config import ADMIN_USER_IDS +from src.api.synology import SynologyAPI + +logger = logging.getLogger(__name__) + +# Инициализация API Synology +synology_api = SynologyAPI() + +async def processes_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /processes для получения списка активных процессов""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + message = await update.message.reply_text("⏳ Получение информации о запущенных процессах...") + + if not synology_api.is_online(): + await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о процессах.", parse_mode="HTML") + return + + try: + processes = synology_api.get_processes(limit=15) # Получаем топ-15 процессов + + if not processes: + await message.edit_text("❌ Ошибка получения информации о процессах\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML") + return + except Exception as e: + await message.edit_text(f"❌ Ошибка получения информации о процессах\n\nПричина: {str(e)}", parse_mode="HTML") + return + + # Формируем сообщение о процессах + reply_text = f"⚙️ Активные процессы Synology NAS\n\n" + + for process in processes: + name = process.get("name", "unknown") + pid = process.get("pid", "?") + cpu_usage = process.get("cpu_usage", 0) + memory_usage = process.get("memory_usage", 0) + + reply_text += f"• {name} (PID: {pid})\n" + reply_text += f" └ CPU: {cpu_usage:.1f}%, Память: {memory_usage:.1f}%\n" + + reply_text += f"\nПоказано {len(processes)} наиболее активных процессов" + await message.edit_text(reply_text, parse_mode="HTML") + +async def network_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /network для получения информации о сетевых подключениях""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + message = await update.message.reply_text("⏳ Получение информации о сетевых подключениях...") + + if not synology_api.is_online(): + await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о сетевых подключениях.", parse_mode="HTML") + return + + try: + network_status = synology_api.get_network_status() + + if not network_status: + await message.edit_text("❌ Ошибка получения информации о сетевых подключениях\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML") + return + except Exception as e: + await message.edit_text(f"❌ Ошибка получения информации о сетевых подключениях\n\nПричина: {str(e)}", parse_mode="HTML") + return + + # Формируем сообщение о сетевых интерфейсах + interfaces = network_status.get("interfaces", []) + + reply_text = f"🌐 Сетевые подключения Synology NAS\n\n" + + for interface in interfaces: + name = interface.get("id", "unknown") + ip = interface.get("ip", "Нет данных") + mac = interface.get("mac", "Нет данных") + status = "Активен" if interface.get("status") else "Неактивен" + + # Информация о трафике + rx_bytes = interface.get("rx", 0) / (1024**2) # Перевод в МБ + tx_bytes = interface.get("tx", 0) / (1024**2) # Перевод в МБ + + reply_text += f"• {name} ({status})\n" + reply_text += f" └ IP: {ip}, MAC: {mac}\n" + + if rx_bytes > 0 or tx_bytes > 0: + reply_text += f" └ Получено: {rx_bytes:.2f} МБ, Отправлено: {tx_bytes:.2f} МБ\n" + + await message.edit_text(reply_text, parse_mode="HTML") + +async def temperature_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /temperature для мониторинга температуры""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + message = await update.message.reply_text("⏳ Получение информации о температуре...") + + if not synology_api.is_online(): + await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о температуре.", parse_mode="HTML") + return + + try: + temp_status = synology_api.get_temperature_status() + + if not temp_status: + await message.edit_text("❌ Ошибка получения информации о температуре\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML") + return + except Exception as e: + await message.edit_text(f"❌ Ошибка получения информации о температуре\n\nПричина: {str(e)}", parse_mode="HTML") + return + + # Формируем сообщение о температуре + system_temp = temp_status.get("system_temperature") + disk_temps = temp_status.get("disk_temperatures", []) + is_warning = temp_status.get("warning", False) + + # Выбор emoji в зависимости от температуры + temp_emoji = "🔥" if is_warning else "🌡️" + + reply_text = f"{temp_emoji} Температура Synology NAS\n\n" + + if system_temp is not None: + temp_status_text = "❗ ПОВЫШЕННАЯ" if is_warning else "✅ Нормальная" + reply_text += f"Температура системы: {system_temp}°C ({temp_status_text})\n\n" + + if disk_temps: + reply_text += "Температура дисков:\n" + for disk in disk_temps: + name = disk.get("name", "unknown") + model = disk.get("model", "unknown") + temp = disk.get("temperature", 0) + + disk_temp_emoji = "🔥" if temp > 45 else "✅" + reply_text += f"• {disk_temp_emoji} {name} ({model}): {temp}°C\n" + + await message.edit_text(reply_text, parse_mode="HTML") + +async def schedule_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /schedule для управления расписанием питания""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + message = await update.message.reply_text("⏳ Получение информации о расписании питания...") + + if not synology_api.is_online(): + await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о расписании питания.", parse_mode="HTML") + return + + try: + schedule = synology_api.get_power_schedule() + + if not schedule: + await message.edit_text("❌ Ошибка получения информации о расписании питания\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML") + return + except Exception as e: + await message.edit_text(f"❌ Ошибка получения информации о расписании питания\n\nПричина: {str(e)}", parse_mode="HTML") + return + + # Формируем сообщение о расписании питания + boot_tasks = schedule.get("boot_tasks", []) + shutdown_tasks = schedule.get("shutdown_tasks", []) + + reply_text = f"⏱️ Расписание питания Synology NAS\n\n" + + if boot_tasks: + reply_text += "Расписание включения:\n" + for task in boot_tasks: + days = task.get("day", []) + time = task.get("time", "00:00") + enabled = task.get("enabled", False) + + # Преобразуем номера дней в названия + day_names = [] + for day in days: + if day == 0: day_names.append("Пн") + elif day == 1: day_names.append("Вт") + elif day == 2: day_names.append("Ср") + elif day == 3: day_names.append("Чт") + elif day == 4: day_names.append("Пт") + elif day == 5: day_names.append("Сб") + elif day == 6: day_names.append("Вс") + + status = "✅ Активно" if enabled else "❌ Отключено" + day_str = ", ".join(day_names) if day_names else "Нет дней" + + reply_text += f"• {status}: {time} ({day_str})\n" + else: + reply_text += "Расписание включения: Не настроено\n" + + reply_text += "\n" + + if shutdown_tasks: + reply_text += "Расписание выключения:\n" + for task in shutdown_tasks: + days = task.get("day", []) + time = task.get("time", "00:00") + enabled = task.get("enabled", False) + + # Преобразуем номера дней в названия + day_names = [] + for day in days: + if day == 0: day_names.append("Пн") + elif day == 1: day_names.append("Вт") + elif day == 2: day_names.append("Ср") + elif day == 3: day_names.append("Чт") + elif day == 4: day_names.append("Пт") + elif day == 5: day_names.append("Сб") + elif day == 6: day_names.append("Вс") + + status = "✅ Активно" if enabled else "❌ Отключено" + day_str = ", ".join(day_names) if day_names else "Нет дней" + + reply_text += f"• {status}: {time} ({day_str})\n" + else: + reply_text += "Расписание выключения: Не настроено\n" + + # Добавляем кнопки для управления расписанием + keyboard = [ + [ + InlineKeyboardButton("➕ Добавить включение", callback_data="schedule_add_boot"), + InlineKeyboardButton("➕ Добавить выключение", callback_data="schedule_add_shutdown") + ], + [ + InlineKeyboardButton("🗑️ Удалить расписание", callback_data="schedule_delete") + ] + ] + reply_markup = InlineKeyboardMarkup(keyboard) + + await message.edit_text(reply_text, parse_mode="HTML", reply_markup=reply_markup) + +async def browse_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /browse для просмотра файлов""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + # Получаем путь из аргументов команды или используем корневую директорию + path = " ".join(context.args) if context.args else "" + + message = await update.message.reply_text(f"⏳ Получение содержимого {'папки ' + path if path else 'общих папок'}...") + + if not synology_api.is_online(): + await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить список файлов.", parse_mode="HTML") + return + + try: + browse_result = synology_api.browse_files(folder_path=path) + + if not browse_result.get("success", False): + error = browse_result.get("error", "unknown") + await message.edit_text(f"❌ Ошибка получения списка файлов\n\nПричина: {error}", parse_mode="HTML") + return + + items = browse_result.get("items", []) + current_path = browse_result.get("path", "") + is_root = browse_result.get("is_root", True) + + except Exception as e: + await message.edit_text(f"❌ Ошибка получения списка файлов\n\nПричина: {str(e)}", parse_mode="HTML") + return + + # Формируем сообщение о файлах и папках + if is_root: + reply_text = f"📁 Общие папки Synology NAS\n\n" + else: + reply_text = f"📁 Содержимое папки\n{current_path}\n\n" + + # Сортируем: сначала папки, потом файлы + folders = [] + files = [] + + for item in items: + if is_root: # Для корневого уровня все элементы - это общие папки + name = item.get("name", "unknown") + path = item.get("path", "") + folders.append((name, path, True)) + else: + name = item.get("name", "unknown") + path = item.get("path", "") + is_dir = item.get("isdir", False) + + if is_dir: + folders.append((name, path, False)) + else: + # Для файлов получаем размер + size = item.get("additional", {}).get("size", 0) + size_str = format_size(size) + files.append((name, path, size_str)) + + # Добавляем папки в сообщение + if folders: + for name, path, is_share in folders: + # Для общих папок добавляем иконку дома + icon = "🏠" if is_share else "📁" + reply_text += f"{icon} {name}\n" + + # Добавляем файлы в сообщение + if files: + for name, path, size in files: + # Выбираем иконку в зависимости от расширения + icon = get_file_icon(name) + reply_text += f"{icon} {name} ({size})\n" + + # Если нет элементов для отображения + if not folders and not files: + reply_text += "📭 Папка пуста\n" + + # Добавляем кнопку возврата наверх, если мы не в корне + if not is_root: + # Определяем родительскую директорию + parent_path = "/".join(current_path.split("/")[:-1]) if "/" in current_path else "" + + keyboard = [ + [InlineKeyboardButton("🔼 Вернуться на уровень выше", callback_data=f"browse_{parent_path}")] + ] + reply_markup = InlineKeyboardMarkup(keyboard) + + await message.edit_text(reply_text, parse_mode="HTML", reply_markup=reply_markup) + else: + await message.edit_text(reply_text, parse_mode="HTML") + +async def search_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /search для поиска файлов""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + # Получаем шаблон поиска из аргументов команды + if not context.args: + await update.message.reply_text("❌ Укажите шаблон для поиска: /search <шаблон>") + return + + pattern = " ".join(context.args) + + message = await update.message.reply_text(f"⏳ Поиск файлов по шаблону «{pattern}»...") + + if not synology_api.is_online(): + await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно выполнить поиск файлов.", parse_mode="HTML") + return + + try: + search_result = synology_api.search_files(pattern=pattern, limit=20) + + if not search_result.get("success", False): + error = search_result.get("error", "unknown") + progress = search_result.get("progress", 0) + + if error == "search_timeout": + await message.edit_text(f"❌ Превышено время ожидания результатов поиска\n\nПроцесс поиска выполнен на {progress}%", parse_mode="HTML") + else: + await message.edit_text(f"❌ Ошибка при поиске файлов\n\nПричина: {error}", parse_mode="HTML") + return + + files = search_result.get("results", []) + total = search_result.get("total", len(files)) + + except Exception as e: + await message.edit_text(f"❌ Ошибка при поиске файлов\n\nПричина: {str(e)}", parse_mode="HTML") + return + + # Формируем сообщение с результатами поиска + reply_text = f"🔍 Результаты поиска по шаблону «{pattern}»\n\n" + + if not files: + reply_text += "📭 Файлы не найдены" + else: + # Сортируем: сначала папки, потом файлы + folders = [] + found_files = [] + + for item in files: + name = item.get("name", "unknown") + path = item.get("path", "") + is_dir = item.get("isdir", False) + + if is_dir: + folders.append((name, path)) + else: + # Для файлов получаем размер и путь к родительской папке + size = item.get("additional", {}).get("size", 0) + size_str = format_size(size) + parent_path = "/".join(path.split("/")[:-1]) + found_files.append((name, path, size_str, parent_path)) + + # Добавляем папки в сообщение + if folders: + reply_text += "Найденные папки:\n" + for name, path in folders[:5]: # Показываем первые 5 папок + reply_text += f"📁 {name}\n" + + if len(folders) > 5: + reply_text += f"...и еще {len(folders) - 5} папок\n" + + reply_text += "\n" + + # Добавляем файлы в сообщение + if found_files: + reply_text += "Найденные файлы:\n" + for name, path, size, parent in found_files[:10]: # Показываем первые 10 файлов + icon = get_file_icon(name) + reply_text += f"{icon} {name} ({size})\n" + reply_text += f" Путь: .../{path.split('/')[-2]}/\n" + + if len(found_files) > 10: + reply_text += f"...и еще {len(found_files) - 10} файлов\n" + + # Добавляем информацию о общем количестве результатов + reply_text += f"\nВсего найдено: {total} элементов" + + await message.edit_text(reply_text, parse_mode="HTML") + +async def updates_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /updates для проверки обновлений""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + message = await update.message.reply_text("⏳ Проверка доступных обновлений...") + + if not synology_api.is_online(): + await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно проверить наличие обновлений.", parse_mode="HTML") + return + + try: + update_info = synology_api.check_for_updates() + + if not update_info.get("success", False): + error = update_info.get("error", "unknown") + await message.edit_text(f"❌ Ошибка при проверке обновлений\n\nПричина: {error}", parse_mode="HTML") + return + + current_version = update_info.get("current_version", "unknown") + update_available = update_info.get("update_available", False) + auto_update = update_info.get("auto_update_enabled", False) + updates = update_info.get("updates", []) + + except Exception as e: + await message.edit_text(f"❌ Ошибка при проверке обновлений\n\nПричина: {str(e)}", parse_mode="HTML") + return + + # Формируем сообщение об обновлениях + if update_available: + reply_text = f"🔄 Доступны обновления DSM\n\n" + reply_text += f"Текущая версия: {current_version}\n" + reply_text += f"Автообновление: {'✅ Включено' if auto_update else '❌ Отключено'}\n\n" + reply_text += "Доступные обновления:\n" + + for update_item in updates: + update_name = update_item.get("name", "unknown") + update_version = update_item.get("version", "unknown") + update_size = update_item.get("size", 0) + update_size_str = format_size(update_size) + + reply_text += f"• {update_name} v{update_version}\n" + reply_text += f" └ Размер: {update_size_str}\n" + else: + reply_text = f"✅ Система в актуальном состоянии\n\n" + reply_text += f"Текущая версия: {current_version}\n" + reply_text += f"Автообновление: {'✅ Включено' if auto_update else '❌ Отключено'}\n" + + await message.edit_text(reply_text, parse_mode="HTML") + +async def backup_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /backup для управления резервным копированием""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + message = await update.message.reply_text("⏳ Получение информации о резервном копировании...") + + if not synology_api.is_online(): + await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о резервном копировании.", parse_mode="HTML") + return + + try: + backup_status = synology_api.get_backup_status() + + if not backup_status.get("success", False): + error = backup_status.get("error", "unknown") + await message.edit_text(f"❌ Ошибка получения информации о резервном копировании\n\nПричина: {error}", parse_mode="HTML") + return + + backups = backup_status.get("backups", {}) + api_status = backup_status.get("available_apis", {}) + + except Exception as e: + await message.edit_text(f"❌ Ошибка получения информации о резервном копировании\n\nПричина: {str(e)}", parse_mode="HTML") + return + + # Формируем сообщение о резервном копировании + reply_text = f"💾 Резервное копирование Synology NAS\n\n" + + # Информация о Hyper Backup + hyper_backups = backups.get("hyper_backup", []) + hyper_api_available = api_status.get("hyper_backup", False) + + if hyper_api_available: + reply_text += "Hyper Backup:\n" + + if hyper_backups: + for backup in hyper_backups: + name = backup.get("name", "unknown") + status = backup.get("status", "unknown") + last_backup = backup.get("last_backup", "never") + + status_emoji = "✅" if status.lower() == "success" else "⚠️" + reply_text += f"• {status_emoji} {name}\n" + reply_text += f" └ Последнее копирование: {last_backup}\n" + else: + reply_text += "Задачи Hyper Backup не настроены\n" + + reply_text += "\n" + + # Информация о Time Backup + time_backups = backups.get("time_backup", []) + time_api_available = api_status.get("time_backup", False) + + if time_api_available: + reply_text += "Time Backup:\n" + + if time_backups: + for backup in time_backups: + name = backup.get("name", "unknown") + status = backup.get("status", "unknown") + + status_emoji = "✅" if status.lower() == "normal" else "⚠️" + reply_text += f"• {status_emoji} {name}\n" + else: + reply_text += "Задачи Time Backup не настроены\n" + + reply_text += "\n" + + # Информация о USB Copy + usb_copy = backups.get("usb_copy", {}) + usb_api_available = api_status.get("usb_copy", False) + + if usb_api_available: + usb_enabled = usb_copy.get("enabled", False) + usb_status = "✅ Включено" if usb_enabled else "❌ Отключено" + + reply_text += f"USB Copy: {usb_status}\n\n" + + # Если ни один из API не доступен + if not any(api_status.values()): + reply_text += "API для резервного копирования не доступны на вашем NAS или требуют дополнительных прав доступа.\n" + + await message.edit_text(reply_text, parse_mode="HTML") + +async def reboot_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /reboot для перезагрузки NAS""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + # Добавляем подтверждение перед перезагрузкой + keyboard = [ + [ + InlineKeyboardButton("✅ Да, перезагрузить", callback_data="confirm_reboot"), + InlineKeyboardButton("❌ Отмена", callback_data="cancel_reboot") + ] + ] + reply_markup = InlineKeyboardMarkup(keyboard) + + await update.message.reply_text( + "⚠️ Вы уверены, что хотите перезагрузить Synology NAS?\n\n" + "Это действие может привести к прерыванию работы всех сервисов.", + parse_mode="HTML", + reply_markup=reply_markup + ) + +async def sleep_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /sleep для перевода NAS в спящий режим""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + # Добавляем подтверждение перед отправкой в спящий режим + keyboard = [ + [ + InlineKeyboardButton("✅ Да, усыпить", callback_data="confirm_sleep"), + InlineKeyboardButton("❌ Отмена", callback_data="cancel_sleep") + ] + ] + reply_markup = InlineKeyboardMarkup(keyboard) + + await update.message.reply_text( + "⚠️ Вы уверены, что хотите перевести Synology NAS в спящий режим?\n\n" + "Это действие приведет к остановке всех сервисов и отключению NAS.", + parse_mode="HTML", + reply_markup=reply_markup + ) + +async def quickreboot_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /quickreboot для быстрой перезагрузки NAS""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + message = await update.message.reply_text("⏳ Выполняется перезагрузка Synology NAS...") + + if not synology_api.is_online(): + await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно выполнить перезагрузку.", parse_mode="HTML") + return + + try: + # Выполняем перезагрузку + result = synology_api.reboot_system() + + if result: + # Формируем сообщение об успешной перезагрузке + reply_text = "🔄 Synology NAS перезагружается\n\n" + reply_text += "Перезагрузка обычно занимает 3-5 минут. После перезагрузки NAS будет снова доступен." + await message.edit_text(reply_text, parse_mode="HTML") + else: + await message.edit_text("❌ Ошибка при выполнении перезагрузки\n\nПроверьте журналы NAS для получения дополнительной информации.", parse_mode="HTML") + + except Exception as e: + await message.edit_text(f"❌ Ошибка при выполнении перезагрузки\n\nПричина: {str(e)}", parse_mode="HTML") + return + +async def wakeup_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /wakeup для включения NAS""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + message = await update.message.reply_text("⏳ Отправка пакета Wake-on-LAN для включения Synology NAS...") + + # Проверяем, не включен ли NAS уже + if synology_api.is_online(force_check=True): + await message.edit_text("ℹ️ Synology NAS уже включен\n\nНет необходимости отправлять сигнал пробуждения.", parse_mode="HTML") + return + + try: + # Отправляем сигнал пробуждения + result = synology_api.power_on() + + if result: + # Формируем сообщение об успешном включении + reply_text = "✅ Synology NAS успешно включен\n\n" + reply_text += "NAS полностью готов к работе." + await message.edit_text(reply_text, parse_mode="HTML") + else: + await message.edit_text("❌ Ошибка при включении NAS\n\nВозможные причины:\n- Функция Wake-on-LAN не настроена на NAS\n- Неверно указан MAC-адрес\n- Проблемы с сетевым подключением", parse_mode="HTML") + + except Exception as e: + await message.edit_text(f"❌ Ошибка при включении NAS\n\nПричина: {str(e)}", parse_mode="HTML") + return + +async def quota_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /quota для просмотра информации о квотах""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + message = await update.message.reply_text("⏳ Получение информации о квотах пользователей...") + + if not synology_api.is_online(): + await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о квотах.", parse_mode="HTML") + return + + try: + quota_info = synology_api.get_quota_info() + + if not quota_info.get("success", False): + error = quota_info.get("error", "unknown") + await message.edit_text(f"❌ Ошибка получения информации о квотах\n\nПричина: {error}", parse_mode="HTML") + return + + user_quotas = quota_info.get("user_quotas", []) + + except Exception as e: + await message.edit_text(f"❌ Ошибка получения информации о квотах\n\nПричина: {str(e)}", parse_mode="HTML") + return + + # Формируем сообщение о квотах + reply_text = f"📊 Квоты пользователей Synology NAS\n\n" + + if not user_quotas: + reply_text += "Квоты пользователей не настроены или недоступны" + else: + for user_quota in user_quotas: + user = user_quota.get("user", "unknown") + quotas = user_quota.get("quotas", []) + + if quotas: + reply_text += f"Пользователь {user}:\n" + + for quota in quotas: + volume = quota.get("volume_name", "unknown") + limit = quota.get("limit", 0) + used = quota.get("used", 0) + + # Переводим байты в ГБ + limit_gb = limit / (1024**3) if limit > 0 else 0 + used_gb = used / (1024**3) + + # Рассчитываем процент использования + if limit_gb > 0: + usage_percent = (used_gb / limit_gb) * 100 + reply_text += f"• Том {volume}: {used_gb:.2f} ГБ из {limit_gb:.2f} ГБ ({usage_percent:.1f}%)\n" + else: + reply_text += f"• Том {volume}: {used_gb:.2f} ГБ (без ограничений)\n" + + reply_text += "\n" + + await message.edit_text(reply_text, parse_mode="HTML") + +async def schedule_callback(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик callback-запросов для управления расписанием питания""" + query = update.callback_query + await query.answer() + + user_id = update.effective_user.id + if user_id not in ADMIN_USER_IDS: + await query.edit_message_text("У вас нет доступа к этому боту.") + return + + action = query.data + + if action.startswith("schedule_"): + action_type = action.split("_")[1] + + if action_type == "add_boot": + # Логика добавления расписания включения + # В реальном боте здесь будет диалог для настройки расписания + await query.edit_message_text("⚙️ Добавление расписания включения\n\nЭта функция находится в разработке.", parse_mode="HTML") + + elif action_type == "add_shutdown": + # Логика добавления расписания выключения + await query.edit_message_text("⚙️ Добавление расписания выключения\n\nЭта функция находится в разработке.", parse_mode="HTML") + + elif action_type == "delete": + # Логика удаления расписания + await query.edit_message_text("⚙️ Удаление расписания\n\nЭта функция находится в разработке.", parse_mode="HTML") + +async def browse_callback(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик callback-запросов для навигации по файловой системе""" + query = update.callback_query + await query.answer() + + user_id = update.effective_user.id + if user_id not in ADMIN_USER_IDS: + await query.edit_message_text("У вас нет доступа к этому боту.") + return + + action = query.data + + if action.startswith("browse_"): + path = action[7:] # Убираем префикс "browse_" + + # Используем команду browse с указанным путем + message = await query.edit_message_text(f"⏳ Получение содержимого {'папки ' + path if path else 'общих папок'}...") + + if not synology_api.is_online(): + await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить список файлов.", parse_mode="HTML") + return + + try: + browse_result = synology_api.browse_files(folder_path=path) + + if not browse_result.get("success", False): + error = browse_result.get("error", "unknown") + await message.edit_text(f"❌ Ошибка получения списка файлов\n\nПричина: {error}", parse_mode="HTML") + return + + items = browse_result.get("items", []) + current_path = browse_result.get("path", "") + is_root = browse_result.get("is_root", True) + + except Exception as e: + await message.edit_text(f"❌ Ошибка получения списка файлов\n\nПричина: {str(e)}", parse_mode="HTML") + return + + # Формируем сообщение о файлах и папках (аналогично функции browse_command) + if is_root: + reply_text = f"📁 Общие папки Synology NAS\n\n" + else: + reply_text = f"📁 Содержимое папки\n{current_path}\n\n" + + # Сортируем: сначала папки, потом файлы + folders = [] + files = [] + + for item in items: + if is_root: # Для корневого уровня все элементы - это общие папки + name = item.get("name", "unknown") + path = item.get("path", "") + folders.append((name, path, True)) + else: + name = item.get("name", "unknown") + path = item.get("path", "") + is_dir = item.get("isdir", False) + + if is_dir: + folders.append((name, path, False)) + else: + # Для файлов получаем размер + size = item.get("additional", {}).get("size", 0) + size_str = format_size(size) + files.append((name, path, size_str)) + + # Добавляем папки в сообщение + if folders: + for name, path, is_share in folders: + # Для общих папок добавляем иконку дома + icon = "🏠" if is_share else "📁" + reply_text += f"{icon} {name}\n" + + # Добавляем файлы в сообщение + if files: + for name, path, size in files: + # Выбираем иконку в зависимости от расширения + icon = get_file_icon(name) + reply_text += f"{icon} {name} ({size})\n" + + # Если нет элементов для отображения + if not folders and not files: + reply_text += "📭 Папка пуста\n" + + # Добавляем кнопку возврата наверх, если мы не в корне + if not is_root: + # Определяем родительскую директорию + parent_path = "/".join(current_path.split("/")[:-1]) if "/" in current_path else "" + + keyboard = [ + [InlineKeyboardButton("🔼 Вернуться на уровень выше", callback_data=f"browse_{parent_path}")] + ] + reply_markup = InlineKeyboardMarkup(keyboard) + + await message.edit_text(reply_text, parse_mode="HTML", reply_markup=reply_markup) + else: + await message.edit_text(reply_text, parse_mode="HTML") + +async def advanced_power_callback(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик callback-запросов для управления питанием""" + query = update.callback_query + await query.answer() + + user_id = update.effective_user.id + if user_id not in ADMIN_USER_IDS: + await query.edit_message_text("У вас нет доступа к этому боту.") + return + + action = query.data + + if action == "confirm_reboot": + # Выполняем перезагрузку + message = await query.edit_message_text("⏳ Выполняется перезагрузка Synology NAS...") + + if not synology_api.is_online(): + await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно выполнить перезагрузку.", parse_mode="HTML") + return + + try: + result = synology_api.reboot_system() + + if result: + reply_text = "🔄 Synology NAS перезагружается\n\n" + reply_text += "Перезагрузка обычно занимает 3-5 минут. После перезагрузки NAS будет снова доступен." + await message.edit_text(reply_text, parse_mode="HTML") + else: + await message.edit_text("❌ Ошибка при выполнении перезагрузки\n\nПроверьте журналы NAS для получения дополнительной информации.", parse_mode="HTML") + except Exception as e: + await message.edit_text(f"❌ Ошибка при выполнении перезагрузки\n\nПричина: {str(e)}", parse_mode="HTML") + + elif action == "cancel_reboot": + # Отменяем перезагрузку + await query.edit_message_text("✅ Перезагрузка отменена", parse_mode="HTML") + + elif action == "confirm_sleep": + # Выполняем переход в спящий режим (выключение) + message = await query.edit_message_text("⏳ Перевод Synology NAS в спящий режим...") + + if not synology_api.is_online(): + await message.edit_text("❌ Synology NAS уже оффлайн\n\nНевозможно выполнить переход в спящий режим.", parse_mode="HTML") + return + + try: + result = synology_api.power_off() + + if result: + reply_text = "💤 Synology NAS переведен в спящий режим\n\n" + reply_text += "Для пробуждения используйте команду /wakeup" + await message.edit_text(reply_text, parse_mode="HTML") + else: + await message.edit_text("❌ Ошибка при переходе в спящий режим\n\nПроверьте журналы NAS для получения дополнительной информации.", parse_mode="HTML") + except Exception as e: + await message.edit_text(f"❌ Ошибка при переходе в спящий режим\n\nПричина: {str(e)}", parse_mode="HTML") + + elif action == "cancel_sleep": + # Отменяем переход в спящий режим + await query.edit_message_text("✅ Переход в спящий режим отменен", parse_mode="HTML") + +# Вспомогательные функции + +def format_size(size_bytes: int) -> str: + """Преобразует размер в байтах в человекочитаемый формат""" + if size_bytes < 1024: + return f"{size_bytes} Б" + elif size_bytes < 1024**2: + return f"{size_bytes/1024:.1f} КБ" + elif size_bytes < 1024**3: + return f"{size_bytes/1024**2:.1f} МБ" + else: + return f"{size_bytes/1024**3:.1f} ГБ" + +def get_file_icon(filename: str) -> str: + """Возвращает эмодзи-иконку в зависимости от типа файла""" + extension = filename.lower().split('.')[-1] if '.' in filename else '' + + if extension in ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'tiff', 'webp']: + return "🖼️" + elif extension in ['mp4', 'avi', 'mov', 'mkv', 'wmv', 'flv']: + return "🎬" + elif extension in ['mp3', 'wav', 'ogg', 'flac', 'aac']: + return "🎵" + elif extension in ['doc', 'docx', 'txt', 'rtf', 'odt']: + return "📄" + elif extension in ['xls', 'xlsx', 'csv']: + return "📊" + elif extension in ['ppt', 'pptx']: + return "📑" + elif extension in ['pdf']: + return "📕" + elif extension in ['zip', 'rar', '7z', 'tar', 'gz']: + return "🗜️" + elif extension in ['exe', 'msi']: + return "⚙️" + else: + return "📄" diff --git a/src/handlers/command_handlers.py b/src/handlers/command_handlers.py new file mode 100644 index 0000000..1b50d8e --- /dev/null +++ b/src/handlers/command_handlers.py @@ -0,0 +1,331 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Обработчики команд для телеграм-бота +""" + +import logging +import socket +from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup +from telegram.ext import ContextTypes + +from src.config.config import ( + ADMIN_USER_IDS, SYNOLOGY_HOST, SYNOLOGY_PORT, SYNOLOGY_MAC +) +from src.api.synology import SynologyAPI + +logger = logging.getLogger(__name__) + +# Инициализация API Synology +synology_api = SynologyAPI() + +async def status_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /status""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + message = await update.message.reply_text("⏳ Проверяю статус Synology NAS...") + + is_online = synology_api.is_online() + + if is_online: + try: + # Если NAS включен, попробуем получить дополнительную информацию + system_info = synology_api.get_system_status() + + if system_info and system_info.get("status") != "error": + model = system_info.get("model", "Неизвестная модель") + version = system_info.get("version_string", system_info.get("version", "Неизвестная версия")) + uptime_seconds = system_info.get("up_time", system_info.get("uptime", 0)) + + # Преобразование времени работы в удобочитаемый формат + days, remainder = divmod(int(uptime_seconds), 86400) + hours, remainder = divmod(remainder, 3600) + minutes, seconds = divmod(remainder, 60) + uptime_str = f"{days}д {hours}ч {minutes}м {seconds}с" + + await message.edit_text( + f"✅ Synology NAS онлайн\n\n" + f"Модель: {model}\n" + f"Версия DSM: {version}\n" + f"Время работы: {uptime_str}", + parse_mode="HTML" + ) + else: + # Обработка возможной ошибки API + error_info = "" + if system_info and system_info.get("status") == "error": + error_code = system_info.get("error_code", "неизвестно") + error_info = f"\nКод ошибки API: {error_code}" + + # Проверяем порт и сеть + network_info = "" + try: + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + s.settimeout(1) + result = s.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT)) + s.close() + if result == 0: + network_info = f"\n\nСетевая информация:\nIP: {SYNOLOGY_HOST}\nПорт {SYNOLOGY_PORT}: открыт" + else: + network_info = f"\n\nСетевая информация:\nIP: {SYNOLOGY_HOST}\nПорт {SYNOLOGY_PORT}: закрыт (код {result})" + except Exception as e: + network_info = f"\n\nСетевая информация:\nIP: {SYNOLOGY_HOST}\nОшибка проверки порта: {str(e)}" + + await message.edit_text( + f"✅ Synology NAS онлайн\n\n" + f"Устройство доступно по сети, но детальная информация через API недоступна. " + f"Возможно, необходимо проверить учетные данные или права доступа." + f"{error_info}" + f"{network_info}", + parse_mode="HTML" + ) + except Exception as e: + await message.edit_text( + f"✅ Synology NAS онлайн\n\n" + f"Ошибка при получении информации: {str(e)[:100]}...\n\n" + f"Сетевая информация:\nIP: {SYNOLOGY_HOST}\nПорт: {SYNOLOGY_PORT}", + parse_mode="HTML" + ) + else: + # Устройство не в сети, проверим соседние порты для диагностики + port_scan_info = "" + try: + for test_port in [SYNOLOGY_PORT-1, SYNOLOGY_PORT, SYNOLOGY_PORT+1, 5000, 5001]: + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + s.settimeout(1) + result = s.connect_ex((SYNOLOGY_HOST, test_port)) + s.close() + status = "открыт" if result == 0 else "закрыт" + port_scan_info += f"Порт {test_port}: {status}\n" + + # Добавим информацию о MAC-адресе для WoL + mac_info = f"MAC: {SYNOLOGY_MAC}" if SYNOLOGY_MAC else "MAC-адрес не настроен" + + await message.edit_text( + f"❌ Synology NAS оффлайн\n\n" + f"Информация о сети:\n" + f"IP: {SYNOLOGY_HOST}\n" + f"{port_scan_info}\n" + f"{mac_info}\n\n" + f"Используйте /power для отправки Wake-on-LAN пакета", + parse_mode="HTML" + ) + except Exception as e: + await message.edit_text( + f"❌ Synology NAS оффлайн\n\n" + f"Ошибка при сканировании портов: {str(e)[:100]}...\n\n" + f"Используйте /power для отправки Wake-on-LAN пакета", + parse_mode="HTML" + ) + +async def power_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /power""" + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + is_online = synology_api.is_online() + + keyboard = [] + + # Кнопка включения + if not is_online: + keyboard.append([InlineKeyboardButton("🟢 Включить", callback_data="power_on")]) + else: + keyboard.append([InlineKeyboardButton("🟢 Включить (уже включено)", callback_data="power_on_no_op")]) + + # Кнопка выключения + if is_online: + keyboard.append([InlineKeyboardButton("🔴 Выключить", callback_data="power_off")]) + else: + keyboard.append([InlineKeyboardButton("🔴 Выключить (уже выключено)", callback_data="power_off_no_op")]) + + # Кнопка перезагрузки + if is_online: + keyboard.append([InlineKeyboardButton("🔄 Перезагрузить", callback_data="reboot")]) + else: + keyboard.append([InlineKeyboardButton("🔄 Перезагрузить (недоступно)", callback_data="reboot_no_op")]) + + # Кнопка отмены + keyboard.append([InlineKeyboardButton("❌ Отмена", callback_data="cancel")]) + + reply_markup = InlineKeyboardMarkup(keyboard) + + status_text = "✅ Онлайн" if is_online else "❌ Оффлайн" + + await update.message.reply_text( + f"Управление питанием Synology NAS\n\n" + f"Текущий статус: {status_text}\n\n" + f"Выберите действие:", + reply_markup=reply_markup, + parse_mode="HTML" + ) + +async def power_callback(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик callback-запросов для кнопок управления питанием""" + query = update.callback_query + await query.answer() + + user_id = query.from_user.id + if user_id not in ADMIN_USER_IDS: + return + + action = query.data + + if action == "cancel": + await query.edit_message_text("❌ Действие отменено") + return + + # Обработка неактивных кнопок + if action in ["power_on_no_op", "power_off_no_op", "reboot_no_op"]: + if action == "power_on_no_op": + await query.edit_message_text("ℹ️ Synology NAS уже включен") + elif action == "power_off_no_op": + await query.edit_message_text("ℹ️ Synology NAS уже выключен") + else: + await query.edit_message_text("ℹ️ Невозможно перезагрузить выключенное устройство") + return + + # Обработка основных действий + if action == "power_on": + await query.edit_message_text("⏳ Отправка сигнала включения Synology NAS...") + + if await context.application.create_task( + handle_power_on(query.message.chat_id, context) + ): + # Функция вернула True, успешное включение + pass + else: + # Функция вернула False, ошибка включения + await context.bot.send_message( + chat_id=query.message.chat_id, + text="❌ Ошибка при включении Synology NAS. Проверьте настройки Wake-on-LAN." + ) + + elif action == "power_off": + await query.edit_message_text("⏳ Отправка команды выключения Synology NAS...") + + try: + success = await handle_power_off(query.message.chat_id, context) + # Если handle_power_off уже отправил сообщение об успехе или ошибке, + # дополнительных сообщений не требуется + except Exception as e: + logger.error(f"Exception in power_off callback: {str(e)}") + await context.bot.send_message( + chat_id=query.message.chat_id, + text="❌ Ошибка при выключении Synology NAS. Проверьте соединение и учетные данные." + ) + + elif action == "reboot": + await query.edit_message_text("⏳ Отправка команды перезагрузки Synology NAS...") + + if await context.application.create_task( + handle_reboot(query.message.chat_id, context) + ): + # Функция вернула True, успешная перезагрузка + pass + else: + # Функция вернула False, ошибка перезагрузки + await context.bot.send_message( + chat_id=query.message.chat_id, + text="❌ Ошибка при перезагрузке Synology NAS. Проверьте соединение и учетные данные." + ) + +async def handle_power_on(chat_id: int, context: ContextTypes.DEFAULT_TYPE) -> bool: + """Асинхронная функция для включения NAS""" + try: + # Отправка запроса на включение + success = synology_api.power_on() + + if success: + await context.bot.send_message( + chat_id=chat_id, + text="✅ Synology NAS успешно включен и доступен" + ) + return True + else: + return False + except Exception as e: + logger.error(f"Error during power on: {str(e)}") + return False + +async def handle_power_off(chat_id: int, context: ContextTypes.DEFAULT_TYPE) -> bool: + """Асинхронная функция для выключения NAS""" + try: + # Проверка доступности NAS + if not synology_api.is_online(): + await context.bot.send_message( + chat_id=chat_id, + text="⚠️ Synology NAS не в сети. Невозможно отправить команду выключения." + ) + return False + + # Отправка запроса на выключение + success = synology_api.power_off() + + if success: + await context.bot.send_message( + chat_id=chat_id, + text="✅ Команда выключения успешно отправлена. Synology NAS выключается..." + ) + return True + else: + await context.bot.send_message( + chat_id=chat_id, + text="⚠️ Ошибка при отправке команды выключения. Пожалуйста, проверьте журналы для получения дополнительной информации." + ) + return False + except Exception as e: + error_msg = str(e) + logger.error(f"Error during power off: {error_msg}") + await context.bot.send_message( + chat_id=chat_id, + text=f"❌ Ошибка при выключении: {error_msg[:100]}..." + ) + return False + +async def handle_reboot(chat_id: int, context: ContextTypes.DEFAULT_TYPE) -> bool: + """Асинхронная функция для перезагрузки NAS""" + try: + # Отправка запроса на перезагрузку + success = synology_api.reboot_system() + + if success: + await context.bot.send_message( + chat_id=chat_id, + text="✅ Команда перезагрузки успешно отправлена. Synology NAS перезагружается..." + ) + + # Ждем некоторое время перед проверкой статуса + await context.bot.send_message( + chat_id=chat_id, + text="⏳ Ожидание перезагрузки системы..." + ) + + # Создаем задачу для ожидания загрузки + wait_successful = synology_api.wait_for_boot() + + if wait_successful: + await context.bot.send_message( + chat_id=chat_id, + text="✅ Synology NAS успешно перезагружен и снова онлайн" + ) + else: + await context.bot.send_message( + chat_id=chat_id, + text="⚠️ Превышено время ожидания перезагрузки. Проверьте статус вручную." + ) + + return True + else: + return False + except Exception as e: + logger.error(f"Error during reboot: {str(e)}") + return False diff --git a/src/handlers/extended_handlers.py b/src/handlers/extended_handlers.py new file mode 100644 index 0000000..ca60d52 --- /dev/null +++ b/src/handlers/extended_handlers.py @@ -0,0 +1,367 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Дополнительные обработчики команд для телеграм-бота +""" + +import logging +from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup +from telegram.ext import ContextTypes + +from src.config.config import ADMIN_USER_IDS +from src.api.synology import SynologyAPI + +logger = logging.getLogger(__name__) + +# Инициализация API Synology +synology_api = SynologyAPI() + +async def check_api_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /checkapi для диагностики проблем с API""" + if not update.message or not update.effective_user: + return + + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + message = await update.message.reply_text("⏳ Проверка доступных API Synology NAS...") + + from src.api.api_discovery import discover_available_apis + from src.config.config import ( + SYNOLOGY_HOST, + SYNOLOGY_PORT, + SYNOLOGY_SECURE, + SYNOLOGY_POWER_API, + SYNOLOGY_INFO_API, + SYNOLOGY_API_VERSION + ) + + # Формируем базовый URL + protocol = "https" if SYNOLOGY_SECURE else "http" + base_url = f"{protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi" + + # Получаем список доступных API + apis = discover_available_apis(base_url) + + if not apis: + await message.edit_text( + "❌ Не удалось получить список доступных API\n\n" + "Проверьте доступность NAS и сетевое подключение.", + parse_mode="HTML" + ) + return + + # Поиск API для управления питанием + power_apis = [name for name in apis.keys() if "power" in name.lower()] + system_info_apis = [name for name in apis.keys() if ("system" in name.lower() or "dsm" in name.lower()) and "info" in name.lower()] + reboot_apis = [name for name in apis.keys() if any(word in name.lower() for word in ["restart", "reboot", "power"])] + + # Формируем рекомендации + recommended_power_api = max(power_apis, key=lambda x: apis[x].get('maxVersion', 1)) if power_apis else "SYNO.Core.System" + recommended_info_api = max(system_info_apis, key=lambda x: apis[x].get('maxVersion', 1)) if system_info_apis else "SYNO.DSM.Info" + + # Формируем текст отчета + api_report = ( + f"✅ Найдено {len(apis)} доступных API\n\n" + f"API для управления питанием:\n" + f"{', '.join(power_apis[:5]) or 'Не найдены'}\n\n" + f"API для информации о системе:\n" + f"{', '.join(system_info_apis[:5]) or 'Не найдены'}\n\n" + f"API для перезагрузки:\n" + f"{', '.join(reboot_apis[:5]) or 'Не найдены'}\n\n" + f"Рекомендуемые настройки:\n" + f"Power API: {recommended_power_api}\n" + f"Info API: {recommended_info_api}\n\n" + f"Текущие настройки в конфигурации:\n" + f"SYNOLOGY_POWER_API = {SYNOLOGY_POWER_API}\n" + f"SYNOLOGY_INFO_API = {SYNOLOGY_INFO_API}\n" + f"SYNOLOGY_API_VERSION = {SYNOLOGY_API_VERSION}" + ) + + await message.edit_text(api_report, parse_mode="HTML") + +async def storage_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /storage""" + if not update.message or not update.effective_user: + return + + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + message = await update.message.reply_text("⏳ Получение информации о хранилище...") + + if not synology_api.is_online(): + await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о хранилище.", parse_mode="HTML") + return + + try: + storage_info = synology_api.get_storage_status() + + if not storage_info: + await message.edit_text("❌ Ошибка получения информации о хранилище\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML") + return + except Exception as e: + await message.edit_text(f"❌ Ошибка получения информации о хранилище\n\nПричина: {str(e)}", parse_mode="HTML") + return + + # Формируем сообщение о состоянии хранилища + summary = storage_info.get("summary", {}) + total_size_gb = summary.get("total_space_gb", 0) + total_used_gb = summary.get("used_space_gb", 0) + free_space_gb = summary.get("free_space_gb", 0) + usage_percent = summary.get("usage_percent", 0) + + reply_text = f"📊 Информация о хранилище Synology NAS\n\n" + reply_text += f"Общий размер: {total_size_gb:.2f} ГБ\n" + reply_text += f"Использовано: {total_used_gb:.2f} ГБ ({usage_percent:.1f}%)\n" + reply_text += f"Свободно: {free_space_gb:.2f} ГБ\n\n" + + # Добавляем информацию о томах + volumes = storage_info.get("volumes", []) + if volumes: + reply_text += "Тома:\n" + for volume in volumes: + name = volume.get("name", "Неизвестно") + status = volume.get("status", "Неизвестно") + size = volume.get("size", 0) + used_size = volume.get("used_size", 0) + size_gb = size / (1024**3) + used_gb = used_size / (1024**3) + percent = round((used_size / size) * 100, 1) if size > 0 else 0 + + reply_text += f"• {name} ({status})\n" + reply_text += f" └ {used_gb:.1f} ГБ из {size_gb:.1f} ГБ ({percent}%)\n" + + # Добавляем информацию о дисках + disks = storage_info.get("disks", []) + if disks: + reply_text += "\nДиски:\n" + for disk in disks: + name = disk.get("name", "Неизвестно") + model = disk.get("model", "Неизвестно") + status = disk.get("status", "Неизвестно") + temp = disk.get("temp", "?") + + reply_text += f"• {name} - {model}\n" + reply_text += f" └ Статус: {status}, Температура: {temp}°C\n" + + await message.edit_text(reply_text, parse_mode="HTML") + +async def shares_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /shares""" + if not update.message or not update.effective_user: + return + + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + message = await update.message.reply_text("⏳ Получение информации об общих папках...") + + if not synology_api.is_online(): + await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию об общих папках.", parse_mode="HTML") + return + + try: + shares = synology_api.get_shared_folders() + + if not shares: + await message.edit_text("❌ Ошибка получения информации об общих папках\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML") + return + except Exception as e: + await message.edit_text(f"❌ Ошибка получения информации об общих папках\n\nПричина: {str(e)}", parse_mode="HTML") + return + + # Формируем сообщение об общих папках + reply_text = f"📁 Общие папки Synology NAS\n\n" + + for share in shares: + name = share.get("name", "Неизвестно") + path = share.get("path", "Неизвестно") + desc = share.get("desc", "") + + reply_text += f"• {name}\n" + reply_text += f" └ Путь: {path}\n" + + if desc: + reply_text += f" └ Описание: {desc}\n" + + await message.edit_text(reply_text, parse_mode="HTML") + +async def system_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /system""" + if not update.message or not update.effective_user: + return + + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + message = await update.message.reply_text("⏳ Получение информации о системе...") + + if not synology_api.is_online(): + await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о системе.", parse_mode="HTML") + return + + try: + system_status = synology_api.get_system_status() + + if not system_status: + await message.edit_text("❌ Ошибка получения информации о системе", parse_mode="HTML") + return + + # Если получен статус с ошибкой + if system_status.get("status") == "error": + error_code = system_status.get("error_code", "неизвестно") + await message.edit_text(f"❌ Ошибка получения информации о системе\n\nКод ошибки API: {error_code}", parse_mode="HTML") + return + except Exception as e: + await message.edit_text(f"❌ Ошибка получения информации о системе\n\nПричина: {str(e)}", parse_mode="HTML") + return + + # Формируем сообщение о состоянии системы + model = system_status.get("model", "Неизвестно") + version = system_status.get("version", "Неизвестно") + serial = system_status.get("serial", "Неизвестно") + uptime_seconds = system_status.get("uptime", 0) + temperature = system_status.get("temperature", "?") + + # Преобразование времени работы в удобочитаемый формат + days, remainder = divmod(uptime_seconds, 86400) + hours, remainder = divmod(remainder, 3600) + minutes, seconds = divmod(remainder, 60) + uptime_str = f"{days}д {hours}ч {minutes}м {seconds}с" + + reply_text = f"🖥️ Информация о системе Synology NAS\n\n" + reply_text += f"Модель: {model}\n" + reply_text += f"Серийный номер: {serial}\n" + reply_text += f"Версия DSM: {version}\n" + reply_text += f"Время работы: {uptime_str}\n" + reply_text += f"Температура: {temperature}°C\n\n" + + # Добавляем информацию о CPU и памяти + memory = system_status.get("memory", {}) + total_memory_gb = memory.get("total_mb", 0) / 1024 + available_memory_gb = memory.get("available_mb", 0) / 1024 + memory_usage = memory.get("usage_percent", 0) + cpu_usage = system_status.get("cpu_usage", 0) + + reply_text += f"Загрузка CPU: {cpu_usage}%\n" + reply_text += f"Память: {total_memory_gb:.1f} ГБ (используется {memory_usage}%)\n" + reply_text += f"Доступно памяти: {available_memory_gb:.1f} ГБ\n\n" + + # Добавляем информацию о сетевых интерфейсах + network_info = system_status.get("network", []) + if network_info: + reply_text += "Сетевые интерфейсы:\n" + for interface in network_info: + device = interface.get("device", "Неизвестно") + ip = interface.get("ip", "Неизвестно") + mac = interface.get("mac", "Неизвестно") + + reply_text += f"• {device}\n" + reply_text += f" └ IP: {ip}, MAC: {mac}\n" + + await message.edit_text(reply_text, parse_mode="HTML") + +async def load_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /load""" + if not update.message or not update.effective_user: + return + + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + message = await update.message.reply_text("⏳ Получение информации о нагрузке системы...") + + if not synology_api.is_online(): + await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о нагрузке системы.", parse_mode="HTML") + return + + try: + system_load = synology_api.get_system_load() + + if not system_load: + await message.edit_text("❌ Ошибка получения информации о нагрузке системы\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML") + return + except Exception as e: + await message.edit_text(f"❌ Ошибка получения информации о нагрузке системы\n\nПричина: {str(e)}", parse_mode="HTML") + return + + # Формируем сообщение о нагрузке системы + cpu_load = system_load.get("cpu_load", 0) + memory = system_load.get("memory", {}) + memory_usage = memory.get("usage_percent", 0) + + reply_text = f"📈 Текущая нагрузка Synology NAS\n\n" + reply_text += f"Загрузка CPU: {cpu_load}%\n" + reply_text += f"Загрузка памяти: {memory_usage}%\n\n" + + # Добавляем информацию о сетевой активности + network = system_load.get("network", {}) + if network: + reply_text += "Сетевая активность:\n" + for device, stats in network.items(): + rx = int(stats.get("rx_bytes", 0)) / (1024**2) # МБ + tx = int(stats.get("tx_bytes", 0)) / (1024**2) # МБ + + reply_text += f"• {device}\n" + reply_text += f" └ Получено: {rx:.1f} МБ, Отправлено: {tx:.1f} МБ\n" + + await message.edit_text(reply_text, parse_mode="HTML") + +async def security_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Обработчик команды /security""" + if not update.message or not update.effective_user: + return + + user_id = update.effective_user.id + + if user_id not in ADMIN_USER_IDS: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + message = await update.message.reply_text("⏳ Получение информации о безопасности...") + + if not synology_api.is_online(): + await message.edit_text("❌ Synology NAS оффлайн\n\nНевозможно получить информацию о безопасности.", parse_mode="HTML") + return + + try: + security_info = synology_api.get_security_status() + + if not security_info.get("success", False): + await message.edit_text("❌ Ошибка получения информации о безопасности\n\nВозможно, API не поддерживает эту функцию или требуются дополнительные права доступа.", parse_mode="HTML") + return + except Exception as e: + await message.edit_text(f"❌ Ошибка получения информации о безопасности\n\nПричина: {str(e)}", parse_mode="HTML") + return + + # Формируем сообщение о безопасности + status = security_info.get("status", "unknown") + is_secure = security_info.get("is_secure", False) + last_check = security_info.get("last_check", "Неизвестно") + + status_emoji = "✅" if is_secure else "⚠️" + status_text = "Безопасно" if is_secure else "Требуется внимание" + + reply_text = f"🔐 Статус безопасности Synology NAS\n\n" + reply_text += f"Статус: {status_emoji} {status_text}\n" + reply_text += f"Подробности: {status}\n" + reply_text += f"Последняя проверка: {last_check}\n" + + await message.edit_text(reply_text, parse_mode="HTML") diff --git a/src/handlers/help_handlers.py b/src/handlers/help_handlers.py new file mode 100644 index 0000000..80fa480 --- /dev/null +++ b/src/handlers/help_handlers.py @@ -0,0 +1,111 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Модуль с функциями для генерации справочных сообщений о командах бота +""" + +import logging +from telegram import Update +from telegram.ext import ContextTypes +from telegram.constants import ParseMode + +from src.config.config import ADMIN_USER_IDS + +logger = logging.getLogger(__name__) + +async def help_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """ + Обработчик команды /help - выводит справку по всем доступным командам + """ + user_id = update.effective_user.id if update.effective_user else "Unknown" + username = update.effective_user.username if update.effective_user else "Unknown" + + logger.info(f"User {user_id} (@{username}) requested help") + + # Проверка прав доступа + if user_id not in ADMIN_USER_IDS and isinstance(user_id, int): + if update.message: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + help_text = ( + "🤖 Synology Power Control Bot\n\n" + "БАЗОВЫЕ КОМАНДЫ:\n" + "/status - Проверить состояние NAS\n" + "/power - Управление питанием NAS (меню)\n" + "/reboot - Перезагрузка NAS (с подтверждением)\n" + "/sleep - Перевод NAS в спящий режим (с подтверждением)\n\n" + + "ИНФОРМАЦИОННЫЕ КОМАНДЫ:\n" + "/system - Информация о системе\n" + "/storage - Состояние хранилища\n" + "/shares - Список общих папок\n" + "/load - Нагрузка на систему\n" + "/security - Информация о безопасности\n" + "/temperature - Температура устройства\n" + "/processes - Список активных процессов\n" + "/network - Сетевая информация\n\n" + + "РАСШИРЕННЫЕ КОМАНДЫ:\n" + "/schedule - Расписание питания\n" + "/browse - Просмотр файлов\n" + "/search <запрос> - Поиск файлов\n" + "/updates - Проверка обновлений\n" + "/backup - Статус резервного копирования\n" + "/quota - Квоты пользователей\n\n" + + "БЫСТРЫЕ КОМАНДЫ:\n" + "/quickreboot - Быстрая перезагрузка\n" + "/wakeup - Пробуждение NAS (WOL)\n\n" + + "СЛУЖЕБНЫЕ КОМАНДЫ:\n" + "/checkapi - Проверка API\n" + "/help - Эта справка\n" + ) + + if update.message: + await update.message.reply_text(help_text, parse_mode=ParseMode.HTML) + elif update.callback_query: + await update.callback_query.answer() + if update.callback_query.message: + try: + await update.callback_query.message.edit_text(help_text, parse_mode=ParseMode.HTML) + except Exception as e: + logger.error(f"Failed to edit message: {e}") + # Отправляем новое сообщение в текущий чат + await context.bot.send_message( + chat_id=update.callback_query.message.chat_id, + text=help_text, + parse_mode=ParseMode.HTML + ) + +async def start_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """ + Обработчик команды /start - приветствие и краткая информация + """ + user_id = update.effective_user.id if update.effective_user else "Unknown" + username = update.effective_user.username if update.effective_user else "Unknown" + + logger.info(f"User {user_id} (@{username}) started the bot") + + # Проверка прав доступа + if user_id not in ADMIN_USER_IDS and isinstance(user_id, int): + if update.message: + await update.message.reply_text("У вас нет доступа к этому боту.") + return + + welcome_text = ( + "👋 Добро пожаловать в Synology Power Control Bot!\n\n" + "С помощью этого бота вы можете управлять питанием вашего Synology NAS " + "и получать различную информацию о его состоянии.\n\n" + "Для просмотра списка доступных команд используйте /help\n\n" + "Базовые команды:\n" + "• /status - Проверить состояние NAS\n" + "• /power - Управление питанием\n" + "• /system - Информация о системе\n" + "• /storage - Состояние хранилища" + ) + + if update.message: + await update.message.reply_text(welcome_text, parse_mode=ParseMode.HTML) diff --git a/src/healthcheck.py b/src/healthcheck.py new file mode 100644 index 0000000..0ec9b8e --- /dev/null +++ b/src/healthcheck.py @@ -0,0 +1,71 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Простой HTTP-сервер для healthcheck Docker-контейнера. +Запускается параллельно с основным ботом и отвечает на запросы /health. +""" + +import os +import http.server +import socketserver +import threading +import logging +from time import sleep + +# Настройка логирования +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' +) +logger = logging.getLogger('healthcheck') + +# Порт для healthcheck +PORT = int(os.getenv('HEALTHCHECK_PORT', 8080)) + +class HealthCheckHandler(http.server.SimpleHTTPRequestHandler): + """Обработчик для healthcheck запросов""" + + def do_GET(self): + """Обработка GET-запросов""" + if self.path == '/health': + self.send_response(200) + self.send_header('Content-Type', 'text/plain') + self.end_headers() + self.wfile.write(b"OK") + else: + self.send_response(404) + self.send_header('Content-Type', 'text/plain') + self.end_headers() + self.wfile.write(b"Not Found") + + def log_message(self, format, *args): + """Переопределяем метод логирования для вывода в наш logger""" + logger.info("%s - - [%s] %s", self.address_string(), self.log_date_time_string(), format % args) + +def run_health_server(): + """Запуск HTTP-сервера для healthcheck""" + with socketserver.TCPServer(("", PORT), HealthCheckHandler) as httpd: + logger.info(f"Starting healthcheck server on port {PORT}") + httpd.serve_forever() + +def start_health_server(): + """Запуск сервера в отдельном потоке""" + # Даем основному приложению время на инициализацию + sleep(5) + + # Запускаем HTTP-сервер в отдельном потоке + thread = threading.Thread(target=run_health_server, daemon=True) + thread.start() + logger.info("Healthcheck server thread started") + return thread + +if __name__ == "__main__": + # Этот код выполняется только если файл запускается напрямую, а не импортируется + thread = start_health_server() + try: + # Держим основной поток живым + while True: + sleep(60) + except KeyboardInterrupt: + logger.info("Healthcheck server shutting down") diff --git a/src/utils/logger.py b/src/utils/logger.py new file mode 100644 index 0000000..3ecbddd --- /dev/null +++ b/src/utils/logger.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Модуль для настройки логирования +""" + +import os +import logging +from logging.handlers import RotatingFileHandler + +def setup_logging(log_level=logging.INFO) -> None: + """ + Настройка логирования с ротацией файлов + + Args: + log_level: Уровень логирования (по умолчанию INFO) + """ + # Создание директории для логов, если её нет + logs_dir = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), "logs") + os.makedirs(logs_dir, exist_ok=True) + + # Путь к файлу лога + log_file = os.path.join(logs_dir, "synology_bot.log") + + # Базовая настройка логгера + logging.basicConfig( + level=log_level, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', + handlers=[ + RotatingFileHandler( + log_file, + maxBytes=10485760, # 10MB + backupCount=3 + ), + logging.StreamHandler() + ] + ) + + # Снижаем уровень логирования для некоторых модулей + logging.getLogger("httpx").setLevel(logging.WARNING) + logging.getLogger("telegram").setLevel(logging.WARNING) + + # Логирование старта системы + logging.info("Logging system initialized") diff --git a/test_api_headers.py b/test_api_headers.py new file mode 100644 index 0000000..ad073d6 --- /dev/null +++ b/test_api_headers.py @@ -0,0 +1,391 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Тестовый скрипт для диагностики проблемы с использованием специальных заголовков +""" + +import requests +import logging +import json +import sys +import os +import urllib3 +import time +import socket +from requests.adapters import HTTPAdapter +from urllib3.util import Retry + +# Отключение предупреждений о небезопасных SSL-соединениях +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + +# Настройка логирования +logging.basicConfig( + level=logging.DEBUG, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' +) +logger = logging.getLogger(__name__) + +# Тестовые учетные данные (для примера) +SYNOLOGY_HOST = "192.168.0.102" +SYNOLOGY_PORT = 5000 +SYNOLOGY_USERNAME = "superadmin" +SYNOLOGY_PASSWORD = "Cl0ud_1985!" +SYNOLOGY_SECURE = False +SYNOLOGY_TIMEOUT = 10 + +def test_api_with_headers(): + """Тестирование API с использованием специальных заголовков для решения проблемы 119""" + + # Создаем сессию + session = requests.Session() + session.verify = False # Отключаем проверку SSL + + # Настройки повторных попыток + retry_strategy = Retry( + total=3, + status_forcelist=[429, 500, 502, 503, 504], + allowed_methods=["GET", "POST"], + backoff_factor=1.0 + ) + adapter = HTTPAdapter(max_retries=retry_strategy) + session.mount("http://", adapter) + session.mount("https://", adapter) + + # Формируем базовый URL + protocol = "https" if SYNOLOGY_SECURE else "http" + base_url = f"{protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi" + + logger.info(f"Тестирование API с заголовками для {base_url}") + + # Тест 1: Получение SID с настройкой cookie и user-agent + logger.info("Тест 1: Авторизация с настройкой cookie и user-agent") + + # Добавление пользовательских заголовков + custom_headers = { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36', + 'Accept': 'application/json, text/javascript, */*; q=0.01', + 'Accept-Language': 'ru-RU,ru;q=0.9,en-US;q=0.8,en;q=0.7', + 'X-Requested-With': 'XMLHttpRequest', + 'Connection': 'keep-alive', + 'Referer': f'{protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/' + } + + session.headers.update(custom_headers) + + try: + # Определяем путь для авторизации + auth_info_url = f"{base_url}/entry.cgi" + auth_info_params = { + "api": "SYNO.API.Info", + "version": "1", + "method": "query", + "query": "SYNO.API.Auth" + } + + auth_info_response = session.get(auth_info_url, params=auth_info_params) + auth_info_data = auth_info_response.json() + + if auth_info_data.get("success"): + auth_info = auth_info_data.get("data", {}).get("SYNO.API.Auth", {}) + auth_path = auth_info.get("path", "auth.cgi") + auth_max_version = auth_info.get("maxVersion", 6) + + logger.info(f"API авторизации: путь={auth_path}, макс. версия={auth_max_version}") + + # Используем версию 3 вместо 6 - тестирование на возможное решение проблемы + auth_version = min(3, auth_max_version) # Пробуем более старую версию API + + # Выполняем авторизацию + auth_url = f"{base_url}/{auth_path}" + auth_params = { + "api": "SYNO.API.Auth", + "version": str(auth_version), + "method": "login", + "account": SYNOLOGY_USERNAME, + "passwd": SYNOLOGY_PASSWORD, + "session": "DirectHeaderTest", + "format": "cookie" + } + + if auth_version >= 6: + auth_params["enable_syno_token"] = "yes" + + logger.info(f"Авторизация с использованием SYNO.API.Auth v{auth_version}") + auth_response = session.get(auth_url, params=auth_params) + auth_data = auth_response.json() + + if auth_data.get("success"): + sid = auth_data.get("data", {}).get("sid") + logger.info(f"Авторизация успешна! SID: {sid[:10]}...") + + # Теперь проверим, работает ли получение информации о системе + # с настройкой cookie и заголовков + + # Сначала настроим куки для сохранения SID + cookies = { + 'id': sid, + 'sid': sid + } + session.cookies.update(cookies) + + # Определяем путь для SYNO.DSM.Info + info_info_url = f"{base_url}/entry.cgi" + info_info_params = { + "api": "SYNO.API.Info", + "version": "1", + "method": "query", + "query": "SYNO.DSM.Info" + } + + info_info_response = session.get(info_info_url, params=info_info_params) + info_info_data = info_info_response.json() + + if info_info_data.get("success"): + info_info = info_info_data.get("data", {}).get("SYNO.DSM.Info", {}) + info_path = info_info.get("path", "entry.cgi") + info_max_version = info_info.get("maxVersion", 1) + info_min_version = info_info.get("minVersion", 1) + + logger.info(f"API SYNO.DSM.Info: путь={info_path}, версия={info_min_version}-{info_max_version}") + + # Используем правильную версию API + info_version = min(2, info_max_version) + + # Делаем запрос для получения информации о системе + # с использованием sid как параметр запроса + logger.info("Тест 2: Получение информации о системе с SID как параметром") + info_url = f"{base_url}/{info_path}" + info_params = { + "api": "SYNO.DSM.Info", + "version": str(info_version), + "method": "getinfo", + "_sid": sid + } + + logger.info(f"Запрос информации с использованием SYNO.DSM.Info v{info_version}") + info_response = session.get(info_url, params=info_params) + info_data = info_response.json() + + if info_data.get("success"): + logger.info("Успешно получена информация о системе!") + logger.info(f"Данные: {json.dumps(info_data.get('data', {}), indent=2)}") + else: + error_code = info_data.get("error", {}).get("code", -1) + logger.error(f"Не удалось получить информацию о системе. Ошибка: {error_code}") + + # Пробуем альтернативный способ + logger.info("Тест 3: Попытка получить базовую информацию через SYNO.Core.System") + + # Определяем путь для SYNO.Core.System + system_info_url = f"{base_url}/entry.cgi" + system_info_params = { + "api": "SYNO.API.Info", + "version": "1", + "method": "query", + "query": "SYNO.Core.System" + } + + system_info_response = session.get(system_info_url, params=system_info_params) + system_info_data = system_info_response.json() + + if system_info_data.get("success"): + system_info = system_info_data.get("data", {}).get("SYNO.Core.System", {}) + system_path = system_info.get("path", "entry.cgi") + system_max_version = system_info.get("maxVersion", 1) + system_min_version = system_info.get("minVersion", 1) + + logger.info(f"API SYNO.Core.System: путь={system_path}, версия={system_min_version}-{system_max_version}") + + # Используем правильную версию API + system_version = 1 + + # Пробуем альтернативную стратегию с X-SYNO-TOKEN + # Некоторые API Synology требуют специальный токен в заголовках + token = auth_data.get("data", {}).get("synotoken") + if token: + session.headers.update({'X-SYNO-TOKEN': token}) + logger.info(f"Добавлен X-SYNO-TOKEN: {token}") + + # Делаем запрос для получения информации о системе + system_url = f"{base_url}/{system_path}" + system_params = { + "api": "SYNO.Core.System", + "version": str(system_version), + "method": "info", + "_sid": sid + } + + logger.info(f"Запрос информации с использованием SYNO.Core.System v{system_version}") + system_response = session.get(system_url, params=system_params) + system_data = system_response.json() + + if system_data.get("success"): + logger.info("Успешно получена информация о системе через SYNO.Core.System!") + logger.info(f"Данные: {json.dumps(system_data.get('data', {}), indent=2)}") + else: + error_code = system_data.get("error", {}).get("code", -1) + logger.error(f"Не удалось получить информацию через SYNO.Core.System. Ошибка: {error_code}") + + # Пробуем другие методы для диагностики + logger.info("Тест 4: Попытка использовать различные методы и заголовки") + + # Пробуем создать полностью новую сессию + new_session = requests.Session() + new_session.verify = False + new_session.headers.update(custom_headers) + + # Пробуем версию 1 для авторизации + auth_version = 1 + auth_params["version"] = str(auth_version) + + auth_response = new_session.get(auth_url, params=auth_params) + auth_data = auth_response.json() + + if auth_data.get("success"): + sid = auth_data.get("data", {}).get("sid") + logger.info(f"Новая авторизация (v{auth_version}) успешна! SID: {sid[:10]}...") + + # Добавляем SID как куки + cookies = { + 'id': sid, + 'sid': sid + } + new_session.cookies.update(cookies) + + # Пробуем другой подход: разделение запросов во времени + logger.info("Тест 5: Разделение запросов во времени") + + # Даем некоторое время для инициализации сессии на сервере + time.sleep(2) + + # Пробуем получить список файлов + filestation_info_url = f"{base_url}/entry.cgi" + filestation_info_params = { + "api": "SYNO.API.Info", + "version": "1", + "method": "query", + "query": "SYNO.FileStation.List" + } + + filestation_info_response = new_session.get(filestation_info_url, params=filestation_info_params) + filestation_info_data = filestation_info_response.json() + + if filestation_info_data.get("success"): + filestation_info = filestation_info_data.get("data", {}).get("SYNO.FileStation.List", {}) + filestation_path = filestation_info.get("path", "entry.cgi") + filestation_max_version = filestation_info.get("maxVersion", 1) + + logger.info(f"API SYNO.FileStation.List: путь={filestation_path}, макс. версия={filestation_max_version}") + + # Используем правильную версию API + filestation_version = min(2, filestation_max_version) + + # Делаем запрос для получения списка общих папок + filestation_url = f"{base_url}/{filestation_path}" + filestation_params = { + "api": "SYNO.FileStation.List", + "version": str(filestation_version), + "method": "list_share", + "_sid": sid + } + + logger.info(f"Запрос списка общих папок с использованием SYNO.FileStation.List v{filestation_version}") + filestation_response = new_session.get(filestation_url, params=filestation_params) + filestation_data = filestation_response.json() + + if filestation_data.get("success"): + logger.info("Успешно получен список общих папок!") + shares = filestation_data.get("data", {}).get("shares", []) + logger.info(f"Общие папки: {json.dumps(shares, indent=2)[:200]}...") + else: + error_code = filestation_data.get("error", {}).get("code", -1) + logger.error(f"Не удалось получить список общих папок. Ошибка: {error_code}") + else: + error_code = auth_data.get("error", {}).get("code", -1) + logger.error(f"Новая авторизация не удалась! Код ошибки: {error_code}") + else: + logger.error("Не удалось получить информацию о SYNO.Core.System API") + else: + logger.error("Не удалось получить информацию о SYNO.DSM.Info API") + else: + error_code = auth_data.get("error", {}).get("code", -1) + logger.error(f"Авторизация не удалась! Код ошибки: {error_code}") + else: + logger.error("Не удалось получить информацию об API авторизации") + + except Exception as e: + logger.error(f"Произошла ошибка: {str(e)}") + + # Тест 6: Проверка сетевой доступности + logger.info("Тест 6: Проверка сетевой доступности") + + try: + # Проверка базового TCP-соединения + socket_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + socket_obj.settimeout(SYNOLOGY_TIMEOUT) + result = socket_obj.connect_ex((SYNOLOGY_HOST, SYNOLOGY_PORT)) + socket_obj.close() + + if result == 0: + logger.info("TCP-соединение успешно установлено") + else: + logger.error(f"Не удалось установить TCP-соединение, код ошибки: {result}") + except Exception as e: + logger.error(f"Ошибка при проверке TCP-соединения: {str(e)}") + + # Тест 7: Запрос без аутентификации для проверки доступности API + logger.info("Тест 7: Запрос без аутентификации для проверки доступности API") + + try: + # Создаем новую сессию без аутентификации + simple_session = requests.Session() + simple_session.verify = False + + # Запрос к SYNO.API.Info не требует аутентификации + api_info_url = f"{base_url}/entry.cgi" + api_info_params = { + "api": "SYNO.API.Info", + "version": "1", + "method": "query", + "query": "all" + } + + logger.info("Запрос информации о всех API без аутентификации") + api_info_response = simple_session.get(api_info_url, params=api_info_params) + + if api_info_response.status_code == 200: + logger.info("API доступно без аутентификации") + api_info_data = api_info_response.json() + + if api_info_data.get("success"): + logger.info("Успешно получена информация о всех API") + api_count = len(api_info_data.get("data", {})) + logger.info(f"Количество доступных API: {api_count}") + + # Поиск API для управления питанием + power_apis = [] + for api_name, api_info in api_info_data.get("data", {}).items(): + if "power" in api_name.lower() or "reboot" in api_name.lower() or "shutdown" in api_name.lower(): + power_apis.append(f"{api_name}: {api_info}") + + logger.info(f"Найдены API для управления питанием: {power_apis}") + + # Поиск API для получения информации о системе + info_apis = [] + for api_name, api_info in api_info_data.get("data", {}).items(): + if "info" in api_name.lower() or "system" in api_name.lower() or "status" in api_name.lower(): + info_apis.append(f"{api_name}: {api_info}") + + logger.info(f"Найдены API для информации о системе: {info_apis[:5]} и еще {len(info_apis)-5}") + else: + error_code = api_info_data.get("error", {}).get("code", -1) + logger.error(f"Запрос к API без аутентификации не удался! Код ошибки: {error_code}") + else: + logger.error(f"API не доступно без аутентификации. HTTP статус: {api_info_response.status_code}") + except Exception as e: + logger.error(f"Ошибка при проверке доступности API: {str(e)}") + +if __name__ == "__main__": + logger.info("Запуск теста API с заголовками") + test_api_with_headers() diff --git a/test_reboot.py b/test_reboot.py new file mode 100644 index 0000000..90e896c --- /dev/null +++ b/test_reboot.py @@ -0,0 +1,73 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Скрипт для тестирования функции перезагрузки Synology NAS +""" + +import os +import sys +import logging +from pathlib import Path + +# Добавляем путь проекта в sys.path +project_dir = str(Path(__file__).resolve().parent) +if project_dir not in sys.path: + sys.path.insert(0, project_dir) + +from src.api.synology import SynologyAPI +from src.config.config import SYNOLOGY_POWER_API, SYNOLOGY_INFO_API, SYNOLOGY_API_VERSION + +# Настройка логирования +logging.basicConfig( + level=logging.DEBUG, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' +) +logger = logging.getLogger(__name__) + +def main(): + """Основная функция для тестирования перезагрузки""" + logger.info("Тестирование функции перезагрузки Synology NAS") + + logger.info(f"Используемые API: POWER={SYNOLOGY_POWER_API}, INFO={SYNOLOGY_INFO_API}, VERSION={SYNOLOGY_API_VERSION}") + + # Инициализация API + synology = SynologyAPI() + + # Проверка онлайн статуса + logger.info("Проверка онлайн статуса...") + is_online = synology.is_online(force_check=True) + logger.info(f"Статус: {'Онлайн' if is_online else 'Оффлайн'}") + + if not is_online: + logger.error("NAS недоступен. Невозможно выполнить перезагрузку.") + return + + # Вывод информации о системе + logger.info("Получение информации о системе...") + system_info = synology.get_system_status() + + if system_info.get("status") == "error": + logger.error(f"Ошибка получения информации о системе: {system_info.get('error')}") + else: + logger.info(f"Информация о системе: {system_info}") + + # Запрос на подтверждение действия + confirm = input("Вы действительно хотите перезагрузить Synology NAS? (y/n): ") + if confirm.lower() != 'y': + logger.info("Операция отменена пользователем.") + return + + # Выполнение перезагрузки + logger.info("Выполнение перезагрузки...") + try: + result = synology.reboot_system() + if result: + logger.info("Перезагрузка выполнена успешно.") + else: + logger.error("Не удалось выполнить перезагрузку.") + except Exception as e: + logger.error(f"Ошибка при выполнении перезагрузки: {str(e)}") + +if __name__ == "__main__": + main() diff --git a/test_system_info.py b/test_system_info.py new file mode 100644 index 0000000..95a9bf3 --- /dev/null +++ b/test_system_info.py @@ -0,0 +1,87 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Скрипт для тестирования получения информации о системе Synology NAS +""" + +import os +import sys +import logging +from pathlib import Path + +# Добавляем путь проекта в sys.path +project_dir = str(Path(__file__).resolve().parent) +if project_dir not in sys.path: + sys.path.insert(0, project_dir) + +from src.api.synology import SynologyAPI +from src.config.config import SYNOLOGY_POWER_API, SYNOLOGY_INFO_API, SYNOLOGY_API_VERSION + +# Настройка логирования +logging.basicConfig( + level=logging.DEBUG, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' +) +logger = logging.getLogger(__name__) + +def main(): + """Основная функция для тестирования получения информации о системе""" + logger.info("Тестирование получения информации о системе Synology NAS") + + logger.info(f"Используемые API: POWER={SYNOLOGY_POWER_API}, INFO={SYNOLOGY_INFO_API}, VERSION={SYNOLOGY_API_VERSION}") + + # Инициализация API + synology = SynologyAPI() + + # Проверка онлайн статуса + logger.info("Проверка онлайн статуса...") + is_online = synology.is_online(force_check=True) + logger.info(f"Статус: {'Онлайн' if is_online else 'Оффлайн'}") + + if not is_online: + logger.error("NAS недоступен. Невозможно получить информацию о системе.") + return + + # Получение списка доступных API + logger.info("Получение списка доступных API...") + from src.api.api_discovery import discover_available_apis + from src.config.config import SYNOLOGY_HOST, SYNOLOGY_PORT, SYNOLOGY_SECURE + + protocol = "https" if SYNOLOGY_SECURE else "http" + base_url = f"{protocol}://{SYNOLOGY_HOST}:{SYNOLOGY_PORT}/webapi" + + apis = discover_available_apis(base_url) + if apis: + logger.info(f"Найдено {len(apis)} API") + + # Фильтрация API для управления питанием + power_apis = [name for name in apis.keys() if "power" in name.lower()] + system_apis = [name for name in apis.keys() if "system" in name.lower() or "dsm.info" in name.lower()] + + logger.info(f"API для управления питанием: {power_apis}") + logger.info(f"API для системной информации: {system_apis}") + + # Проверка конкретных API + for api_name in [SYNOLOGY_POWER_API, SYNOLOGY_INFO_API]: + if api_name in apis: + api_info = apis[api_name] + logger.info(f"API {api_name}: versions={api_info.get('minVersion')}-{api_info.get('maxVersion')}, path={api_info.get('path')}") + else: + logger.warning(f"API {api_name} не найден в списке доступных API") + else: + logger.error("Не удалось получить список доступных API") + + # Вывод информации о системе + logger.info("Получение информации о системе...") + system_info = synology.get_system_status() + + if system_info.get("status") == "error": + logger.error(f"Ошибка получения информации о системе: {system_info.get('error')}") + else: + logger.info(f"Информация о системе: {system_info}") + + logger.info("Тестирование завершено.") + +if __name__ == "__main__": + main() diff --git a/ОТЧЕТ_ПО_API.md b/ОТЧЕТ_ПО_API.md new file mode 100644 index 0000000..68eba65 --- /dev/null +++ b/ОТЧЕТ_ПО_API.md @@ -0,0 +1,185 @@ +# Отчет по доступным API и возможностям управления Synology NAS + +## 1. Доступные API Synology NAS + +На вашем Synology NAS (модель DS223j с DSM 7.2.2) обнаружено **572** различных API. Ниже перечислены ключевые категории API, которые можно использовать для расширения функциональности бота: + +### 1.1. API для управления питанием +- **SYNO.Core.Hardware.PowerRecovery** (v1) - основной API для управления питанием +- **SYNO.Core.Hardware.PowerSchedule** (v1) - API для настройки расписания включения/выключения +- **SYNO.Core.Hardware.NeedReboot** (v1) - API для перезагрузки системы + +### 1.2. API для системной информации +- **SYNO.DSM.Info** (v2) - основной API для получения общей информации о системе +- **SYNO.Core.System** (v1) - API для получения расширенной системной информации +- **SYNO.Core.System.Status** (v1) - API для получения статуса системы +- **SYNO.Core.System.Utilization** (v1) - API для получения сведений о загрузке системы + +### 1.3. API для хранилища и файлов +- **SYNO.Storage.CGI.Storage** - информация о хранилище и дисках +- **SYNO.FileStation.List** - получение списка файлов и папок + +### 1.4. API для мониторинга +- **SYNO.Core.System.Process** - информация о запущенных процессах +- **SYNO.Core.System.SystemHealth** - состояние здоровья системы + +## 2. Расширенные команды управления NAS + +Бот уже поддерживает следующие базовые команды: +- `/start` - Начало работы с ботом +- `/status` - Проверка текущего статуса NAS +- `/power` - Управление питанием NAS +- `/help` - Вывод справки + +Также реализованы расширенные команды: +- `/system` - Подробная информация о системе +- `/storage` - Информация о хранилище и дисках +- `/shares` - Список общих папок +- `/load` - Текущая нагрузка на систему +- `/security` - Статус безопасности системы +- `/checkapi` - Проверка доступных API Synology + +### 2.1. Рекомендуемые дополнительные команды + +На основе анализа доступных API предлагаю добавить следующие команды: + +#### 2.1.1. Управление питанием и расписанием +- `/schedule` - Управление расписанием включения/выключения NAS +- `/wakeup` - Немедленное включение NAS через Wake-on-LAN +- `/quickreboot` - Быстрая перезагрузка без запроса подтверждения + +#### 2.1.2. Мониторинг системы +- `/processes` - Просмотр активных процессов и их загрузки +- `/network` - Детальная информация о сетевых подключениях +- `/temperature` - Мониторинг температуры системы и дисков +- `/updates` - Проверка доступных обновлений для DSM + +#### 2.1.3. Управление файлами +- `/browse [путь]` - Просмотр файлов в указанной директории +- `/search [шаблон]` - Поиск файлов по шаблону +- `/quota` - Просмотр информации о квотах пользователей + +#### 2.1.4. Резервное копирование +- `/backup` - Управление задачами резервного копирования +- `/backupstatus` - Проверка статуса резервного копирования + +## 3. Возможности использования API + +### 3.1. Оптимизация текущего кода +- Обнаружено, что для успешного взаимодействия с API необходимы специальные HTTP-заголовки, имитирующие браузер +- API версии 3 показывает лучшую стабильность для базовых операций, чем версия 6 +- Для аутентификации рекомендуется использовать куки и форматы, совместимые с веб-интерфейсом + +### 3.2. Рекомендуемые настройки API +- **SYNOLOGY_POWER_API = SYNO.Core.Hardware.PowerRecovery** +- **SYNOLOGY_INFO_API = SYNO.DSM.Info** +- **SYNOLOGY_API_VERSION = 2** (вместо текущего значения 6) + +### 3.3. Новые функциональные возможности + +#### 3.3.1. Мониторинг производительности +```python +def get_performance_stats(): + """Получение детальной статистики производительности""" + result = api._make_api_request("SYNO.Core.System.Utilization", "get", version=1) + return result +``` + +#### 3.3.2. Управление сервисами +```python +def manage_services(service_name, action="status"): + """Управление системными сервисами (start/stop/restart/status)""" + result = api._make_api_request("SYNO.Core.Service", action, version=1, + params={"service": service_name}) + return result +``` + +#### 3.3.3. Просмотр журналов +```python +def get_system_logs(limit=20): + """Получение системных журналов""" + result = api._make_api_request("SYNO.Core.SyslogClient.Log", "list", version=1, + params={"limit": limit}) + return result +``` + +#### 3.3.4. Настройка расписания питания +```python +def set_power_schedule(days, time, action="boot"): + """Настройка расписания питания (boot/shutdown)""" + result = api._make_api_request("SYNO.Core.Hardware.PowerSchedule", "set", version=1, + params={"day": days, "time": time, "action": action}) + return result +``` + +## 4. Решение текущих проблем API + +### 4.1. Проблема с получением информации о хранилище +Текущий код использует заглушку для `get_storage_status()`. Рекомендуемая реализация: + +```python +def get_storage_status(self) -> Dict[str, Any]: + """Получение информации о хранилище""" + # Попробуем получить информацию о дисках + disk_result = self._make_api_request("SYNO.Storage.CGI.Storage", "load_info", version=1) + + if not disk_result: + # Пробуем альтернативный API + disk_result = self._make_api_request("SYNO.Core.Storage", "info", version=1) + + # Собираем результат + volumes = disk_result.get("volumes", []) + disks = disk_result.get("disks", []) + + total_size = sum(vol.get("size", {}).get("total", 0) for vol in volumes) + total_used = sum(vol.get("size", {}).get("used", 0) for vol in volumes) + + return { + "volumes": volumes, + "disks": disks, + "total_size": total_size, + "total_used": total_used + } +``` + +### 4.2. Проблема с получением списка общих папок + +```python +def get_shared_folders(self) -> List[Dict[str, Any]]: + """Получение списка общих папок""" + result = self._make_api_request("SYNO.FileStation.List", "list_share", version=2) + + if not result: + return [] + + return result.get("shares", []) +``` + +### 4.3. Проблема с получением информации о нагрузке + +```python +def get_system_load(self) -> Dict[str, Any]: + """Получение информации о нагрузке системы""" + result = self._make_api_request("SYNO.Core.System.Utilization", "get", version=1) + + if not result: + return {} + + return { + "cpu_load": result.get("cpu", {}).get("user", 0) + result.get("cpu", {}).get("system", 0), + "memory": result.get("memory", {}), + "network": result.get("network", {}) + } +``` + +## 5. Заключение + +Synology NAS предоставляет обширный набор API, которые можно использовать для создания полноценного Telegram-бота для управления и мониторинга. Основные рекомендации: + +1. Обновить версию API до более стабильной (v2-3 вместо v6) +2. Добавить специальные HTTP-заголовки для имитации веб-браузера +3. Использовать куки для сохранения сессии +4. Реализовать новые функции управления на основе доступных API +5. Добавить обработку ошибок для нестабильных API (особенно хранилища и общих папок) + +Дополнительно рекомендуется реализовать функции автоматического мониторинга и уведомления о важных событиях, таких как высокая температура, заканчивающееся место на дисках или необходимость обновления системы.