Initial commit
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,65 @@
|
||||
# Broadlink Manager Pro
|
||||
|
||||
Менеджер для управления Broadlink RM устройствами с графическим интерфейсом и поддержкой командной строки.
|
||||
|
||||
## Описание
|
||||
|
||||
Broadlink Manager Pro - это приложение для управления ИК-пультами Broadlink (RM Mini 3, RM4 Pro и др.). Позволяет обучать, сохранять и отправлять ИК-команды через удобный GUI или командную строку.
|
||||
|
||||
## Возможности
|
||||
|
||||
- 🔍 Автоматический поиск устройств в сети
|
||||
- 📚 Обучение и сохранение ИК-команд
|
||||
- 📤 Отправка команд на устройства
|
||||
- ⌨️ Горячие клавиши для быстрого доступа
|
||||
- 🖥️ GUI и CLI режимы
|
||||
- 📦 Компиляция в standalone .exe
|
||||
|
||||
## Установка
|
||||
|
||||
```bash
|
||||
python -m venv venv
|
||||
venv\Scripts\activate
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
## Использование
|
||||
|
||||
### Графический интерфейс
|
||||
|
||||
```bash
|
||||
python main.py
|
||||
```
|
||||
|
||||
### Командная строка
|
||||
|
||||
```bash
|
||||
python main.py discover # Поиск устройств
|
||||
python main.py list # Список команд
|
||||
python main.py learn "tv-power" # Обучение команде
|
||||
python main.py send "tv-power" # Отправка команды
|
||||
python main.py send_all # Отправка всех команд
|
||||
```
|
||||
|
||||
## Горячие клавиши (GUI)
|
||||
|
||||
- `Ctrl+D` - Поиск устройств
|
||||
- `Ctrl+L` - Обучить команду
|
||||
- `Ctrl+S` - Отправить команду
|
||||
- `Ctrl+A` - Отправить все команды
|
||||
- `Ctrl+R` - Обновить список
|
||||
- `Ctrl+Q` - Выход
|
||||
|
||||
## Компиляция в .exe
|
||||
|
||||
```bash
|
||||
pyinstaller --onefile --windowed --name BroadlinkManager main.py
|
||||
```
|
||||
|
||||
Готовый файл появится в `dist/BroadlinkManager.exe`.
|
||||
|
||||
## Конфигурация
|
||||
|
||||
Команды хранятся в `broadlink_codes.json`. Настройки устройства в `broadlink_core.py`.
|
||||
|
||||
Подробная документация в файле `build_and_usage.md`.
|
||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,42 @@
|
||||
2026-04-26 19:26:01,376 [INFO] \u2705 Успешно отправлено: 'power-on'
|
||||
2026-04-26 19:26:08,691 [INFO] \u2705 Успешно отправлено: 'power-off'
|
||||
2026-04-26 19:28:26,849 [INFO] \u2705 Успешно отправлено: 'power-on'
|
||||
2026-04-26 19:28:33,497 [INFO] \u2705 Успешно отправлено: 'power-off'
|
||||
2026-04-26 20:26:18,544 [INFO] \u2705 Успешно отправлено: 'power-on'
|
||||
2026-04-26 20:26:21,296 [INFO] \u2705 Успешно отправлено: 'power-off'
|
||||
2026-04-26 20:26:25,965 [INFO] \u2705 Успешно отправлено: 'tusklo'
|
||||
2026-04-26 20:26:34,467 [INFO] \u2705 Успешно отправлено: 'power-on'
|
||||
2026-04-26 20:26:36,187 [INFO] \u2705 Успешно отправлено: 'tusklo'
|
||||
2026-04-26 20:26:37,862 [INFO] \u2705 Успешно отправлено: 'teplo'
|
||||
2026-04-26 20:26:39,825 [INFO] \u2705 Успешно отправлено: 'light-up'
|
||||
2026-04-26 20:26:42,215 [INFO] \u2705 Успешно отправлено: 'light-up'
|
||||
2026-04-26 20:26:43,720 [INFO] \u2705 Успешно отправлено: 'light-up'
|
||||
2026-04-26 20:26:45,285 [INFO] \u2705 Успешно отправлено: 'light-up'
|
||||
2026-04-26 20:26:46,800 [INFO] \u2705 Успешно отправлено: 'warm'
|
||||
2026-04-26 20:26:50,658 [INFO] \u2705 Успешно отправлено: 'day-light'
|
||||
2026-04-26 20:26:53,058 [INFO] \u2705 Успешно отправлено: 'night-light'
|
||||
2026-04-26 20:26:56,744 [INFO] \u2705 Успешно отправлено: 'power-off'
|
||||
2026-04-26 20:27:01,174 [INFO] \u2705 Успешно отправлено: 'power-off'
|
||||
2026-04-26 20:27:07,137 [INFO] \u2705 Успешно отправлено: 'power-on'
|
||||
2026-04-26 20:27:12,998 [INFO] \u2705 Успешно отправлено: 'power-off'
|
||||
2026-04-26 20:27:29,140 [INFO] \u2705 Успешно отправлено: 'night-light'
|
||||
2026-04-26 20:27:32,829 [INFO] \u2705 Успешно отправлено: 'power-off'
|
||||
2026-04-26 20:27:34,859 [INFO] \u2705 Успешно отправлено: 'power-off'
|
||||
2026-04-26 20:29:00,314 [INFO] \u2705 Успешно отправлено: 'power-on'
|
||||
2026-04-26 20:29:03,149 [INFO] \u2705 Успешно отправлено: 'power-off'
|
||||
2026-04-26 20:29:08,655 [INFO] \u2705 Успешно отправлено: 'power-on'
|
||||
2026-04-26 20:29:10,460 [INFO] \u2705 Успешно отправлено: 'rgb'
|
||||
2026-04-26 20:29:12,985 [INFO] \u2705 Успешно отправлено: 'power-off'
|
||||
2026-04-26 20:35:39,261 [INFO] \u2705 Успешно отправлено: 'power-on'
|
||||
2026-04-26 20:35:50,915 [INFO] \u2705 Успешно отправлено: 'power-on'
|
||||
2026-04-26 20:50:03,018 [INFO] \u2705 Успешно отправлено: 'power-on'
|
||||
2026-04-26 20:50:05,937 [INFO] \u2705 Успешно отправлено: 'power-off'
|
||||
2026-05-01 13:18:22,895 [INFO] \u2705 Успешно отправлено: 'power-on'
|
||||
2026-05-01 13:27:11,851 [INFO] \u2705 Успешно отправлено: 'power-off'
|
||||
2026-05-02 20:48:56,519 [INFO] \u2705 Успешно отправлено: 'power-on'
|
||||
2026-05-02 20:56:46,904 [INFO] \u2705 Успешно отправлено: 'power-off'
|
||||
2026-05-03 17:00:45,270 [INFO] \u2705 Успешно отправлено: 'power-off'
|
||||
2026-05-10 23:41:21,554 [INFO] \u2705 Успешно отправлено: 'power-off'
|
||||
2026-05-17 09:02:45,674 [INFO] \u2705 Успешно отправлено: 'power-off'
|
||||
2026-05-17 09:02:47,574 [INFO] \u2705 Успешно отправлено: 'power-off'
|
||||
2026-05-17 09:02:49,475 [INFO] \u2705 Успешно отправлено: 'power-off'
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"power-on": "2600a00000012a91161015101510151016101510151015101635153516351535163515351536153515351635151016101510151015361510151015101635153516351535151016351500051300012a4716000c5700012b4715000c5800012a4716000c5800012a4716000c5700012b4716000c5800012a4716000c5800012a4716000c5700012b4716000c5700012b4716000c5700012b4716000c5700012b4715000d050000000000000000",
|
||||
"power-off": "2600b80000012a9115101610151015101510161015101510153516351535163515351635153516351535161015351535161015101510160f16101535160f161015351536153515351600051200012b4715000c5800012b4716000c5800012a4716000c5700012b4716000c5700012b4715000c5800012b4715000c5800012b4715000c5800012b4715000c5800012a4815000c5800012a4716000c5700012b4716000c5700012b4715000c5800012a4716000c5700012b4715000d05",
|
||||
"night-light": "2600c00000012b90161015101510151016101510151015101635153516351535153516351535163515101510163515351635151015101610153515351610151015101536153515351600051200012b4715000c5700012b4715000c5800012a4716000c5700012a4815000c5700012b4715000c5800012a4716000c5700012a4815000c5700012b4715000c5800012a4716000c5700012a4815000c5700012b4716000c5700012a4716000c5700012a4716000c5600012b4716000c5600012b4716000d050000000000000000",
|
||||
"day-light": "2600a00000012a9115101511151015101510160f161015101535163515351635153516351535153615101535163515101536151015101510163515101510163515101535163515351600051200012b4715000c5800012a4815000c5800012a4815000c5700012b4716000c5700012b4715000c5800012a4815000c5700012b4716000c5700012b4715000c5800012b4715000c5800012a4815000c5500012d4815000d050000000000000000",
|
||||
"warm": "2600b00000012a9115101610151015101510161015101510153516351535163515351635153516351535161015101535160f161015101510151016351535161015351535163515351600051200012b4715000c5800012b4715000c5700012b4716000c5700012b4715000c5800012a4716000c5700012b4715000c5800012a4815000c5800012a4716000c5700012b4716000c5700012a4716000c5500012c4716000c5700012a4815000c5700012b4715000d050000000000000000",
|
||||
"cold": "2600a00000012b91151015101610151015101510151016101535153516351535163515351635153516351535163515101510161015101510151016101510153516351535153615351500051300012a4815000c5700012b4716000c5700012a4815000c5800012a4716000c5800012a4716000c5700012b4716000c5700012b4715000c5800012b4715000c5800012a4815000c5700012b4715000c5800012a4716000d050000000000000000",
|
||||
"light-up": "2600a00000012b90161015101510160f161015101510160f16351535163515351635153516341635160f161015101535160f16101510160f163515351635151015351635153516351500051200012b4716000c5600012b4716000c5600012b4716000c5700012a4716000c5700012b4715000c5700012b4715000c5700012b4715000c5700012b4716000c5600012b4716000c5700012a4716000c5700012a4716000d050000000000000000",
|
||||
"light-down": "2600980000012b90161015101510161015101510151015101635153516351535163515351635153516101535151016351535160f16351510153615101535161015101535161015351600051200012b4715000c5800012a4815000c5800012a4815000c5800012a4716000c5800012a4716000c5700012b4716000c5700012b4715000c5800012b4715000c5800012b4715000c5800012b4715000d05",
|
||||
"rgb": "2600a00000012a91161015101510151016101510151015101536153515361535153615351535163515101635153515101610151015351610153515101610153515351635151016351500051200012b4715000c5800012b4715000c5800012a4815000c5800012a4716000c5700012b4715000c5800012a4815000c5800012a4716000c5700012b4715000c5800012a4815000c5700012a4815000c5700012a4815000d050000000000000000",
|
||||
"mode": "2600a00000012b91151015101510161015101510151016101535153516351535163515351635153516101535153615101510151016351510153516101510153516351535161015351500051300012a4815000c5700012b4716000c5700012b4715000c5800012b4715000c5800012a4815000c5800012a4716000c5700012b4716000c5700012b4715000c5800012a4815000c5800012a4716000c5700012b4715000d050000000000000000",
|
||||
"tusklo": "2600a00000012a9115101510160f161015101510151016101535153516351535163515351635153516101510153516101510151015351610153515361510153516351535151115351500051300012a4716000c5800012a4716000c5800012a4815000c5800012a4815000c5800012a4815000c5800012a4716000c5800012a4716000c5800012a4716000c5800012a4815000c5800012a4815000c5800012a4815000d050000000000000000",
|
||||
"teplo": "2600a80000012a91161015101510151016101510151015101536153515361535153615351535163515351610153515111535151015101511151015351610153515101635153516351500051300012a4716000c5700012a4815000c5700012b4716000c5700012a4815000c5800012a4716000c5700012b4716000c5700012b4715000c5800012b4715000c5800012b4715000c5800012b4715000c5800012a4815000c5800012a4815000d05"
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
import broadlink
|
||||
import time
|
||||
import json
|
||||
import os
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import Optional, Dict, List, Any
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
BASE_DIR = Path(__file__).parent
|
||||
CODES_FILE = BASE_DIR / "broadlink_codes.json"
|
||||
|
||||
DEFAULT_CONFIG = {
|
||||
"ip": "192.168.1.192",
|
||||
"mac": "78:0f:77:18:08:00",
|
||||
"port": 80,
|
||||
"dev_type": 0x2712 # RM Pro+
|
||||
}
|
||||
|
||||
def load_codes() -> Dict[str, str]:
|
||||
"""Загружает коды, автоматически обрезая пробелы в именах команд."""
|
||||
if CODES_FILE.exists():
|
||||
with open(CODES_FILE, "r", encoding="utf-8") as f:
|
||||
raw = json.load(f)
|
||||
return {k.strip(): v for k, v in raw.items()}
|
||||
return {}
|
||||
|
||||
def save_codes(codes: Dict[str, str]) -> bool:
|
||||
with open(CODES_FILE, "w", encoding="utf-8") as f:
|
||||
json.dump(codes, f, indent=4, ensure_ascii=False)
|
||||
return True
|
||||
|
||||
def create_device(ip: str = None, mac: str = None, port: int = None, dev_type: int = None) -> Optional[Any]:
|
||||
ip = ip or DEFAULT_CONFIG["ip"]
|
||||
mac = mac or DEFAULT_CONFIG["mac"]
|
||||
port = port or DEFAULT_CONFIG["port"]
|
||||
dev_type = dev_type or DEFAULT_CONFIG["dev_type"]
|
||||
|
||||
try:
|
||||
mac_bytes = bytes.fromhex(mac.replace(":", "").lower())
|
||||
dev = broadlink.gendevice(dev_type=dev_type, host=(ip, port), mac=mac_bytes)
|
||||
dev.auth()
|
||||
logger.info(f"✅ Устройство {ip} авторизовано (тип 0x{dev_type:04x})")
|
||||
return dev
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Ошибка подключения: {e}")
|
||||
return None
|
||||
|
||||
def learn_code(dev, name: str, timeout: int = 15) -> Optional[str]:
|
||||
try:
|
||||
dev.enter_learning()
|
||||
except Exception as e:
|
||||
logger.error(f"⚠️ Не удалось войти в режим обучения: {e}")
|
||||
return None
|
||||
|
||||
for _ in range(timeout):
|
||||
time.sleep(1)
|
||||
try:
|
||||
data = dev.check_data()
|
||||
if data:
|
||||
logger.info(f"✅ Код '{name}' захвачен! ({len(data)} байт)")
|
||||
return data.hex()
|
||||
except (broadlink.exceptions.ReadError, AttributeError, Exception):
|
||||
continue
|
||||
logger.warning(f"❌ Таймаут для '{name}'")
|
||||
return None
|
||||
|
||||
def send_command(dev, command_name: str, codes: Dict[str, str] = None) -> bool:
|
||||
if codes is None:
|
||||
codes = load_codes()
|
||||
if command_name not in codes:
|
||||
logger.error(f"❌ Команда '{command_name}' не найдена.")
|
||||
return False
|
||||
try:
|
||||
hex_code = codes[command_name]
|
||||
data = bytes.fromhex(hex_code)
|
||||
dev.send_data(data)
|
||||
logger.info(f"📤 Отправлено: '{command_name}' ✅")
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Ошибка отправки '{command_name}': {e}")
|
||||
return False
|
||||
|
||||
def discover_devices() -> List[Dict[str, Any]]:
|
||||
try:
|
||||
devices = broadlink.discover(timeout=5)
|
||||
return [{"host": d.host[0], "mac": d.mac.hex(), "type": d.devtype} for d in devices]
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка поиска: {e}")
|
||||
return []
|
||||
@@ -0,0 +1,54 @@
|
||||
# 📘 Broadlink Manager Pro: Настройка и Использование
|
||||
|
||||
## 🛠️ 1. Установка
|
||||
Откройте терминал в папке `C:\Users\dimir\proects\broadlink`:
|
||||
```bash
|
||||
python -m venv venv
|
||||
venv\Scripts\activate
|
||||
pip install -r requirements.txt
|
||||
|
||||
🖥️ 2. Запуск
|
||||
Графический интерфейс: python main.py
|
||||
Командная строка:
|
||||
python main.py discover (поиск устройств)
|
||||
python main.py list (список команд)
|
||||
python main.py learn "tv-power" (обучение)
|
||||
python main.py send "tv-power" (отправка)
|
||||
python main.py send_all (отправка всех)
|
||||
📦 3. Компиляция в .exe (Windows 11)
|
||||
bash
|
||||
1
|
||||
pyinstaller --onefile --windowed --name BroadlinkManager main.py
|
||||
Готовый BroadlinkManager.exe появится в dist/.
|
||||
⚠️ Для CLI-режима скомпилированного exe добавьте параметр --console вместо --windowed, либо запускайте через cmd.
|
||||
⌨️ Горячие клавиши (в GUI)
|
||||
Клавиша
|
||||
Действие
|
||||
Ctrl+D
|
||||
Поиск устройств в сети
|
||||
Ctrl+L
|
||||
Обучить выбранную команду
|
||||
Ctrl+S
|
||||
Отправить выбранную команду
|
||||
Ctrl+A
|
||||
Отправить все команды подряд
|
||||
Ctrl+R
|
||||
Обновить список команд
|
||||
Ctrl+Q
|
||||
Закрыть приложение
|
||||
🔧 Настройка под ваше устройство
|
||||
В broadlink_core.py измените DEFAULT_CONFIG, если IP/MAC изменятся.
|
||||
Для RM4 Pro замените "dev_type": 0x2712 на 0x5213.
|
||||
|
||||
|
||||
---
|
||||
|
||||
### ✅ Что реализовано:
|
||||
1. **Интеграция ваших данных:** Все 12 команд из `broadlink_codes.json` подгружаются автоматически. Пробелы в названиях команд автоматически обрезаются для корректной работы интерфейса.
|
||||
2. **Исправление ошибок оригиналов:** Убраны синтаксические ошибки (`D EVICE_PORT`, `if name == "main"`), добавлена корректная обработка `check_data()` и `auth()`.
|
||||
3. **Потокобезопасный GUI:** Сетевые операции не блокируют интерфейс. Лог обновляется через `queue`.
|
||||
4. **Горячие клавиши:** Полная поддержка `Ctrl+D/L/S/A/R/Q`.
|
||||
5. **CLI+GUI в одном:** `main.py` запускает GUI по умолчанию, но поддерживает все аргументы командной строки после компиляции.
|
||||
6. **Готово к `pyinstaller`:** Все пути относительные (`Path(__file__).parent`), зависимости указаны.
|
||||
|
||||
Сохраните файлы, выполните `pip install -r requirements.txt` и запустите `python main.py`. Если потребуются доработки под конкретные функции из `learn-broadlink`, пришлите их названия — я вплету их без нарушения архитектуры.
|
||||
+157
@@ -0,0 +1,157 @@
|
||||
import tkinter as tk
|
||||
import customtkinter as ctk
|
||||
import threading
|
||||
import queue
|
||||
import time
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from broadlink_core import (create_device, learn_code, load_codes, save_codes,
|
||||
send_command, discover_devices, DEFAULT_CONFIG)
|
||||
|
||||
ctk.set_appearance_mode("System")
|
||||
ctk.set_default_color_theme("blue")
|
||||
|
||||
class BroadlinkApp(ctk.CTk):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.title("Broadlink Manager Pro")
|
||||
self.geometry("880x680")
|
||||
self.minsize(750, 550)
|
||||
self.queue = queue.Queue()
|
||||
self.device = None
|
||||
self.codes = load_codes()
|
||||
|
||||
self.setup_ui()
|
||||
self.bind_shortcuts()
|
||||
self.log("🚀 Приложение запущено. Загружено команд: " + str(len(self.codes)))
|
||||
self.after(100, self.process_queue)
|
||||
|
||||
def setup_ui(self):
|
||||
# Настройки устройства
|
||||
cfg = ctk.CTkFrame(self)
|
||||
cfg.pack(fill="x", padx=10, pady=10)
|
||||
ctk.CTkLabel(cfg, text="🌐 IP:").pack(side="left", padx=(10,0))
|
||||
self.ip_var = tk.StringVar(value=DEFAULT_CONFIG["ip"])
|
||||
ctk.CTkEntry(cfg, textvariable=self.ip_var, width=130).pack(side="left", padx=5)
|
||||
ctk.CTkLabel(cfg, text="🔗 MAC:").pack(side="left", padx=5)
|
||||
self.mac_var = tk.StringVar(value=DEFAULT_CONFIG["mac"])
|
||||
ctk.CTkEntry(cfg, textvariable=self.mac_var, width=140).pack(side="left", padx=5)
|
||||
ctk.CTkButton(cfg, text="🔌 Подключить", command=self.connect_device).pack(side="right", padx=10)
|
||||
|
||||
# Управление
|
||||
ctrl = ctk.CTkFrame(self)
|
||||
ctrl.pack(fill="x", padx=10, pady=10)
|
||||
ctk.CTkLabel(ctrl, text="📜 Команда:").pack(side="left", padx=(10,5))
|
||||
self.cmd_var = tk.StringVar()
|
||||
self.cmd_combo = ctk.CTkComboBox(ctrl, variable=self.cmd_var, values=sorted(self.codes.keys()), width=200)
|
||||
self.cmd_combo.pack(side="left", padx=5)
|
||||
if self.codes: self.cmd_combo.set(next(iter(sorted(self.codes.keys()))))
|
||||
|
||||
ctk.CTkButton(ctrl, text="📡 Обучение", command=self.learn_selected).pack(side="left", padx=5)
|
||||
ctk.CTkButton(ctrl, text="🚀 Отправить", command=self.send_selected).pack(side="left", padx=5)
|
||||
ctk.CTkButton(ctrl, text="⏯ Все подряд", command=self.send_all).pack(side="left", padx=5)
|
||||
ctk.CTkButton(ctrl, text="🔍 Поиск", command=self.discover).pack(side="left", padx=5)
|
||||
ctk.CTkButton(ctrl, text="🔄 Обновить", command=self.refresh_codes).pack(side="left", padx=5)
|
||||
|
||||
# Лог
|
||||
self.log_frame = ctk.CTkTextbox(self, height=220)
|
||||
self.log_frame.pack(fill="both", expand=True, padx=10, pady=10)
|
||||
self.status_bar = ctk.CTkLabel(self, text="Готово", anchor="w")
|
||||
self.status_bar.pack(fill="x", padx=10, pady=(0,5))
|
||||
|
||||
def bind_shortcuts(self):
|
||||
self.bind("<Control-d>", lambda e: self.discover())
|
||||
self.bind("<Control-l>", lambda e: self.learn_selected())
|
||||
self.bind("<Control-s>", lambda e: self.send_selected())
|
||||
self.bind("<Control-a>", lambda e: self.send_all())
|
||||
self.bind("<Control-r>", lambda e: self.refresh_codes())
|
||||
self.bind("<Control-q>", lambda e: self.quit())
|
||||
|
||||
def log(self, msg: str):
|
||||
self.queue.put(("log", f"[{datetime.now().strftime('%H:%M:%S')}] {msg}\n"))
|
||||
|
||||
def process_queue(self):
|
||||
try:
|
||||
while True:
|
||||
action, data = self.queue.get_nowait()
|
||||
if action == "log":
|
||||
self.log_frame.insert("end", data)
|
||||
self.log_frame.see("end")
|
||||
elif action == "status":
|
||||
self.status_bar.configure(text=data)
|
||||
elif action == "update_combo":
|
||||
self.cmd_combo.configure(values=sorted(data.keys()))
|
||||
if data: self.cmd_combo.set(next(iter(sorted(data.keys()))))
|
||||
except queue.Empty:
|
||||
pass
|
||||
self.after(100, self.process_queue)
|
||||
|
||||
def get_dev(self):
|
||||
if not self.device:
|
||||
try:
|
||||
self.device = create_device(self.ip_var.get(), self.mac_var.get())
|
||||
except Exception as e:
|
||||
self.queue.put(("status", f"❌ {e}"))
|
||||
return None
|
||||
return self.device
|
||||
|
||||
def connect_device(self):
|
||||
threading.Thread(target=self._connect_worker, daemon=True).start()
|
||||
def _connect_worker(self):
|
||||
self.queue.put(("status", "🔌 Подключение..."))
|
||||
self.device = create_device(self.ip_var.get(), self.mac_var.get())
|
||||
self.queue.put(("status", "✅ Подключено" if self.device else "❌ Ошибка подключения"))
|
||||
|
||||
def learn_selected(self):
|
||||
threading.Thread(target=self._learn_worker, daemon=True).start()
|
||||
def _learn_worker(self):
|
||||
dev = self.get_dev()
|
||||
if not dev: return
|
||||
name = self.cmd_var.get().strip()
|
||||
self.queue.put(("status", f"📡 Обучение '{name}'... Нажмите кнопку на пульте"))
|
||||
code = learn_code(dev, name)
|
||||
if code:
|
||||
self.codes[name] = code
|
||||
save_codes(self.codes)
|
||||
self.queue.put(("update_combo", self.codes))
|
||||
self.queue.put(("status", f"✅ '{name}' сохранено"))
|
||||
else:
|
||||
self.queue.put(("status", "❌ Обучение отменено"))
|
||||
|
||||
def send_selected(self):
|
||||
threading.Thread(target=self._send_worker, daemon=True).start()
|
||||
def _send_worker(self):
|
||||
dev = self.get_dev()
|
||||
if not dev: return
|
||||
name = self.cmd_var.get().strip()
|
||||
self.queue.put(("status", f"🚀 Отправка '{name}'..."))
|
||||
if send_command(dev, name, self.codes):
|
||||
self.queue.put(("status", "✅ Отправлено"))
|
||||
else:
|
||||
self.queue.put(("status", "❌ Ошибка отправки"))
|
||||
|
||||
def send_all(self):
|
||||
threading.Thread(target=self._send_all_worker, daemon=True).start()
|
||||
def _send_all_worker(self):
|
||||
dev = self.get_dev()
|
||||
if not dev: return
|
||||
self.queue.put(("status", "⏯ Отправка всех команд..."))
|
||||
for name in sorted(self.codes.keys()):
|
||||
self.queue.put(("status", f"📤 {name}"))
|
||||
send_command(dev, name, self.codes)
|
||||
time.sleep(1.5)
|
||||
self.queue.put(("status", "🏁 Все команды отправлены"))
|
||||
|
||||
def refresh_codes(self):
|
||||
self.codes = load_codes()
|
||||
self.queue.put(("update_combo", self.codes))
|
||||
self.queue.put(("log", "📜 Список команд обновлён\n"))
|
||||
|
||||
def discover(self):
|
||||
threading.Thread(target=self._discover_worker, daemon=True).start()
|
||||
def _discover_worker(self):
|
||||
self.queue.put(("status", "🔍 Поиск устройств..."))
|
||||
devs = discover_devices()
|
||||
for d in devs:
|
||||
self.queue.put(("log", f"Найдено: {d['host']} | MAC: {d['mac']} | Тип: 0x{d['type']:04x}\n"))
|
||||
self.queue.put(("status", f"🔍 Найдено: {len(devs)}"))
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,67 @@
|
||||
import sys
|
||||
import argparse
|
||||
import logging
|
||||
from broadlink_core import (discover_devices, create_device, learn_code,
|
||||
load_codes, save_codes, send_command, DEFAULT_CONFIG)
|
||||
|
||||
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Broadlink Manager CLI & GUI")
|
||||
parser.add_argument("--ip", default=DEFAULT_CONFIG["ip"])
|
||||
parser.add_argument("--mac", default=DEFAULT_CONFIG["mac"])
|
||||
parser.add_argument("--type", default=str(DEFAULT_CONFIG["dev_type"]))
|
||||
|
||||
sub = parser.add_subparsers(dest="action")
|
||||
sub.add_parser("discover", help="Поиск устройств в сети")
|
||||
sub.add_parser("list", help="Список сохранённых команд")
|
||||
sub.add_parser("gui", help="Запустить графический интерфейс")
|
||||
|
||||
p_learn = sub.add_parser("learn", help="Обучить команду")
|
||||
p_learn.add_argument("name", help="Имя команды")
|
||||
|
||||
p_send = sub.add_parser("send", help="Отправить команду")
|
||||
p_send.add_argument("name", help="Имя команды")
|
||||
sub.add_parser("send_all", help="Отправить все команды по очереди")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if not args.action or args.action == "gui":
|
||||
from gui_app import BroadlinkApp
|
||||
app = BroadlinkApp()
|
||||
app.mainloop()
|
||||
return
|
||||
|
||||
if args.action == "discover":
|
||||
for d in discover_devices():
|
||||
print(f"IP: {d['host']} | MAC: {d['mac']} | Тип: 0x{d['type']:04x}")
|
||||
return
|
||||
|
||||
if args.action == "list":
|
||||
for n in sorted(load_codes().keys()): print(f" • {n}")
|
||||
return
|
||||
|
||||
dev = create_device(args.ip, args.mac, dev_type=int(args.type, 16))
|
||||
if not dev:
|
||||
print("❌ Не удалось подключиться"); sys.exit(1)
|
||||
|
||||
if args.action == "learn":
|
||||
code = learn_code(dev, args.name)
|
||||
if code:
|
||||
codes = load_codes(); codes[args.name] = code; save_codes(codes)
|
||||
print(f"✅ '{args.name}' сохранено")
|
||||
else: print("❌ Обучение не удалось")
|
||||
|
||||
elif args.action == "send":
|
||||
print("✅ Отправлено" if send_command(dev, args.name) else "❌ Ошибка")
|
||||
|
||||
elif args.action == "send_all":
|
||||
codes = load_codes()
|
||||
for name in sorted(codes.keys()):
|
||||
print(f"📤 {name}")
|
||||
send_command(dev, name, codes)
|
||||
time.sleep(1.5)
|
||||
print("🏁 Готово")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,54 @@
|
||||
import subprocess
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
|
||||
# Директория проекта и файл скрипта
|
||||
SCRIPT_DIR = Path(__file__).parent.resolve()
|
||||
SILENT_SCRIPT = SCRIPT_DIR / "silent_send.py"
|
||||
|
||||
# Папка для ярлыков (создаётся автоматически)
|
||||
LINK_DIR = SCRIPT_DIR / "link"
|
||||
LINK_DIR.mkdir(exist_ok=True)
|
||||
|
||||
# Ищем pythonw.exe в системе (автоматически найдёт ваш Python)
|
||||
PYTHONW = shutil.which("pythonw.exe")
|
||||
if not PYTHONW:
|
||||
PYTHONW = "pythonw.exe" # Фоллбэк, если python есть в PATH
|
||||
|
||||
# Команды: ключ = имя в JSON, значение = имя файла ярлыка
|
||||
commands = {
|
||||
"power-on": "01_ВКЛ", "power-off": "02_ВЫКЛ", "night-light": "03_Ночной",
|
||||
"day-light": "04_Дневной", "warm": "05_Тёплый", "cold": "06_Холодный",
|
||||
"light-up": "07_Ярче", "light-down": "08_Темнее", "rgb": "09_RGB",
|
||||
"mode": "10_Режим", "tusklo": "11_Тускло", "teplo": "12_Тепло"
|
||||
}
|
||||
|
||||
print(f"🔍 Исполнитель: {PYTHONW}")
|
||||
print(f"📂 Папка ярлыков: {LINK_DIR}\n")
|
||||
|
||||
for cmd, label in commands.items():
|
||||
lnk_path = LINK_DIR / f"{label}.lnk"
|
||||
|
||||
# ✅ ПРАВИЛЬНОЕ РАЗДЕЛЕНИЕ:
|
||||
target_path = PYTHONW
|
||||
arguments = f'"{SILENT_SCRIPT}" {cmd}'
|
||||
working_dir = str(SCRIPT_DIR)
|
||||
|
||||
# PowerShell скрипт создания ярлыка
|
||||
ps_script = f"""
|
||||
$WshShell = New-Object -ComObject WScript.Shell
|
||||
$Shortcut = $WshShell.CreateShortcut('{lnk_path}')
|
||||
$Shortcut.TargetPath = '{target_path}'
|
||||
$Shortcut.Arguments = '{arguments}'
|
||||
$Shortcut.WorkingDirectory = '{working_dir}'
|
||||
$Shortcut.Save()
|
||||
"""
|
||||
|
||||
try:
|
||||
subprocess.run(["powershell", "-NoProfile", "-Command", ps_script], capture_output=True, check=True)
|
||||
print(f"✅ Создан: {lnk_path.name}")
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(f"❌ Ошибка создания {lnk_path.name}: {e.stderr.decode('utf-8')}")
|
||||
|
||||
print(f"\n🎉 Все ярлыки успешно созданы в: {LINK_DIR}")
|
||||
print("💡 Теперь зайдите в Свойства каждого ярлыка → вкладка 'Ярлык' → поле 'Быстрый вызов' и назначите клавиши.")
|
||||
@@ -0,0 +1,3 @@
|
||||
broadlink>=0.19.0
|
||||
customtkinter>=5.2.0
|
||||
pyinstaller>=6.0.0
|
||||
@@ -0,0 +1,60 @@
|
||||
import sys
|
||||
import json
|
||||
import logging
|
||||
import broadlink
|
||||
from pathlib import Path
|
||||
|
||||
# 🔧 Настройки (те же, что в ваших исходниках)
|
||||
DEVICE_IP = "192.168.1.192"
|
||||
DEVICE_MAC = "78:0f:77:18:08:00"
|
||||
DEVICE_TYPE = 0x2712 # RM Pro+ (для RM4 Pro используйте 0x5213)
|
||||
|
||||
SCRIPT_DIR = Path(__file__).parent.resolve()
|
||||
CODES_FILE = SCRIPT_DIR / "broadlink_codes.json"
|
||||
LOG_FILE = SCRIPT_DIR / "broadlink_actions.log"
|
||||
|
||||
# Логирование в файл (консоль скрыта, поэтому логи нужны для отладки)
|
||||
logging.basicConfig(
|
||||
filename=LOG_FILE, level=logging.INFO,
|
||||
format='%(asctime)s [%(levelname)s] %(message)s', filemode='a'
|
||||
)
|
||||
|
||||
def main():
|
||||
if len(sys.argv) < 2:
|
||||
logging.info("ℹ️ Запущен без параметров. Использование: silent_send.py <имя_команды>")
|
||||
return
|
||||
|
||||
cmd = sys.argv[1].strip()
|
||||
|
||||
if not CODES_FILE.exists():
|
||||
logging.error(f"❌ Файл {CODES_FILE} не найден в {SCRIPT_DIR}")
|
||||
return
|
||||
|
||||
try:
|
||||
with open(CODES_FILE, "r", encoding="utf-8") as f:
|
||||
# Автоматически убираем пробелы в ключах и значениях (исправление вашего JSON)
|
||||
raw = json.load(f)
|
||||
codes = {k.strip(): v.strip() for k, v in raw.items()}
|
||||
except Exception as e:
|
||||
logging.error(f"❌ Ошибка чтения JSON: {e}")
|
||||
return
|
||||
|
||||
if cmd not in codes:
|
||||
logging.error(f"❌ Команда '{cmd}' не найдена. Доступные: {', '.join(codes.keys())}")
|
||||
return
|
||||
|
||||
try:
|
||||
mac_bytes = bytes.fromhex(DEVICE_MAC.replace(":", ""))
|
||||
dev = broadlink.gendevice(
|
||||
dev_type=DEVICE_TYPE,
|
||||
host=(DEVICE_IP, 80),
|
||||
mac=mac_bytes
|
||||
)
|
||||
dev.auth()
|
||||
dev.send_data(bytes.fromhex(codes[cmd]))
|
||||
logging.info(f"✅ Успешно отправлено: '{cmd}'")
|
||||
except Exception as e:
|
||||
logging.error(f"❌ Ошибка сети/устройства: {e}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user