impress_sig_mesh_hacs/custom_components/sigmesh_gateway/web_ui.py
impressionyang 4c3eb62dfb feat: 实现完整的 Web UI 配网管理功能
新增 Web UI 组件:
- web_ui.py: RESTful API 端点(状态、扫描、配网、分组、设备)
- sigmesh-gateway-panel.js: Lovelace Dashboard 自定义卡片
  - 设备扫描和发现
- 配网操作(开始/停止/绑定 App Key)
  - 分组管理(添加/移除)
  - 实时状态监控

配置更新:
- __init__.py: 集成 Web UI 和服务注册
- const.py: 添加服务常量定义
- services.py: 保留服务调用用于向后兼容
- README.md: 添加 Web UI 配置说明
- docs/UI 使用指南.md: 详细的 UI 使用文档

使用方式:
1. 配置 frontend.extra_module_url 加载 JS 面板
2. 在 Lovelace Dashboard 添加 custom:sigmesh-gateway-panel 卡片
3. 通过 UI 完成所有配网和分组操作

API 端点:
- GET /api/sigmesh_gateway/status - 获取配网状态
- POST /api/sigmesh_gateway/scan - 开始扫描
- POST /api/sigmesh_gateway/provisioning - 配网操作
- POST /api/sigmesh_gateway/group - 分组管理
- GET /api/sigmesh_gateway/devices - 设备列表
2026-04-16 13:41:28 +08:00

295 lines
10 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""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,
})