|
|
|
""" |
|
Home Assistant设备管理器 - 图形界面 |
|
用于查询Home Assistant设备并将其添加到配置文件中 |
|
""" |
|
|
|
import os |
|
import sys |
|
import json |
|
import time |
|
import threading |
|
from typing import Dict, List, Optional, Any, Tuple |
|
import logging |
|
|
|
|
|
current_dir = os.path.dirname(os.path.abspath(__file__)) |
|
project_root = os.path.dirname(current_dir) |
|
sys.path.append(project_root) |
|
|
|
|
|
from src.utils.config_manager import ConfigManager |
|
|
|
try: |
|
from PyQt5.QtWidgets import ( |
|
QApplication, QMainWindow, QWidget, QPushButton, QTableWidgetItem, |
|
QHeaderView, QMessageBox, QHBoxLayout, QLineEdit, QComboBox, |
|
QTableWidget, QStackedWidget, QTabBar, QAbstractItemView, QVBoxLayout, |
|
QFrame |
|
) |
|
from PyQt5.QtCore import Qt, QThread, pyqtSignal |
|
from PyQt5.QtGui import QFont, QIcon, QColor |
|
from PyQt5 import uic |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
except ImportError: |
|
print("错误: 未安装PyQt5库") |
|
print("请运行: pip install PyQt5") |
|
sys.exit(1) |
|
|
|
try: |
|
import requests |
|
except ImportError: |
|
print("错误: 未安装requests库") |
|
print("请运行: pip install requests") |
|
sys.exit(1) |
|
|
|
|
|
DOMAIN_ICONS = { |
|
"light": "灯具 💡", |
|
"switch": "开关 🔌", |
|
"sensor": "传感器 🌡️", |
|
"climate": "空调 ❄️", |
|
"fan": "风扇 💨", |
|
"media_player": "媒体播放器 📺", |
|
"camera": "摄像头 📷", |
|
"cover": "窗帘 🪟", |
|
"vacuum": "扫地机器人 🧹", |
|
"binary_sensor": "二元传感器 🔔", |
|
"lock": "锁 🔒", |
|
"alarm_control_panel": "安防面板 🚨", |
|
"automation": "自动化 ⚙️", |
|
"script": "脚本 📜" |
|
} |
|
|
|
class DeviceLoadThread(QThread): |
|
"""加载设备的线程""" |
|
devices_loaded = pyqtSignal(list) |
|
error_occurred = pyqtSignal(str) |
|
|
|
def __init__(self, url, token, domain="all"): |
|
super().__init__() |
|
self.url = url |
|
self.token = token |
|
self.domain = domain |
|
self._is_running = True |
|
|
|
def run(self): |
|
try: |
|
|
|
if not self._is_running: |
|
return |
|
|
|
devices = self.get_device_list(self.url, self.token, self.domain) |
|
|
|
|
|
if not self._is_running: |
|
return |
|
|
|
self.devices_loaded.emit(devices) |
|
except Exception as e: |
|
if self._is_running: |
|
self.error_occurred.emit(str(e)) |
|
|
|
def terminate(self): |
|
"""安全终止线程""" |
|
self._is_running = False |
|
super().terminate() |
|
|
|
def get_device_list(self, url: str, token: str, domain: str = "all") -> List[Dict[str, Any]]: |
|
"""从Home Assistant API获取设备列表""" |
|
headers = { |
|
"Authorization": f"Bearer {token}", |
|
"Content-Type": "application/json", |
|
} |
|
|
|
try: |
|
|
|
response = requests.get(f"{url}/api/states", headers=headers, timeout=10) |
|
|
|
if response.status_code != 200: |
|
error_msg = f"错误: 无法获取设备列表 (HTTP {response.status_code}): {response.text}" |
|
self.error_occurred.emit(error_msg) |
|
return [] |
|
|
|
|
|
if not self._is_running: |
|
return [] |
|
|
|
|
|
entities = response.json() |
|
|
|
|
|
domain_entities = [] |
|
for entity in entities: |
|
|
|
if not self._is_running: |
|
return [] |
|
|
|
entity_id = entity.get("entity_id", "") |
|
entity_domain = entity_id.split(".", 1)[0] if "." in entity_id else "" |
|
|
|
if domain == "all" or entity_domain == domain: |
|
domain_entities.append({ |
|
"entity_id": entity_id, |
|
"domain": entity_domain, |
|
"friendly_name": entity.get("attributes", {}).get("friendly_name", entity_id), |
|
"state": entity.get("state", "unknown") |
|
}) |
|
|
|
|
|
domain_entities.sort(key=lambda x: (x["domain"], x["friendly_name"])) |
|
return domain_entities |
|
|
|
except Exception as e: |
|
if self._is_running: |
|
self.error_occurred.emit(f"错误: 获取设备列表失败 - {e}") |
|
return [] |
|
|
|
class HomeAssistantDeviceManager(QMainWindow): |
|
"""Home Assistant设备管理器GUI""" |
|
|
|
def __init__(self): |
|
super().__init__() |
|
|
|
|
|
self.config = ConfigManager.get_instance() |
|
self.ha_url = self.config.get_config("HOME_ASSISTANT.URL", "") |
|
self.ha_token = self.config.get_config("HOME_ASSISTANT.TOKEN", "") |
|
|
|
if not self.ha_url or not self.ha_token: |
|
QMessageBox.critical( |
|
self, |
|
"配置错误", |
|
"未找到Home Assistant配置,请确保config/config.json中包含有效的HOME_ASSISTANT.URL和HOME_ASSISTANT.TOKEN" |
|
) |
|
sys.exit(1) |
|
|
|
|
|
self.added_devices = self.config.get_config("HOME_ASSISTANT.DEVICES", []) |
|
|
|
|
|
self.current_devices = [] |
|
|
|
|
|
self.domain_mapping = {} |
|
|
|
|
|
self.threads = [] |
|
self.load_thread = None |
|
|
|
|
|
self.logger = logging.getLogger("HADeviceManager") |
|
|
|
|
|
self.load_ui() |
|
|
|
|
|
self.apply_stylesheet() |
|
|
|
|
|
self.init_ui() |
|
|
|
|
|
self.connect_signals() |
|
|
|
|
|
self.load_devices("all") |
|
|
|
def closeEvent(self, event): |
|
"""窗口关闭事件处理""" |
|
|
|
self.stop_all_threads() |
|
super().closeEvent(event) |
|
|
|
def stop_all_threads(self): |
|
"""停止所有线程""" |
|
|
|
if self.load_thread and self.load_thread.isRunning(): |
|
self.logger.info("停止当前加载线程...") |
|
try: |
|
self.load_thread.terminate() |
|
if not self.load_thread.wait(1000): |
|
self.logger.warning("加载线程未能在1秒内停止") |
|
except Exception as e: |
|
self.logger.error(f"停止加载线程时出错: {e}") |
|
|
|
|
|
for thread in self.threads[:]: |
|
if thread and thread.isRunning(): |
|
self.logger.info(f"停止线程: {thread}") |
|
try: |
|
if hasattr(thread, 'terminate'): |
|
thread.terminate() |
|
if not thread.wait(1000): |
|
self.logger.warning(f"线程未能在1秒内停止: {thread}") |
|
except Exception as e: |
|
self.logger.error(f"停止线程时出错: {e}") |
|
|
|
|
|
self.threads.clear() |
|
self.load_thread = None |
|
|
|
def apply_stylesheet(self): |
|
"""应用自定义样式表美化界面""" |
|
stylesheet = """ |
|
QMainWindow { |
|
background-color: #f0f0f0; /* 窗口背景色 */ |
|
} |
|
|
|
/* 卡片样式 (使用 QFrame 替代) */ |
|
QFrame#available_card, QFrame#added_card { |
|
background-color: white; |
|
border-radius: 8px; |
|
border: 1px solid #dcdcdc; |
|
padding: 5px; /* 内边距 */ |
|
} |
|
|
|
/* 导航栏样式 (QTabBar) */ |
|
QTabBar::tab { |
|
background: #e1e1e1; |
|
border: 1px solid #c4c4c4; |
|
border-bottom: none; /* 无下边框 */ |
|
border-top-left-radius: 4px; |
|
border-top-right-radius: 4px; |
|
padding: 8px 15px; |
|
margin-right: 2px; |
|
color: #333; /* 标签文字颜色 */ |
|
} |
|
|
|
QTabBar::tab:selected { |
|
background: white; /* 选中时背景与卡片一致 */ |
|
border-color: #c4c4c4; |
|
margin-bottom: -1px; /* 轻微重叠,消除边框 */ |
|
color: #000; /* 选中标签文字颜色 */ |
|
} |
|
|
|
QTabBar::tab:!selected { |
|
margin-top: 2px; /* 未选中标签稍低 */ |
|
} |
|
|
|
/* Tab Bar下划线 (可选) */ |
|
/* QTabBar { |
|
border-bottom: 1px solid #c4c4c4; |
|
} */ |
|
|
|
/* 通用控件样式 */ |
|
QComboBox, QLineEdit, QPushButton { |
|
padding: 6px 10px; |
|
border: 1px solid #cccccc; |
|
border-radius: 4px; |
|
min-height: 20px; /* 保证最小高度 */ |
|
font-size: 10pt; /* 统一字体大小 */ |
|
} |
|
|
|
QLineEdit, QComboBox { |
|
background-color: white; |
|
} |
|
|
|
/* 按钮样式 */ |
|
QPushButton { |
|
background-color: #0078d4; /* 蓝色背景 */ |
|
color: white; |
|
font-weight: bold; |
|
min-width: 70px; /* 按钮最小宽度 */ |
|
} |
|
|
|
QPushButton:hover { |
|
background-color: #005a9e; |
|
} |
|
|
|
QPushButton:pressed { |
|
background-color: #003f6e; |
|
} |
|
|
|
QPushButton#delete_button { /* 可以为特定按钮设置样式,如果需要 */ |
|
background-color: #e74c3c; /* 红色删除按钮 */ |
|
} |
|
QPushButton#delete_button:hover { |
|
background-color: #c0392b; |
|
} |
|
|
|
/* 下拉框箭头 */ |
|
QComboBox::drop-down { |
|
border: none; |
|
padding-right: 5px; |
|
} |
|
QComboBox::down-arrow { |
|
image: url(:/qt-project.org/styles/commonstyle/images/standardbutton-down-arrow-16.png); /* 使用系统箭头 */ |
|
width: 12px; |
|
height: 12px; |
|
} |
|
|
|
/* 表格样式 */ |
|
QTableWidget { |
|
border: 1px solid #dcdcdc; |
|
gridline-color: #e0e0e0; |
|
selection-background-color: #a6d1f4; /* 选中行背景色 */ |
|
selection-color: black; /* 选中行文字颜色 */ |
|
alternate-background-color: #f9f9f9; /* 隔行变色 */ |
|
font-size: 10pt; |
|
} |
|
/* QTableWidget::item { |
|
padding: 4px; /* 单元格内边距 */ |
|
/* } */ |
|
|
|
/* 表头样式 */ |
|
QHeaderView::section { |
|
background-color: #e8e8e8; |
|
padding: 5px; |
|
border: 1px solid #dcdcdc; |
|
border-bottom: none; /* 移除表头底部边框 */ |
|
font-weight: bold; |
|
font-size: 10pt; |
|
} |
|
|
|
/* 滚动条美化 (可选,可能需要根据平台调整) */ |
|
QScrollBar:vertical { |
|
border: 1px solid #cccccc; |
|
background: #f0f0f0; |
|
width: 12px; |
|
margin: 0px 0px 0px 0px; |
|
} |
|
QScrollBar::handle:vertical { |
|
background: #c0c0c0; |
|
min-height: 20px; |
|
border-radius: 6px; |
|
} |
|
QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical { |
|
height: 0px; |
|
background: none; |
|
} |
|
QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical { |
|
background: none; |
|
} |
|
|
|
QScrollBar:horizontal { |
|
border: 1px solid #cccccc; |
|
background: #f0f0f0; |
|
height: 12px; |
|
margin: 0px 0px 0px 0px; |
|
} |
|
QScrollBar::handle:horizontal { |
|
background: #c0c0c0; |
|
min-width: 20px; |
|
border-radius: 6px; |
|
} |
|
QScrollBar::add-line:horizontal, QScrollBar::sub-line:horizontal { |
|
width: 0px; |
|
background: none; |
|
} |
|
QScrollBar::add-page:horizontal, QScrollBar::sub-page:horizontal { |
|
background: none; |
|
} |
|
|
|
""" |
|
self.setStyleSheet(stylesheet) |
|
self.logger.info("已应用自定义样式表") |
|
|
|
def load_ui(self): |
|
"""加载UI文件""" |
|
ui_path = os.path.join(current_dir, "ha_manage.ui") |
|
uic.loadUi(ui_path, self) |
|
|
|
def init_ui(self): |
|
"""初始化UI组件""" |
|
try: |
|
|
|
ui_path = os.path.join(current_dir, "ha_manage.ui") |
|
uic.loadUi(ui_path, self) |
|
|
|
|
|
self.device_table.verticalHeader().setVisible(False) |
|
self.device_table.horizontalHeader().setSectionResizeMode(0, QHeaderView.Stretch) |
|
self.device_table.horizontalHeader().setSectionResizeMode(1, QHeaderView.ResizeToContents) |
|
self.device_table.horizontalHeader().setSectionResizeMode(2, QHeaderView.ResizeToContents) |
|
self.device_table.horizontalHeader().setSectionResizeMode(3, QHeaderView.ResizeToContents) |
|
|
|
self.added_device_table.verticalHeader().setVisible(False) |
|
self.added_device_table.horizontalHeader().setSectionResizeMode(0, QHeaderView.Stretch) |
|
self.added_device_table.horizontalHeader().setSectionResizeMode(1, QHeaderView.ResizeToContents) |
|
self.added_device_table.horizontalHeader().setSectionResizeMode(2, QHeaderView.ResizeToContents) |
|
|
|
|
|
self._setup_navigation() |
|
|
|
|
|
self.search_input.textChanged.connect(self.filter_devices) |
|
|
|
|
|
self.domain_combo.clear() |
|
self.domain_mapping = {"全部": "all"} |
|
self.domain_combo.addItem("全部") |
|
domains = [ |
|
("light", "灯光 💡"), |
|
("switch", "开关 🔌"), |
|
("sensor", "传感器 🌡️"), |
|
("binary_sensor", "二元传感器 🔔"), |
|
("climate", "温控 ❄️"), |
|
("fan", "风扇 💨"), |
|
("cover", "窗帘 🪟"), |
|
("media_player", "媒体播放器 📺") |
|
] |
|
for domain_id, domain_name in domains: |
|
self.domain_mapping[domain_name] = domain_id |
|
self.domain_combo.addItem(domain_name) |
|
|
|
|
|
self.domain_combo.setCurrentIndex(0) |
|
|
|
|
|
self.domain_combo.currentTextChanged.connect(self.domain_changed) |
|
|
|
|
|
self.load_devices("all") |
|
|
|
except Exception as e: |
|
self.logger.error(f"初始化UI失败: {str(e)}") |
|
raise |
|
|
|
def _setup_navigation(self): |
|
"""设置导航栏 - 使用 QTabBar""" |
|
|
|
self.logger.info("开始设置导航栏 (QTabBar)") |
|
|
|
try: |
|
|
|
|
|
|
|
|
|
if not isinstance(self.nav_segment, QTabBar): |
|
|
|
self.logger.error("导航控件 'nav_segment' 不是 QTabBar 类型!") |
|
|
|
tab_bar = self.findChild(QTabBar) |
|
if tab_bar: |
|
self.nav_segment = tab_bar |
|
self.logger.warning("已自动查找并设置 QTabBar 实例。请确保 UI 文件中的名称一致。") |
|
else: |
|
QMessageBox.critical(self, "UI错误", "未能找到导航栏控件 (QTabBar)。请检查UI文件。") |
|
return |
|
|
|
|
|
|
|
|
|
|
|
while self.nav_segment.count() > 0: |
|
self.nav_segment.removeTab(0) |
|
|
|
self.nav_segment.addTab("可用设备") |
|
self.nav_segment.addTab("已添加设备") |
|
|
|
|
|
self._nav_keys = ["available", "added"] |
|
|
|
|
|
self.nav_segment.currentChanged.connect(self.on_page_changed_by_index) |
|
|
|
|
|
self.nav_segment.setCurrentIndex(0) |
|
self.logger.info("导航栏设置完成,默认选中索引 0 ('可用设备')") |
|
except Exception as e: |
|
self.logger.error(f"设置导航栏失败: {e}") |
|
|
|
QMessageBox.warning(self, "警告", f"导航栏设置失败: {e}") |
|
|
|
def connect_signals(self): |
|
"""连接信号槽""" |
|
|
|
self.domain_combo.currentTextChanged.connect(self.domain_changed) |
|
|
|
|
|
self.search_input.textChanged.connect(self.filter_devices) |
|
|
|
|
|
self.refresh_button.clicked.connect(self.refresh_devices) |
|
|
|
|
|
self.add_button.clicked.connect(self.add_selected_device) |
|
|
|
|
|
self.added_device_table.cellChanged.connect(self.on_prompt_edited) |
|
|
|
|
|
self.device_table.cellChanged.connect(self.on_available_device_prompt_edited) |
|
|
|
def on_page_changed_by_index(self, index: int): |
|
"""当 QTabBar 切换时调用""" |
|
try: |
|
routeKey = self._nav_keys[index] |
|
self.logger.info(f"切换到页面索引 {index}, key: {routeKey}") |
|
|
|
|
|
if routeKey == "available": |
|
self.stackedWidget.setCurrentIndex(0) |
|
elif routeKey == "added": |
|
self.stackedWidget.setCurrentIndex(1) |
|
self.reload_config() |
|
self.refresh_added_devices() |
|
else: |
|
self.logger.warning(f"未知的导航索引: {index}, key: {routeKey}") |
|
except IndexError: |
|
self.logger.error(f"导航索引越界: {index}") |
|
except Exception as e: |
|
self.logger.error(f"页面切换处理失败: {e}") |
|
|
|
def reload_config(self): |
|
"""重新从磁盘加载配置文件""" |
|
try: |
|
|
|
config_path = os.path.join(project_root, "config", "config.json") |
|
|
|
|
|
if not os.path.exists(config_path): |
|
self.logger.warning(f"配置文件不存在: {config_path}") |
|
return |
|
|
|
|
|
with open(config_path, "r", encoding="utf-8") as f: |
|
config_data = json.load(f) |
|
|
|
|
|
if "HOME_ASSISTANT" in config_data and "DEVICES" in config_data["HOME_ASSISTANT"]: |
|
self.added_devices = config_data["HOME_ASSISTANT"]["DEVICES"] |
|
self.logger.info(f"已从配置文件重新加载 {len(self.added_devices)} 个设备") |
|
else: |
|
self.added_devices = [] |
|
self.logger.warning("配置文件中未找到设备配置") |
|
|
|
except Exception as e: |
|
self.logger.error(f"重新加载配置文件失败: {e}") |
|
QMessageBox.warning(self, "警告", f"重新加载配置文件失败: {e}") |
|
|
|
def domain_changed(self): |
|
"""当域选择变化时调用""" |
|
current_text = self.domain_combo.currentText() |
|
domain = self.domain_mapping.get(current_text, "all") |
|
self.load_devices(domain) |
|
|
|
def load_devices(self, domain): |
|
"""加载设备列表""" |
|
|
|
self.search_input.clear() |
|
|
|
|
|
self.device_table.setRowCount(0) |
|
loading_row = self.device_table.rowCount() |
|
self.device_table.insertRow(loading_row) |
|
loading_item = QTableWidgetItem("正在加载设备...") |
|
loading_item.setTextAlignment(Qt.AlignCenter) |
|
self.device_table.setItem(loading_row, 0, loading_item) |
|
self.device_table.setSpan(loading_row, 0, 1, 4) |
|
|
|
|
|
if self.load_thread and self.load_thread.isRunning(): |
|
self.logger.info("等待上一个加载线程完成...") |
|
|
|
if not self.load_thread.wait(1000): |
|
self.logger.warning("上一个加载线程未在1秒内完成,强制终止") |
|
|
|
if self.load_thread in self.threads: |
|
self.threads.remove(self.load_thread) |
|
self.load_thread = None |
|
|
|
|
|
self.load_thread = DeviceLoadThread(self.ha_url, self.ha_token, domain) |
|
self.load_thread.devices_loaded.connect(self.update_device_table) |
|
self.load_thread.error_occurred.connect(self.show_error) |
|
self.load_thread.start() |
|
|
|
|
|
self.threads.append(self.load_thread) |
|
|
|
def update_device_table(self, devices): |
|
"""更新设备表格""" |
|
|
|
sender = self.sender() |
|
if sender in self.threads: |
|
self.threads.remove(sender) |
|
|
|
self.current_devices = devices |
|
self.device_table.setRowCount(0) |
|
|
|
if not devices: |
|
|
|
no_device_row = self.device_table.rowCount() |
|
self.device_table.insertRow(no_device_row) |
|
no_device_item = QTableWidgetItem("未找到设备") |
|
no_device_item.setTextAlignment(Qt.AlignCenter) |
|
self.device_table.setItem(no_device_row, 0, no_device_item) |
|
self.device_table.setSpan(no_device_row, 0, 1, 4) |
|
return |
|
|
|
|
|
for device in devices: |
|
row = self.device_table.rowCount() |
|
self.device_table.insertRow(row) |
|
|
|
|
|
friendly_name_item = QTableWidgetItem(device["friendly_name"]) |
|
|
|
self.device_table.setItem(row, 0, friendly_name_item) |
|
|
|
|
|
entity_id_item = QTableWidgetItem(device["entity_id"]) |
|
entity_id_item.setFlags(entity_id_item.flags() & ~Qt.ItemIsEditable) |
|
self.device_table.setItem(row, 1, entity_id_item) |
|
|
|
|
|
domain = device["domain"] |
|
domain_display = DOMAIN_ICONS.get(domain, domain) |
|
domain_item = QTableWidgetItem(domain_display) |
|
domain_item.setFlags(domain_item.flags() & ~Qt.ItemIsEditable) |
|
self.device_table.setItem(row, 2, domain_item) |
|
|
|
|
|
state = device["state"] |
|
state_item = QTableWidgetItem(state) |
|
state_item.setFlags(state_item.flags() & ~Qt.ItemIsEditable) |
|
self.device_table.setItem(row, 3, state_item) |
|
|
|
|
|
|
|
if any(d.get("entity_id") == device["entity_id"] for d in self.added_devices): |
|
for col in range(4): |
|
item = self.device_table.item(row, col) |
|
if item: |
|
item.setBackground(QColor(Qt.lightGray)) |
|
|
|
def refresh_devices(self): |
|
"""刷新设备列表""" |
|
current_text = self.domain_combo.currentText() |
|
domain = self.domain_mapping.get(current_text, "all") |
|
self.load_devices(domain) |
|
|
|
def filter_devices(self): |
|
"""根据搜索框过滤设备""" |
|
search_text = self.search_input.text().lower() |
|
|
|
for row in range(self.device_table.rowCount()): |
|
show_row = True |
|
|
|
if search_text: |
|
prompt = self.device_table.item(row, 0).text().lower() |
|
entity_id = self.device_table.item(row, 1).text().lower() |
|
|
|
show_row = search_text in prompt or search_text in entity_id |
|
|
|
self.device_table.setRowHidden(row, not show_row) |
|
|
|
def add_selected_device(self): |
|
"""添加选中的设备""" |
|
|
|
selected_indexes = self.device_table.selectedIndexes() |
|
if not selected_indexes: |
|
QMessageBox.warning(self, "警告", "请先选择一个设备") |
|
return |
|
|
|
|
|
|
|
row = selected_indexes[0].row() |
|
|
|
|
|
if row < 0 or row >= self.device_table.rowCount(): |
|
self.logger.warning(f"无效的选中行: {row}") |
|
return |
|
|
|
|
|
if self.device_table.item(row, 1) is None: |
|
self.logger.warning(f"选中的行不是有效的设备行: {row}") |
|
QMessageBox.warning(self, "警告", "请选择一个有效的设备行") |
|
return |
|
|
|
entity_id = self.device_table.item(row, 1).text() |
|
|
|
|
|
if any(d.get("entity_id") == entity_id for d in self.added_devices): |
|
QMessageBox.information(self, "提示", f"设备 {entity_id} 已添加") |
|
return |
|
|
|
|
|
friendly_name = self.custom_name_input.text().strip() or self.device_table.item(row, 0).text() |
|
|
|
|
|
self.save_device_to_config(entity_id, friendly_name) |
|
|
|
|
|
|
|
|
|
|
|
|
|
added_tab_index = self._nav_keys.index("added") |
|
if added_tab_index is not None: |
|
self.nav_segment.setCurrentIndex(added_tab_index) |
|
|
|
else: |
|
self.reload_config() |
|
self.refresh_added_devices() |
|
|
|
|
|
self.refresh_devices() |
|
|
|
|
|
self.custom_name_input.clear() |
|
|
|
def refresh_added_devices(self): |
|
"""刷新已添加设备表格""" |
|
|
|
|
|
|
|
try: |
|
self.added_device_table.cellChanged.disconnect(self.on_prompt_edited) |
|
except: |
|
pass |
|
|
|
|
|
self.added_device_table.setRowCount(0) |
|
|
|
|
|
if not self.added_devices: |
|
empty_row = self.added_device_table.rowCount() |
|
self.added_device_table.insertRow(empty_row) |
|
empty_item = QTableWidgetItem("未添加任何设备") |
|
empty_item.setTextAlignment(Qt.AlignCenter) |
|
self.added_device_table.setItem(empty_row, 0, empty_item) |
|
self.added_device_table.setSpan(empty_row, 0, 1, 3) |
|
|
|
self.added_device_table.cellChanged.connect(self.on_prompt_edited) |
|
return |
|
|
|
|
|
for device in self.added_devices: |
|
row = self.added_device_table.rowCount() |
|
self.added_device_table.insertRow(row) |
|
|
|
|
|
friendly_name = device.get("friendly_name", "") |
|
friendly_name_item = QTableWidgetItem(friendly_name) |
|
|
|
self.added_device_table.setItem(row, 0, friendly_name_item) |
|
|
|
|
|
entity_id = device.get("entity_id", "") |
|
entity_id_item = QTableWidgetItem(entity_id) |
|
entity_id_item.setFlags(entity_id_item.flags() & ~Qt.ItemIsEditable) |
|
self.added_device_table.setItem(row, 1, entity_id_item) |
|
|
|
|
|
delete_button = QPushButton("删除") |
|
delete_button.clicked.connect(lambda checked, r=row: self.delete_device(r)) |
|
self.added_device_table.setCellWidget(row, 2, delete_button) |
|
|
|
|
|
self.added_device_table.cellChanged.connect(self.on_prompt_edited) |
|
|
|
def delete_device(self, row): |
|
"""删除指定行的设备""" |
|
entity_id = self.added_device_table.item(row, 1).text() |
|
|
|
|
|
reply = QMessageBox.question( |
|
self, |
|
"确认删除", |
|
f"确定要删除设备 {entity_id} 吗?", |
|
QMessageBox.Yes | QMessageBox.No, |
|
QMessageBox.No |
|
) |
|
|
|
if reply == QMessageBox.Yes: |
|
|
|
success = self.delete_device_from_config(entity_id) |
|
|
|
if success: |
|
|
|
self.reload_config() |
|
|
|
|
|
self.refresh_added_devices() |
|
self.refresh_devices() |
|
|
|
def save_device_to_config(self, entity_id: str, friendly_name: Optional[str] = None) -> bool: |
|
"""将设备添加到配置文件中""" |
|
try: |
|
|
|
config_path = os.path.join(project_root, "config", "config.json") |
|
|
|
|
|
with open(config_path, "r", encoding="utf-8") as f: |
|
config = json.load(f) |
|
|
|
|
|
if "HOME_ASSISTANT" not in config: |
|
config["HOME_ASSISTANT"] = {} |
|
|
|
if "DEVICES" not in config["HOME_ASSISTANT"]: |
|
config["HOME_ASSISTANT"]["DEVICES"] = [] |
|
|
|
|
|
for device in config["HOME_ASSISTANT"]["DEVICES"]: |
|
if device.get("entity_id") == entity_id: |
|
|
|
if friendly_name and device.get("friendly_name") != friendly_name: |
|
device["friendly_name"] = friendly_name |
|
|
|
|
|
with open(config_path, "w", encoding="utf-8") as f: |
|
json.dump(config, f, ensure_ascii=False, indent=2) |
|
|
|
QMessageBox.information( |
|
self, |
|
"更新成功", |
|
f"设备 {entity_id} 的Prompt已更新为: {friendly_name}" |
|
) |
|
else: |
|
QMessageBox.information( |
|
self, |
|
"提示", |
|
f"设备 {entity_id} 已存在于配置中" |
|
) |
|
|
|
return True |
|
|
|
|
|
new_device = { |
|
"entity_id": entity_id |
|
} |
|
|
|
if friendly_name: |
|
new_device["friendly_name"] = friendly_name |
|
|
|
config["HOME_ASSISTANT"]["DEVICES"].append(new_device) |
|
|
|
|
|
with open(config_path, "w", encoding="utf-8") as f: |
|
json.dump(config, f, ensure_ascii=False, indent=2) |
|
|
|
QMessageBox.information( |
|
self, |
|
"添加成功", |
|
f"成功添加设备: {entity_id}" + (f" (Prompt: {friendly_name})" if friendly_name else "") |
|
) |
|
|
|
return True |
|
|
|
except Exception as e: |
|
QMessageBox.critical( |
|
self, |
|
"错误", |
|
f"保存配置失败: {e}" |
|
) |
|
return False |
|
|
|
def delete_device_from_config(self, entity_id: str) -> bool: |
|
"""从配置文件中删除设备""" |
|
try: |
|
|
|
config_path = os.path.join(project_root, "config", "config.json") |
|
|
|
|
|
with open(config_path, "r", encoding="utf-8") as f: |
|
config = json.load(f) |
|
|
|
|
|
if ("HOME_ASSISTANT" not in config or |
|
"DEVICES" not in config["HOME_ASSISTANT"]): |
|
QMessageBox.warning( |
|
self, |
|
"警告", |
|
"配置中不存在Home Assistant设备" |
|
) |
|
return False |
|
|
|
|
|
devices = config["HOME_ASSISTANT"]["DEVICES"] |
|
initial_count = len(devices) |
|
|
|
config["HOME_ASSISTANT"]["DEVICES"] = [ |
|
device for device in devices |
|
if device.get("entity_id") != entity_id |
|
] |
|
|
|
if len(config["HOME_ASSISTANT"]["DEVICES"]) < initial_count: |
|
|
|
with open(config_path, "w", encoding="utf-8") as f: |
|
json.dump(config, f, ensure_ascii=False, indent=2) |
|
|
|
QMessageBox.information( |
|
self, |
|
"删除成功", |
|
f"成功删除设备: {entity_id}" |
|
) |
|
return True |
|
else: |
|
QMessageBox.warning( |
|
self, |
|
"警告", |
|
f"未找到设备: {entity_id}" |
|
) |
|
return False |
|
|
|
except Exception as e: |
|
QMessageBox.critical( |
|
self, |
|
"错误", |
|
f"删除设备失败: {e}" |
|
) |
|
return False |
|
|
|
def show_error(self, error_message): |
|
"""显示错误消息""" |
|
|
|
sender = self.sender() |
|
if sender in self.threads: |
|
self.threads.remove(sender) |
|
|
|
self.device_table.setRowCount(0) |
|
error_row = self.device_table.rowCount() |
|
self.device_table.insertRow(error_row) |
|
error_item = QTableWidgetItem(f"加载失败: {error_message}") |
|
error_item.setTextAlignment(Qt.AlignCenter) |
|
self.device_table.setItem(error_row, 0, error_item) |
|
self.device_table.setSpan(error_row, 0, 1, 4) |
|
|
|
QMessageBox.critical( |
|
self, |
|
"错误", |
|
f"加载设备失败: {error_message}" |
|
) |
|
|
|
def on_prompt_edited(self, row, column): |
|
"""处理已添加设备Prompt编辑完成事件""" |
|
|
|
if column != 0: |
|
return |
|
|
|
entity_id = self.added_device_table.item(row, 1).text() |
|
new_prompt = self.added_device_table.item(row, 0).text() |
|
|
|
|
|
self.save_device_to_config(entity_id, new_prompt) |
|
|
|
def on_available_device_prompt_edited(self, row, column): |
|
"""处理可用设备Prompt编辑完成事件""" |
|
|
|
if column != 0: |
|
return |
|
|
|
|
|
new_prompt = self.device_table.item(row, 0).text() |
|
|
|
if row in [index.row() for index in self.device_table.selectedIndexes()]: |
|
self.custom_name_input.setText(new_prompt) |
|
self.logger.info(f"已更新自定义名称输入框: {new_prompt}") |
|
|
|
def main(): |
|
"""主函数""" |
|
app = QApplication(sys.argv) |
|
|
|
|
|
window = HomeAssistantDeviceManager() |
|
|
|
window.setMinimumSize(800, 480) |
|
|
|
window.resize(800, 480) |
|
window.show() |
|
|
|
sys.exit(app.exec_()) |
|
|
|
|
|
if __name__ == "__main__": |
|
main() |