diff --git a/README.md b/README.md
index e7d5105..20ed444 100644
--- a/README.md
+++ b/README.md
@@ -107,6 +107,40 @@ sigmesh_gateway:
poll_interval: 30
```
+## Web UI 配网管理
+
+### 添加 Lovelace 面板
+
+1. **复制前端文件**
+ ```bash
+ mkdir -p /config/www/sigmesh_gateway
+ cp custom_components/sigmesh_gateway/sigmesh-gateway-panel.js /config/www/sigmesh_gateway/
+ ```
+
+2. **配置 frontend**
+
+ 在 `configuration.yaml` 中添加:
+ ```yaml
+ frontend:
+ extra_module_url:
+ - /local/sigmesh_gateway/sigmesh-gateway-panel.js
+ ```
+
+3. **重启 Home Assistant**
+
+4. **添加卡片到 Dashboard**
+ - 编辑仪表板 → 添加卡片 → 自定义卡片
+ - 输入:`custom:sigmesh-gateway-panel`
+
+### Web UI 功能
+
+- 📡 **设备扫描** - 扫描可用的 Bluetooth Mesh 设备
+- 🔐 **配网管理** - 开始/停止配网,绑定 App Key
+- 📋 **分组管理** - 添加/移除设备到组
+- 📊 **状态监控** - 实时查看配网状态和设备列表
+
+详细说明请参考:[UI 使用指南](docs/UI 使用指南.md)
+
## 串口连接
### 接线方式
diff --git a/custom_components/sigmesh_gateway/__init__.py b/custom_components/sigmesh_gateway/__init__.py
index b301d17..8cce11c 100644
--- a/custom_components/sigmesh_gateway/__init__.py
+++ b/custom_components/sigmesh_gateway/__init__.py
@@ -21,8 +21,18 @@ from .const import (
DEFAULT_NETWORK_KEY,
DOMAIN,
)
+from .const import (
+ SERVICE_START_SCAN,
+ SERVICE_STOP_PROVISIONING,
+ SERVICE_START_PROVISIONING,
+ SERVICE_BIND_APPKEY,
+ SERVICE_ADD_TO_GROUP,
+ SERVICE_REMOVE_FROM_GROUP,
+ SERVICE_SEND_VENDOR_COMMAND,
+)
from .coordinator import SigMeshGatewayCoordinator
from .serial_reader import SerialReader
+from .web_ui import setup_web_ui
from .services import setup_services
_LOGGER = logging.getLogger(__name__)
@@ -35,7 +45,7 @@ PLATFORMS: list[Platform] = [
Platform.DEVICE_TRACKER,
]
-# 全局协调器字典(用于服务调用)
+# 全局协调器字典(用于 Web UI 调用)
_coordinators: dict[str, SigMeshGatewayCoordinator] = {}
@@ -85,8 +95,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
# 注册全局协调器
_coordinators[entry.entry_id] = coordinator
- # 设置服务(仅第一次)
- if not hass.services.has_service(DOMAIN, "start_scan"):
+ # 设置 Web UI 和服务(仅第一次)
+ if len(_coordinators) == 1:
+ setup_web_ui(hass, _coordinators)
setup_services(hass, _coordinators)
# 启动协调器
@@ -127,13 +138,13 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
# 如果所有集成都已卸载,移除服务
if not _coordinators:
- hass.services.async_remove(DOMAIN, "start_scan")
- hass.services.async_remove(DOMAIN, "stop_provisioning")
- hass.services.async_remove(DOMAIN, "start_provisioning")
- hass.services.async_remove(DOMAIN, "bind_appkey")
- hass.services.async_remove(DOMAIN, "add_to_group")
- hass.services.async_remove(DOMAIN, "remove_from_group")
- hass.services.async_remove(DOMAIN, "send_vendor_command")
+ hass.services.async_remove(DOMAIN, SERVICE_START_SCAN)
+ hass.services.async_remove(DOMAIN, SERVICE_STOP_PROVISIONING)
+ hass.services.async_remove(DOMAIN, SERVICE_START_PROVISIONING)
+ hass.services.async_remove(DOMAIN, SERVICE_BIND_APPKEY)
+ hass.services.async_remove(DOMAIN, SERVICE_ADD_TO_GROUP)
+ hass.services.async_remove(DOMAIN, SERVICE_REMOVE_FROM_GROUP)
+ hass.services.async_remove(DOMAIN, SERVICE_SEND_VENDOR_COMMAND)
return unload_ok
diff --git a/custom_components/sigmesh_gateway/const.py b/custom_components/sigmesh_gateway/const.py
index 041a310..05f6446 100644
--- a/custom_components/sigmesh_gateway/const.py
+++ b/custom_components/sigmesh_gateway/const.py
@@ -177,3 +177,12 @@ SERIAL_EVENT_PREFIX = "+EVENT="
SERIAL_MESH_RECV = "+EVENT=MESH,recv"
SERIAL_PROV_DEVICE_JOINED = "+EVENT=PROV,device_joined"
SERIAL_PROV_DEVICE_LEFT = "+EVENT=PROV,device_left"
+
+# 服务名称
+SERVICE_START_SCAN = "start_scan"
+SERVICE_STOP_PROVISIONING = "stop_provisioning"
+SERVICE_START_PROVISIONING = "start_provisioning"
+SERVICE_BIND_APPKEY = "bind_appkey"
+SERVICE_ADD_TO_GROUP = "add_to_group"
+SERVICE_REMOVE_FROM_GROUP = "remove_from_group"
+SERVICE_SEND_VENDOR_COMMAND = "send_vendor_command"
diff --git a/custom_components/sigmesh_gateway/services.py b/custom_components/sigmesh_gateway/services.py
index d3e31ce..d871fe6 100644
--- a/custom_components/sigmesh_gateway/services.py
+++ b/custom_components/sigmesh_gateway/services.py
@@ -1,4 +1,7 @@
-"""SigMesh Gateway 服务定义。"""
+"""SigMesh Gateway 服务定义(保留用于向后兼容)。
+
+注意:配网功能已迁移到 Web UI,服务调用接口保留用于自动化脚本。
+"""
import voluptuous as vol
from homeassistant.helpers import config_validation as cv
@@ -55,6 +58,9 @@ SERVICE_SCHEMA_SEND_VENDOR_COMMAND = {
def setup_services(hass, coordinators: dict) -> None:
"""设置 HA 服务。
+ 注意:配网功能已迁移到 Web UI,服务调用接口保留用于自动化脚本。
+ 推荐使用 Web UI 进行配网和分组管理操作。
+
Args:
hass: HomeAssistant 实例
coordinators: 协调器字典 {entry_id: coordinator}
diff --git a/custom_components/sigmesh_gateway/sigmesh-gateway-panel.js b/custom_components/sigmesh_gateway/sigmesh-gateway-panel.js
new file mode 100644
index 0000000..ddfc427
--- /dev/null
+++ b/custom_components/sigmesh_gateway/sigmesh-gateway-panel.js
@@ -0,0 +1,550 @@
+/**
+ * SigMesh Gateway 配网控制面板
+ *
+ * 这是一个自定义 Lovelace 卡片,用于管理 SigMesh 网关的配网和分组功能
+ *
+ * 使用方法:
+ * 1. 将此文件保存到 HA 的 www/community/sigmesh_gateway/ 目录
+ * 2. 在 HA 的 configuration.yaml 中添加:
+ * frontend:
+ * extra_module_url:
+ * - /local/community/sigmesh_gateway/sigmesh-gateway-panel.js
+ * 3. 在 Lovelace Dashboard 中添加自定义卡片
+ */
+
+// 配置常量
+const API_BASE = '/api/sigmesh_gateway';
+const POLL_INTERVAL = 3000; // 3 秒轮询一次
+
+/**
+ * 主面板组件
+ */
+class SigMeshGatewayPanel extends HTMLElement {
+ constructor() {
+ super();
+ this._hass = null;
+ this._state = 'idle';
+ this._devices = [];
+ this._groups = [];
+ this._pollTimer = null;
+ this._selectedDevice = null;
+ this._groupAddress = '0xC001';
+ }
+
+ set hass(hass) {
+ this._hass = hass;
+ }
+
+ connectedCallback() {
+ this._startPolling();
+ }
+
+ disconnectedCallback() {
+ this._stopPolling();
+ }
+
+ _startPolling() {
+ this._fetchStatus();
+ this._pollTimer = setInterval(() => this._fetchStatus(), POLL_INTERVAL);
+ }
+
+ _stopPolling() {
+ if (this._pollTimer) {
+ clearInterval(this._pollTimer);
+ this._pollTimer = null;
+ }
+ }
+
+ async _fetchStatus() {
+ try {
+ const response = await fetch(`${API_BASE}/status`, {
+ headers: {
+ 'Authorization': `Bearer ${this._hass.auth.data.access_token}`,
+ 'Content-Type': 'application/json',
+ },
+ });
+ const data = await response.json();
+ this._state = data.state;
+ this._devices = data.devices || [];
+ this._groups = data.groups || [];
+ this._render();
+ } catch (error) {
+ console.error('获取状态失败:', error);
+ }
+ }
+
+ _render() {
+ this.innerHTML = `
+
+
+
+
+ ${this._renderScanSection()}
+ ${this._renderDeviceList()}
+ ${this._renderProvActions()}
+ ${this._renderGroupManagement()}
+ ${this._renderGroupList()}
+ `;
+
+ this._attachEventListeners();
+ }
+
+ _getStateColor() {
+ const colors = {
+ idle: '#4caf50',
+ scanning: '#2196f3',
+ prov_starting: '#ff9800',
+ prov_in_progress: '#ff9800',
+ prov_completed: '#4caf50',
+ prov_failed: '#f44336',
+ timeout: '#f44336',
+ };
+ return colors[this._state] || '#757575';
+ }
+
+ _renderScanSection() {
+ const isScanning = this._state === 'scanning';
+ return `
+
+
设备扫描
+
+
+
+
+
+ `;
+ }
+
+ _renderDeviceList() {
+ if (this._devices.length === 0) {
+ return `
+
+ `;
+ }
+
+ return `
+
+
已发现设备 (${this._devices.length})
+
+ ${this._devices.map((device, index) => `
+
+
+
设备 ${device.mac}
+
+ 元素数:${device.elements} |
+ 地址:${device.address || '未分配'}
+
+
+
+ `).join('')}
+
+
+ `;
+ }
+
+ _renderProvActions() {
+ if (!this._selectedDevice && this._selectedDevice !== 0) {
+ return '';
+ }
+
+ const device = this._devices[this._selectedDevice];
+ const isProvInProgress = this._state === 'prov_in_progress' || this._state === 'prov_starting';
+
+ return `
+
+
配网操作 - ${device?.mac}
+
+
+
+
+
+
+
+
+
+ `;
+ }
+
+ _renderGroupManagement() {
+ if (!this._selectedDevice && this._selectedDevice !== 0) {
+ return '';
+ }
+
+ return `
+
+ `;
+ }
+
+ _renderGroupList() {
+ if (this._groups.length === 0) {
+ return `
+
+ `;
+ }
+
+ return `
+
+
组配置 (${this._groups.length})
+
+ ${this._groups.map(group => `
+
+ 组地址:${group.address}
+ 元素:${group.element_address}
+ Model: ${group.model_id}
+
+ `).join('')}
+
+
+ `;
+ }
+
+ _attachEventListeners() {
+ // 扫描按钮
+ this.querySelector('#btn-scan')?.addEventListener('click', () => this._handleScan());
+ this.querySelector('#btn-refresh')?.addEventListener('click', () => this._fetchStatus());
+
+ // 设备选择
+ this.querySelectorAll('.device-item').forEach(item => {
+ item.addEventListener('click', () => {
+ const index = parseInt(item.dataset.index);
+ this._selectedDevice = index === this._selectedDevice ? null : index;
+ this._render();
+ });
+ });
+
+ // 配网按钮
+ this.querySelector('#btn-prov-start')?.addEventListener('click', () => this._handleProvStart());
+ this.querySelector('#btn-prov-stop')?.addEventListener('click', () => this._handleProvStop());
+ this.querySelector('#btn-bind-key')?.addEventListener('click', () => this._handleBindKey());
+
+ // 分组按钮
+ this.querySelector('#btn-add-group')?.addEventListener('click', () => this._handleAddGroup());
+ this.querySelector('#btn-remove-group')?.addEventListener('click', () => this._handleRemoveGroup());
+ }
+
+ async _handleScan() {
+ try {
+ await fetch(`${API_BASE}/scan`, {
+ method: 'POST',
+ headers: {
+ 'Authorization': `Bearer ${this._hass.auth.data.access_token}`,
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({}),
+ });
+ this._fetchStatus();
+ } catch (error) {
+ console.error('扫描失败:', error);
+ }
+ }
+
+ async _handleProvStart() {
+ const deviceAddress = this.querySelector('#device-address')?.value;
+ try {
+ await fetch(`${API_BASE}/provisioning`, {
+ method: 'POST',
+ headers: {
+ 'Authorization': `Bearer ${this._hass.auth.data.access_token}`,
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({
+ action: 'start',
+ device_address: deviceAddress,
+ }),
+ });
+ this._fetchStatus();
+ } catch (error) {
+ console.error('配网失败:', error);
+ }
+ }
+
+ async _handleProvStop() {
+ try {
+ await fetch(`${API_BASE}/provisioning`, {
+ method: 'POST',
+ headers: {
+ 'Authorization': `Bearer ${this._hass.auth.data.access_token}`,
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({ action: 'stop' }),
+ });
+ this._fetchStatus();
+ } catch (error) {
+ console.error('停止配网失败:', error);
+ }
+ }
+
+ async _handleBindKey() {
+ const deviceAddress = this.querySelector('#device-address')?.value;
+ const elementAddress = 0;
+ try {
+ await fetch(`${API_BASE}/provisioning`, {
+ method: 'POST',
+ headers: {
+ 'Authorization': `Bearer ${this._hass.auth.data.access_token}`,
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({
+ action: 'bind',
+ device_address: deviceAddress,
+ element_address: elementAddress,
+ }),
+ });
+ alert('App Key 绑定成功');
+ this._fetchStatus();
+ } catch (error) {
+ console.error('绑定 App Key 失败:', error);
+ alert('绑定失败:' + error.message);
+ }
+ }
+
+ async _handleAddGroup() {
+ const targetAddress = this.querySelector('#target-address')?.value;
+ const groupAddress = this.querySelector('#group-address')?.value;
+ const modelId = parseInt(this.querySelector('#model-id')?.value || '4352');
+
+ try {
+ await fetch(`${API_BASE}/group`, {
+ method: 'POST',
+ headers: {
+ 'Authorization': `Bearer ${this._hass.auth.data.access_token}`,
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({
+ action: 'add',
+ target_address: targetAddress,
+ group_address: groupAddress,
+ model_id: modelId,
+ is_sig: true,
+ }),
+ });
+ this._groupAddress = groupAddress;
+ this._fetchStatus();
+ } catch (error) {
+ console.error('添加分组失败:', error);
+ }
+ }
+
+ async _handleRemoveGroup() {
+ const targetAddress = this.querySelector('#target-address')?.value;
+ const groupAddress = this.querySelector('#group-address')?.value;
+ const modelId = parseInt(this.querySelector('#model-id')?.value || '4352');
+
+ try {
+ await fetch(`${API_BASE}/group`, {
+ method: 'POST',
+ headers: {
+ 'Authorization': `Bearer ${this._hass.auth.data.access_token}`,
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({
+ action: 'remove',
+ target_address: targetAddress,
+ group_address: groupAddress,
+ model_id: modelId,
+ is_sig: true,
+ }),
+ });
+ this._fetchStatus();
+ } catch (error) {
+ console.error('移除分组失败:', error);
+ }
+ }
+}
+
+// 注册自定义元素
+customElements.define('sigmesh-gateway-panel', SigMeshGatewayPanel);
+
+// 导出到 window 供调试使用
+window.SigMeshGatewayPanel = SigMeshGatewayPanel;
+
+console.log('SigMesh Gateway Panel 已加载');
diff --git a/custom_components/sigmesh_gateway/web_ui.py b/custom_components/sigmesh_gateway/web_ui.py
new file mode 100644
index 0000000..d90f0e5
--- /dev/null
+++ b/custom_components/sigmesh_gateway/web_ui.py
@@ -0,0 +1,294 @@
+"""SigMesh Gateway Web UI 面板."""
+
+from __future__ import annotations
+
+import logging
+from typing import Any
+
+import voluptuous as vol
+from aiohttp import web
+from homeassistant.components.http import HomeAssistantView
+from homeassistant.core import HomeAssistant
+from homeassistant.helpers import config_validation as cv
+
+from .const import DOMAIN
+from .coordinator import SigMeshGatewayCoordinator
+
+_LOGGER = logging.getLogger(__name__)
+
+
+def setup_web_ui(hass: HomeAssistant, coordinators: dict[str, SigMeshGatewayCoordinator]) -> None:
+ """设置 Web UI 路由。
+
+ Args:
+ hass: Home Assistant 实例
+ coordinators: 协调器字典
+ """
+
+ # 注册 Web 视图
+ hass.http.register_view(SigMeshGatewayStatusView(coordinators))
+ hass.http.register_view(SigMeshGatewayScanView(coordinators))
+ hass.http.register_view(SigMeshGatewayProvView(coordinators))
+ hass.http.register_view(SigMeshGatewayGroupView(coordinators))
+ hass.http.register_view(SigMeshGatewayDevicesView(coordinators))
+
+
+class SigMeshGatewayStatusView(HomeAssistantView):
+ """获取配网状态视图."""
+
+ requires_auth = True
+ url = "/api/sigmesh_gateway/status"
+ name = "api:sigmesh_gateway:status"
+
+ def __init__(self, coordinators: dict[str, SigMeshGatewayCoordinator]) -> None:
+ self.coordinators = coordinators
+
+ async def get(self, request: web.Request) -> web.Response:
+ """获取配网状态。
+
+ 返回示例:
+ {
+ "state": "idle",
+ "devices": [
+ {"mac": "AA:BB:CC:DD:EE:FF", "elements": 2, "address": "0001"}
+ ],
+ "groups": [
+ {"address": "C001", "devices": ["0001"], "model_id": 4352}
+ ]
+ }
+ """
+ result = {
+ "state": "idle",
+ "devices": [],
+ "groups": [],
+ }
+
+ for coordinator in self.coordinators.values():
+ prov_devices = coordinator.get_prov_devices()
+ result["devices"].extend([
+ {
+ "mac": dev.mac_address,
+ "elements": dev.element_count,
+ "address": hex(dev.unicast_address) if dev.unicast_address else None,
+ }
+ for dev in prov_devices.values()
+ ])
+
+ # 获取组配置
+ for group_addr, configs in coordinator.prov_manager._group_configs.items():
+ result["groups"].extend([
+ {
+ "address": hex(group_addr),
+ "element_address": hex(cfg.element_address),
+ "model_id": hex(cfg.model_id),
+ }
+ for cfg in configs
+ ])
+
+ if coordinator.prov_state.value != "idle":
+ result["state"] = coordinator.prov_state.value
+
+ return self.json(result)
+
+
+class SigMeshGatewayScanView(HomeAssistantView):
+ """开始扫描设备视图."""
+
+ requires_auth = True
+ url = "/api/sigmesh_gateway/scan"
+ name = "api:sigmesh_gateway:scan"
+
+ def __init__(self, coordinators: dict[str, SigMeshGatewayCoordinator]) -> None:
+ self.coordinators = coordinators
+
+ async def post(self, request: web.Request) -> web.Response:
+ """开始扫描设备。
+
+ 请求体: {} 或 {"timeout": 60}
+ """
+ try:
+ data = await request.json() if request.can_read_body else {}
+ timeout = data.get("timeout", 60)
+
+ for coordinator in self.coordinators.values():
+ await coordinator.start_scanning()
+
+ return self.json({
+ "success": True,
+ "message": f"开始扫描设备,超时时间:{timeout}秒",
+ })
+ except Exception as e:
+ _LOGGER.error("扫描设备失败:%s", e)
+ return self.json({"success": False, "error": str(e)}, status=400)
+
+
+class SigMeshGatewayProvView(HomeAssistantView):
+ """配网操作视图."""
+
+ requires_auth = True
+ url = "/api/sigmesh_gateway/provisioning"
+ name = "api:sigmesh_gateway:provisioning"
+
+ def __init__(self, coordinators: dict[str, SigMeshGatewayCoordinator]) -> None:
+ self.coordinators = coordinators
+
+ async def post(self, request: web.Request) -> web.Response:
+ """开始配网设备。
+
+ 请求体:
+ {
+ "action": "start", // start, stop, bind
+ "device_address": "001A",
+ "element_address": 0, // 可选,bind 时需要
+ "timeout": 180 // 可选,超时时间(秒)
+ }
+ """
+ try:
+ data = await request.json() if request.can_read_body else {}
+ action = data.get("action")
+ device_address = data.get("device_address")
+ element_address = data.get("element_address", 0)
+ timeout = data.get("timeout", 180)
+
+ if action == "start":
+ if not device_address:
+ return self.json({"success": False, "error": "需要设备地址"}, status=400)
+ for coordinator in self.coordinators.values():
+ await coordinator.start_provisioning(device_address)
+ return self.json({
+ "success": True,
+ "message": f"开始配网设备 {device_address},超时时间:{timeout}秒",
+ })
+ elif action == "stop":
+ for coordinator in self.coordinators.values():
+ await coordinator.stop_provisioning()
+ return self.json({"success": True, "message": "停止配网"})
+ elif action == "bind":
+ if not device_address:
+ return self.json({"success": False, "error": "需要设备地址"}, status=400)
+ for coordinator in self.coordinators.values():
+ await coordinator.bind_app_key(device_address, element_address)
+ return self.json({
+ "success": True,
+ "message": f"绑定 App Key: {device_address}",
+ })
+ else:
+ return self.json({"success": False, "error": "未知操作"}, status=400)
+
+ except Exception as e:
+ _LOGGER.error("配网操作失败:%s", e)
+ return self.json({"success": False, "error": str(e)}, status=400)
+
+
+class SigMeshGatewayGroupView(HomeAssistantView):
+ """分组管理视图."""
+
+ requires_auth = True
+ url = "/api/sigmesh_gateway/group"
+ name = "api:sigmesh_gateway:group"
+
+ def __init__(self, coordinators: dict[str, SigMeshGatewayCoordinator]) -> None:
+ self.coordinators = coordinators
+
+ async def post(self, request: web.Request) -> web.Response:
+ """管理设备分组。
+
+ 请求体:
+ {
+ "action": "add", // add, remove
+ "target_address": "001A",
+ "element_address": 0,
+ "group_address": "C001",
+ "model_id": 4352,
+ "is_sig": true
+ }
+ """
+ try:
+ data = await request.json() if request.can_read_body else {}
+ action = data.get("action")
+ target_address = data.get("target_address")
+ element_address = data.get("element_address", 0)
+ group_address = data.get("group_address")
+ model_id = data.get("model_id", 4352)
+ is_sig = data.get("is_sig", True)
+
+ # 解析组地址
+ if isinstance(group_address, str):
+ group_address = int(group_address, 16)
+
+ if action == "add":
+ for coordinator in self.coordinators.values():
+ await coordinator.add_device_to_group(
+ target_address, element_address, group_address, model_id, is_sig
+ )
+ return self.json({
+ "success": True,
+ "message": f"添加设备 {target_address} 到组 {hex(group_address)}",
+ })
+ elif action == "remove":
+ for coordinator in self.coordinators.values():
+ await coordinator.remove_device_from_group(
+ target_address, element_address, group_address, model_id, is_sig
+ )
+ return self.json({
+ "success": True,
+ "message": f"从组 {hex(group_address)} 移除设备 {target_address}",
+ })
+ else:
+ return self.json({"success": False, "error": "未知操作"}, status=400)
+
+ except Exception as e:
+ _LOGGER.error("分组操作失败:%s", e)
+ return self.json({"success": False, "error": str(e)}, status=400)
+
+
+class SigMeshGatewayDevicesView(HomeAssistantView):
+ """获取设备列表视图."""
+
+ requires_auth = True
+ url = "/api/sigmesh_gateway/devices"
+ name = "api:sigmesh_gateway:devices"
+
+ def __init__(self, coordinators: dict[str, SigMeshGatewayCoordinator]) -> None:
+ self.coordinators = coordinators
+
+ async def get(self, request: web.Request) -> web.Response:
+ """获取所有设备列表。
+
+ 返回示例:
+ {
+ "prov_devices": [...],
+ "mesh_devices": [...]
+ }
+ """
+ prov_devices = []
+ mesh_devices = []
+
+ for coordinator in self.coordinators.values():
+ # 获取配网设备
+ prov_devices.extend([
+ {
+ "mac": dev.mac_address,
+ "elements": dev.element_count,
+ "unicast_address": dev.unicast_address,
+ "joined_at": dev.joined_at.isoformat() if dev.joined_at else None,
+ }
+ for dev in coordinator.get_prov_devices().values()
+ ])
+
+ # 获取 Mesh 设备状态
+ if coordinator.data:
+ mesh_devices.extend([
+ {
+ "mac": mac,
+ "model_id": hex(device.model_id) if device.model_id else None,
+ "states": device.states,
+ "last_update": device.last_update,
+ }
+ for mac, device in coordinator.data.items()
+ ])
+
+ return self.json({
+ "prov_devices": prov_devices,
+ "mesh_devices": mesh_devices,
+ })
diff --git a/docs/UI 使用指南.md b/docs/UI 使用指南.md
new file mode 100644
index 0000000..a328dc6
--- /dev/null
+++ b/docs/UI 使用指南.md
@@ -0,0 +1,227 @@
+# SigMesh Gateway UI 使用指南
+
+## 概述
+
+SigMesh Gateway 集成提供完整的 Web UI 界面,所有配网和分组管理功能都可以通过 UI 完成,无需使用服务调用或命令行。
+
+## UI 访问方式
+
+### 方式 1: Lovelace Dashboard 卡片(推荐)
+
+将 SigMesh Gateway 面板添加到 Lovelace Dashboard:
+
+1. **复制前端文件**
+ ```bash
+ # 在 HAOS 上执行
+ mkdir -p /config/www/sigmesh_gateway
+ cp -r custom_components/sigmesh_gateway/sigmesh-gateway-panel.js /config/www/sigmesh_gateway/
+ ```
+
+2. **添加前端模块**
+
+ 在 `configuration.yaml` 中添加:
+ ```yaml
+ frontend:
+ extra_module_url:
+ - /local/sigmesh_gateway/sigmesh-gateway-panel.js
+ ```
+
+3. **重启 Home Assistant**
+
+4. **添加卡片到 Dashboard**
+ - 打开 Lovelace Dashboard
+ - 点击右下角"编辑仪表板"
+ - 点击"+"添加卡片
+ - 选择"自定义卡片"
+ - 输入卡片类型:`custom:sigmesh-gateway-panel`
+
+### 方式 2: 直接访问 API
+
+配网管理 API 端点:
+
+| 端点 | 方法 | 功能 |
+|------|------|------|
+| `/api/sigmesh_gateway/status` | GET | 获取配网状态和设备列表 |
+| `/api/sigmesh_gateway/scan` | POST | 开始扫描设备 |
+| `/api/sigmesh_gateway/provisioning` | POST | 配网操作(开始/停止/绑定) |
+| `/api/sigmesh_gateway/group` | POST | 分组管理(添加/移除) |
+| `/api/sigmesh_gateway/devices` | GET | 获取所有设备详情 |
+
+## UI 界面说明
+
+### 主界面布局
+
+```
+┌─────────────────────────────────────────────┐
+│ SigMesh Gateway 配网管理 [状态徽章] │
+├─────────────────────────────────────────────┤
+│ 设备扫描 │
+│ [开始扫描] [刷新设备列表] │
+├─────────────────────────────────────────────┤
+│ 已发现设备 (3) │
+│ ┌─────────────────────────────────────┐ │
+│ │ 设备 AA:BB:CC:DD:EE:FF │ │
+│ │ 元素数:2 | 地址:未分配 │ │
+│ └─────────────────────────────────────┘ │
+│ ┌─────────────────────────────────────┐ │
+│ │ 设备 11:22:33:44:55:66 │ │
+│ │ 元素数:1 | 地址:0001 │ │
+│ └─────────────────────────────────────┘ │
+├─────────────────────────────────────────────┤
+│ 配网操作 - AA:BB:CC:DD:EE:FF │
+│ [设备地址输入框] │
+│ [开始配网] [停止配网] [绑定 App Key] │
+├─────────────────────────────────────────────┤
+│ 分组管理 │
+│ [目标地址] [组地址 C001] [Model ID 4352] │
+│ [添加到组] [从组移除] │
+├─────────────────────────────────────────────┤
+│ 组配置 (2) │
+│ 组地址:0xC001 | 元素:0000 | Model: 0x1100│
+│ 组地址:0xC002 | 元素:0000 | Model: 0x1000│
+└─────────────────────────────────────────────┘
+```
+
+### 状态说明
+
+| 状态 | 颜色 | 说明 |
+|------|------|------|
+| idle | 绿色 | 空闲,无配网操作 |
+| scanning | 蓝色 | 正在扫描设备 |
+| prov_starting | 橙色 | 配网启动中 |
+| prov_in_progress | 橙色 | 配网进行中 |
+| prov_completed | 绿色 | 配网完成 |
+| prov_failed | 红色 | 配网失败 |
+| timeout | 红色 | 配网超时 |
+
+## 操作步骤
+
+### 1. 扫描设备
+
+1. 点击"开始扫描"按钮
+2. 等待设备上报(扫描中的设备需要处于配网模式)
+3. 扫描到的设备会显示在"已发现设备"列表中
+
+### 2. 配网设备
+
+1. 在"已发现设备"列表中点击要配网的设备(选中后高亮)
+2. 点击"开始配网"按钮
+3. 等待配网完成(状态变为 prov_completed)
+4. 可选:点击"绑定 App Key"完成密钥绑定
+
+### 3. 添加到组
+
+1. 选中要配置的设备
+2. 在"分组管理"区域配置:
+ - 目标地址:自动填充为选中设备的地址
+ - 组地址:输入组地址(如 C001)
+ - Model ID:输入设备的 Model ID
+3. 点击"添加到组"按钮
+
+### 4. 从组移除
+
+1. 选中要配置的设备
+2. 配置组地址和 Model ID
+3. 点击"从组移除"按钮
+
+## API 使用示例
+
+### 获取状态
+
+```bash
+curl -X GET \
+ -H "Authorization: Bearer YOUR_TOKEN" \
+ http://localhost:8123/api/sigmesh_gateway/status
+```
+
+响应示例:
+```json
+{
+ "state": "idle",
+ "devices": [
+ {"mac": "AA:BB:CC:DD:EE:FF", "elements": 2, "address": null}
+ ],
+ "groups": [
+ {"address": "0xc001", "element_address": "0x0", "model_id": "0x1100"}
+ ]
+}
+```
+
+### 开始扫描
+
+```bash
+curl -X POST \
+ -H "Authorization: Bearer YOUR_TOKEN" \
+ -H "Content-Type: application/json" \
+ -d '{}' \
+ http://localhost:8123/api/sigmesh_gateway/scan
+```
+
+### 开始配网
+
+```bash
+curl -X POST \
+ -H "Authorization: Bearer YOUR_TOKEN" \
+ -H "Content-Type: application/json" \
+ -d '{"action": "start", "device_address": "001A"}' \
+ http://localhost:8123/api/sigmesh_gateway/provisioning
+```
+
+### 添加到组
+
+```bash
+curl -X POST \
+ -H "Authorization: Bearer YOUR_TOKEN" \
+ -H "Content-Type: application/json" \
+ -d '{
+ "action": "add",
+ "target_address": "001A",
+ "group_address": "C001",
+ "model_id": 4352,
+ "is_sig": true
+ }' \
+ http://localhost:8123/api/sigmesh_gateway/group
+```
+
+## 故障排查
+
+### UI 不显示
+
+**检查步骤**:
+1. 确认 JS 文件已复制到正确位置
+2. 检查 configuration.yaml 配置
+3. 清除浏览器缓存
+4. 查看浏览器控制台错误
+
+### API 返回 401
+
+**原因**: 认证失败
+
+**解决**:
+1. 确保使用有效的 access token
+2. 在 HA Profile 页面生成新的 long-lived token
+
+### 配网超时
+
+**可能原因**:
+- 设备未进入配网模式
+- 网络密钥配置不正确
+
+**解决**:
+1. 参考设备说明书将设备置于配网模式
+2. 检查集成配置中的 Network Key
+
+## 配置检查清单
+
+配网前确认以下配置:
+
+- [ ] 串口连接正常
+- [ ] Network Key 配置正确(32 字符十六进制)
+- [ ] App Key 配置正确(32 字符十六进制)
+- [ ] 设备已进入配网模式
+- [ ] 组地址在有效范围(0xC000-0xCFFF)
+
+---
+
+**文档版本**: 1.0
+**最后更新**: 2026-04-16