commit 9815bc3f76c231ee796dd77ede505c35bf9865c5 Author: dinlo Date: Sun May 31 18:43:20 2026 +0800 Initial commit Co-Authored-By: Claude Opus 4.8 (1M context) diff --git a/README.md b/README.md new file mode 100644 index 0000000..1186ce3 --- /dev/null +++ b/README.md @@ -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`. diff --git a/__pycache__/broadlink_core.cpython-312.pyc b/__pycache__/broadlink_core.cpython-312.pyc new file mode 100644 index 0000000..bf48b1a Binary files /dev/null and b/__pycache__/broadlink_core.cpython-312.pyc differ diff --git a/__pycache__/gui_app.cpython-312.pyc b/__pycache__/gui_app.cpython-312.pyc new file mode 100644 index 0000000..5110763 Binary files /dev/null and b/__pycache__/gui_app.cpython-312.pyc differ diff --git a/broadlink_actions.log b/broadlink_actions.log new file mode 100644 index 0000000..24efc89 --- /dev/null +++ b/broadlink_actions.log @@ -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' diff --git a/broadlink_codes.json b/broadlink_codes.json new file mode 100644 index 0000000..6564d84 --- /dev/null +++ b/broadlink_codes.json @@ -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" +} \ No newline at end of file diff --git a/broadlink_core.py b/broadlink_core.py new file mode 100644 index 0000000..5fa58a0 --- /dev/null +++ b/broadlink_core.py @@ -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 [] \ No newline at end of file diff --git a/build_and_usage.md b/build_and_usage.md new file mode 100644 index 0000000..4da67cf --- /dev/null +++ b/build_and_usage.md @@ -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`, пришлите их названия — я вплету их без нарушения архитектуры. \ No newline at end of file diff --git a/gui_app.py b/gui_app.py new file mode 100644 index 0000000..6b46a19 --- /dev/null +++ b/gui_app.py @@ -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("", lambda e: self.discover()) + self.bind("", lambda e: self.learn_selected()) + self.bind("", lambda e: self.send_selected()) + self.bind("", lambda e: self.send_all()) + self.bind("", lambda e: self.refresh_codes()) + self.bind("", 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)}")) \ No newline at end of file diff --git a/link/01_ВКЛ.lnk b/link/01_ВКЛ.lnk new file mode 100644 index 0000000..b30c66b Binary files /dev/null and b/link/01_ВКЛ.lnk differ diff --git a/link/02_ВЫКЛ.lnk b/link/02_ВЫКЛ.lnk new file mode 100644 index 0000000..934630c Binary files /dev/null and b/link/02_ВЫКЛ.lnk differ diff --git a/link/03_Ночной.lnk b/link/03_Ночной.lnk new file mode 100644 index 0000000..83500d2 Binary files /dev/null and b/link/03_Ночной.lnk differ diff --git a/link/04_Дневной.lnk b/link/04_Дневной.lnk new file mode 100644 index 0000000..5bd9e46 Binary files /dev/null and b/link/04_Дневной.lnk differ diff --git a/link/05_Тёплый.lnk b/link/05_Тёплый.lnk new file mode 100644 index 0000000..c0225a5 Binary files /dev/null and b/link/05_Тёплый.lnk differ diff --git a/link/06_Холодный.lnk b/link/06_Холодный.lnk new file mode 100644 index 0000000..f464c44 Binary files /dev/null and b/link/06_Холодный.lnk differ diff --git a/link/07_Ярче.lnk b/link/07_Ярче.lnk new file mode 100644 index 0000000..d2a8750 Binary files /dev/null and b/link/07_Ярче.lnk differ diff --git a/link/08_Темнее.lnk b/link/08_Темнее.lnk new file mode 100644 index 0000000..401c2c2 Binary files /dev/null and b/link/08_Темнее.lnk differ diff --git a/link/09_RGB.lnk b/link/09_RGB.lnk new file mode 100644 index 0000000..b4c82ff Binary files /dev/null and b/link/09_RGB.lnk differ diff --git a/link/10_Режим.lnk b/link/10_Режим.lnk new file mode 100644 index 0000000..47915b0 Binary files /dev/null and b/link/10_Режим.lnk differ diff --git a/link/11_Тускло.lnk b/link/11_Тускло.lnk new file mode 100644 index 0000000..ca618bf Binary files /dev/null and b/link/11_Тускло.lnk differ diff --git a/link/12_Тепло.lnk b/link/12_Тепло.lnk new file mode 100644 index 0000000..70bf872 Binary files /dev/null and b/link/12_Тепло.lnk differ diff --git a/main.py b/main.py new file mode 100644 index 0000000..85eb06c --- /dev/null +++ b/main.py @@ -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() \ No newline at end of file diff --git a/make_shortcuts.py b/make_shortcuts.py new file mode 100644 index 0000000..1bbe0f4 --- /dev/null +++ b/make_shortcuts.py @@ -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("💡 Теперь зайдите в Свойства каждого ярлыка → вкладка 'Ярлык' → поле 'Быстрый вызов' и назначите клавиши.") \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..66a2707 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +broadlink>=0.19.0 +customtkinter>=5.2.0 +pyinstaller>=6.0.0 \ No newline at end of file diff --git a/silent_send.py b/silent_send.py new file mode 100644 index 0000000..cbe71b0 --- /dev/null +++ b/silent_send.py @@ -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() \ No newline at end of file