feat: 添加配网和分组管理功能
新增功能: - 配网管理模块 (provisioning.py): 支持设备扫描、配网、超时处理 - 配网配置步骤: UI 配置流程增加配网参数配置(Network Key, App Key 等) - 分组管理:支持 SIG 分组和 VENDOR 分组的加入/删除操作 - HA 服务调用:7 个配网和分组相关的服务 文件变更: - const.py: 添加配网相关常量(CONF_NETWORK_KEY, PROV_TIMEOUT 等) - config_flow.py: 增加 prov_config 配置步骤和 OptionsFlow 菜单 - provisioning.py: 新建配网管理器(ProvisioningManager 类) - coordinator.py: 集成配网管理器,添加配网状态管理方法 - services.py: 新建服务定义和注册 - services.yaml: HA 服务定义文件 - __init__.py: 集成服务注册和卸载 - PRD.md: 更新服务调用接口和配置参数文档 配网功能说明: - 首次使用需配置 Network Key, App Key, Network ID, IV Index - 配网超时时间:180 秒 - 组地址范围:0xC000 - 0xCFFF - 支持 SIG 标准分组和 VENDOR 自定义分组
This commit is contained in:
parent
f1f2c77af4
commit
d21e7f1b3f
95
PRD.md
95
PRD.md
@ -569,21 +569,91 @@ class SigMeshGatewayCoordinator:
|
||||
async def async_request_refresh() -> None
|
||||
```
|
||||
|
||||
### 8.2 服务调用接口(TODO)
|
||||
### 8.2 服务调用接口
|
||||
|
||||
已实现的服务调用接口:
|
||||
|
||||
```yaml
|
||||
# service.yaml
|
||||
sigmesh_gateway.send_command:
|
||||
# sigmesh_gateway.start_scan - 开始扫描设备
|
||||
start_scan:
|
||||
name: 开始扫描设备
|
||||
description: 开始扫描可用的 Bluetooth Mesh 设备
|
||||
|
||||
# sigmesh_gateway.stop_provisioning - 停止配网
|
||||
stop_provisioning:
|
||||
name: 停止配网
|
||||
description: 停止当前的配网操作
|
||||
|
||||
# sigmesh_gateway.start_provisioning - 开始配网
|
||||
start_provisioning:
|
||||
fields:
|
||||
device_id:
|
||||
description: 设备 ID
|
||||
example: "AA:BB:CC:DD:EE:FF"
|
||||
device_address:
|
||||
description: 要配网的设备地址(16 进制字符串)
|
||||
example: "001A"
|
||||
|
||||
# sigmesh_gateway.bind_appkey - 绑定 App Key
|
||||
bind_appkey:
|
||||
fields:
|
||||
device_address:
|
||||
description: 设备地址
|
||||
example: "001A"
|
||||
element_address:
|
||||
description: 元素地址(默认为 0)
|
||||
example: 0
|
||||
|
||||
# sigmesh_gateway.add_to_group - 添加到组
|
||||
add_to_group:
|
||||
fields:
|
||||
target_address:
|
||||
description: 目标设备地址
|
||||
example: "001A"
|
||||
element_address:
|
||||
description: 元素地址
|
||||
example: 0
|
||||
group_address:
|
||||
description: 组地址(建议使用 0xC000 以上)
|
||||
example: "C001"
|
||||
model_id:
|
||||
description: Model ID
|
||||
example: 4352
|
||||
is_sig:
|
||||
description: 是否为 SIG 标准分组
|
||||
example: true
|
||||
|
||||
# sigmesh_gateway.remove_from_group - 从组移除
|
||||
remove_from_group:
|
||||
fields:
|
||||
target_address:
|
||||
description: 目标设备地址
|
||||
example: "001A"
|
||||
element_address:
|
||||
description: 元素地址
|
||||
example: 0
|
||||
group_address:
|
||||
description: 组地址
|
||||
example: "C001"
|
||||
model_id:
|
||||
description: Model ID
|
||||
example: 4352
|
||||
is_sig:
|
||||
description: 是否为 SIG 标准分组
|
||||
example: true
|
||||
|
||||
# sigmesh_gateway.send_vendor_command - 发送 VENDOR 命令
|
||||
send_vendor_command:
|
||||
fields:
|
||||
target_address:
|
||||
description: 目标设备地址
|
||||
example: "001A"
|
||||
element_address:
|
||||
description: 元素地址
|
||||
example: 0
|
||||
opcode:
|
||||
description: 操作码
|
||||
example: "0x8202"
|
||||
description: VENDOR 操作码
|
||||
example: "1102"
|
||||
payload:
|
||||
description: 数据负载
|
||||
example: "01"
|
||||
description: 数据负载(16 进制)
|
||||
example: "0000"
|
||||
```
|
||||
|
||||
---
|
||||
@ -596,6 +666,11 @@ sigmesh_gateway.send_command:
|
||||
|------|------|------|--------|------|
|
||||
| serial_device | string | 是 | /dev/ttyUSB0 | 串口设备路径 |
|
||||
| baudrate | int | 否 | 115200 | 波特率 |
|
||||
| network_key | string | 否 | 32 个 0 | 网络密钥(16 字节,32 字符十六进制) |
|
||||
| app_key | string | 否 | 32 个 0 | 应用密钥(16 字节,32 字符十六进制) |
|
||||
| network_id | string | 否 | "0000" | 网络 ID(2 字节,4 字符十六进制) |
|
||||
| iv_index | int | 否 | 0 | IV Index(4 字节) |
|
||||
| group_address | string | 否 | "0xC000" | 组地址起始值(16 进制字符串) |
|
||||
|
||||
### 9.2 选项参数
|
||||
|
||||
|
||||
@ -8,9 +8,22 @@ from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .const import DOMAIN
|
||||
from .const import (
|
||||
CONF_APP_KEY,
|
||||
CONF_GROUP_ADDRESS,
|
||||
CONF_IV_INDEX,
|
||||
CONF_NETWORK_ID,
|
||||
CONF_NETWORK_KEY,
|
||||
DEFAULT_APP_KEY,
|
||||
DEFAULT_GROUP_ADDRESS_START,
|
||||
DEFAULT_IV_INDEX,
|
||||
DEFAULT_NETWORK_ID,
|
||||
DEFAULT_NETWORK_KEY,
|
||||
DOMAIN,
|
||||
)
|
||||
from .coordinator import SigMeshGatewayCoordinator
|
||||
from .serial_reader import SerialReader
|
||||
from .services import setup_services
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@ -22,6 +35,9 @@ PLATFORMS: list[Platform] = [
|
||||
Platform.DEVICE_TRACKER,
|
||||
]
|
||||
|
||||
# 全局协调器字典(用于服务调用)
|
||||
_coordinators: dict[str, SigMeshGatewayCoordinator] = {}
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""设置 SigMesh Gateway 配置入口."""
|
||||
@ -32,6 +48,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
baudrate = entry.data.get("baudrate", 115200)
|
||||
poll_interval = entry.options.get("poll_interval", 30)
|
||||
|
||||
# 获取配网配置(首次使用需要)
|
||||
network_key = entry.data.get(CONF_NETWORK_KEY, DEFAULT_NETWORK_KEY)
|
||||
app_key = entry.data.get(CONF_APP_KEY, DEFAULT_APP_KEY)
|
||||
network_id = entry.data.get(CONF_NETWORK_ID, DEFAULT_NETWORK_ID)
|
||||
iv_index = entry.data.get(CONF_IV_INDEX, DEFAULT_IV_INDEX)
|
||||
group_address_start = entry.data.get(CONF_GROUP_ADDRESS, hex(DEFAULT_GROUP_ADDRESS_START))
|
||||
if isinstance(group_address_start, str):
|
||||
group_address_start = int(group_address_start, 16)
|
||||
|
||||
# 创建串口读取器
|
||||
serial_reader = SerialReader(
|
||||
device=device,
|
||||
@ -43,6 +68,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
hass=hass,
|
||||
serial_reader=serial_reader,
|
||||
poll_interval=poll_interval,
|
||||
network_key=network_key,
|
||||
app_key=app_key,
|
||||
network_id=network_id,
|
||||
iv_index=iv_index,
|
||||
group_address_start=group_address_start,
|
||||
)
|
||||
|
||||
# 存储 coordinator
|
||||
@ -52,6 +82,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"serial_reader": serial_reader,
|
||||
}
|
||||
|
||||
# 注册全局协调器
|
||||
_coordinators[entry.entry_id] = coordinator
|
||||
|
||||
# 设置服务(仅第一次)
|
||||
if not hass.services.has_service(DOMAIN, "start_scan"):
|
||||
setup_services(hass, _coordinators)
|
||||
|
||||
# 启动协调器
|
||||
await coordinator.start()
|
||||
|
||||
@ -85,6 +122,19 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
# 停止协调器
|
||||
await coordinator.stop()
|
||||
|
||||
# 移除全局协调器
|
||||
_coordinators.pop(entry.entry_id, None)
|
||||
|
||||
# 如果所有集成都已卸载,移除服务
|
||||
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")
|
||||
|
||||
return unload_ok
|
||||
|
||||
|
||||
|
||||
@ -11,10 +11,22 @@ from homeassistant.data_entry_flow import FlowResult
|
||||
from homeassistant.helpers import selector
|
||||
|
||||
from .const import (
|
||||
CONF_APP_KEY,
|
||||
CONF_BAUDRATE,
|
||||
CONF_GROUP_ADDRESS,
|
||||
CONF_IV_INDEX,
|
||||
CONF_NETWORK_ID,
|
||||
CONF_NETWORK_KEY,
|
||||
CONF_SERIAL_DEVICE,
|
||||
DEFAULT_APP_KEY,
|
||||
DEFAULT_BAUDRATE,
|
||||
DEFAULT_GROUP_ADDRESS_START,
|
||||
DEFAULT_IV_INDEX,
|
||||
DEFAULT_NAME,
|
||||
DEFAULT_NETWORK_ID,
|
||||
DEFAULT_NETWORK_KEY,
|
||||
DOMAIN,
|
||||
PROV_TIMEOUT,
|
||||
)
|
||||
|
||||
|
||||
@ -23,18 +35,36 @@ class SigMeshGatewayConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
|
||||
VERSION = 1
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""初始化配置流程."""
|
||||
self._errors: dict[str, str] = {}
|
||||
self._user_input: dict[str, Any] | None = None
|
||||
self._prov_config: dict[str, Any] | None = None
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> FlowResult:
|
||||
"""处理用户配置步骤."""
|
||||
errors = {}
|
||||
"""处理用户配置步骤 - 串口配置."""
|
||||
self._errors = {}
|
||||
|
||||
if user_input is not None:
|
||||
# TODO: 验证串口连接
|
||||
return self.async_create_entry(
|
||||
title=user_input.get(CONF_SERIAL_DEVICE, DEFAULT_NAME),
|
||||
data=user_input,
|
||||
# 验证串口连接
|
||||
try:
|
||||
import serial
|
||||
|
||||
test_serial = serial.Serial(
|
||||
user_input[CONF_SERIAL_DEVICE],
|
||||
baudrate=user_input.get(CONF_BAUDRATE, DEFAULT_BAUDRATE),
|
||||
timeout=0.5,
|
||||
)
|
||||
test_serial.close()
|
||||
self._user_input = user_input
|
||||
# 进入配网配置步骤
|
||||
return await self.async_step_prov_config()
|
||||
except serial.SerialException as e:
|
||||
self._errors[CONF_SERIAL_DEVICE] = f"无法打开串口:{e}"
|
||||
except Exception as e:
|
||||
self._errors["base"] = f"验证失败:{e}"
|
||||
|
||||
# 获取可用串口列表
|
||||
try:
|
||||
@ -59,10 +89,55 @@ class SigMeshGatewayConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
vol.Required(CONF_SERIAL_DEVICE, default="/dev/ttyUSB0"): selector.SelectSelector(
|
||||
selector.SelectSelectorConfig(options=port_list),
|
||||
),
|
||||
vol.Required("baudrate", default=DEFAULT_BAUDRATE): vol.Coerce(int),
|
||||
vol.Required(CONF_BAUDRATE, default=DEFAULT_BAUDRATE): vol.Coerce(int),
|
||||
}
|
||||
),
|
||||
errors=errors,
|
||||
errors=self._errors,
|
||||
)
|
||||
|
||||
async def async_step_prov_config(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> FlowResult:
|
||||
"""处理配网配置步骤 - 首次使用需要配置。
|
||||
|
||||
根据文档,USB dongle 首次使用需要设置配网参数:
|
||||
- Network Key (16 字节网络密钥)
|
||||
- App Key (16 字节应用密钥)
|
||||
- Network ID (2 字节网络 ID)
|
||||
- IV Index (4 字节)
|
||||
"""
|
||||
self._errors = {}
|
||||
|
||||
if user_input is not None:
|
||||
self._prov_config = user_input
|
||||
# 保存所有配置,完成配置流程
|
||||
return self.async_create_entry(
|
||||
title=self._user_input.get(CONF_SERIAL_DEVICE, DEFAULT_NAME),
|
||||
data={
|
||||
**self._user_input,
|
||||
**user_input,
|
||||
},
|
||||
)
|
||||
|
||||
# 配网配置表单
|
||||
return self.async_show_form(
|
||||
step_id="prov_config",
|
||||
data_schema=vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_NETWORK_KEY, default=DEFAULT_NETWORK_KEY): str,
|
||||
vol.Required(CONF_APP_KEY, default=DEFAULT_APP_KEY): str,
|
||||
vol.Required(CONF_NETWORK_ID, default=DEFAULT_NETWORK_ID): str,
|
||||
vol.Required(CONF_IV_INDEX, default=DEFAULT_IV_INDEX): int,
|
||||
vol.Required(
|
||||
CONF_GROUP_ADDRESS,
|
||||
default=hex(DEFAULT_GROUP_ADDRESS_START),
|
||||
): str,
|
||||
}
|
||||
),
|
||||
description_placeholders={
|
||||
"timeout": PROV_TIMEOUT,
|
||||
},
|
||||
errors=self._errors,
|
||||
)
|
||||
|
||||
@callback
|
||||
@ -79,14 +154,27 @@ class SigMeshGatewayOptionsFlow(config_entries.OptionsFlow):
|
||||
def __init__(self) -> None:
|
||||
"""初始化选项流程."""
|
||||
self._errors: dict[str, str] = {}
|
||||
self._prov_action: str | None = None
|
||||
|
||||
async def async_step_init(self, user_input: dict[str, Any] | None = None) -> FlowResult:
|
||||
async def async_step_init(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> FlowResult:
|
||||
"""管理选项."""
|
||||
# 显示主菜单
|
||||
return self.async_show_menu(
|
||||
step_id="init",
|
||||
menu_options=["poll_config", "prov_action", "group_config"],
|
||||
)
|
||||
|
||||
async def async_step_poll_config(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> FlowResult:
|
||||
"""配置轮询间隔."""
|
||||
if user_input is not None:
|
||||
return self.async_create_entry(title="", data=user_input)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="init",
|
||||
step_id="poll_config",
|
||||
data_schema=vol.Schema(
|
||||
{
|
||||
vol.Required(
|
||||
@ -96,3 +184,158 @@ class SigMeshGatewayOptionsFlow(config_entries.OptionsFlow):
|
||||
}
|
||||
),
|
||||
)
|
||||
|
||||
async def async_step_prov_action(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> FlowResult:
|
||||
"""配网操作选择."""
|
||||
if user_input is not None:
|
||||
self._prov_action = user_input.get("action")
|
||||
if self._prov_action == "start_scan":
|
||||
return await self.async_step_start_scan()
|
||||
elif self._prov_action == "stop_prov":
|
||||
return await self.async_step_stop_prov()
|
||||
elif self._prov_action == "bind_appkey":
|
||||
return await self.async_step_bind_appkey()
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="prov_action",
|
||||
data_schema=vol.Schema(
|
||||
{
|
||||
vol.Required("action"): selector.SelectSelector(
|
||||
selector.SelectSelectorConfig(
|
||||
options=[
|
||||
selector.SelectOptionDict(value="start_scan", label="开始扫描设备"),
|
||||
selector.SelectOptionDict(value="stop_prov", label="停止配网"),
|
||||
selector.SelectOptionDict(value="bind_appkey", label="绑定 App Key"),
|
||||
],
|
||||
),
|
||||
),
|
||||
}
|
||||
),
|
||||
)
|
||||
|
||||
async def async_step_start_scan(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> FlowResult:
|
||||
"""开始扫描设备."""
|
||||
if user_input is not None:
|
||||
# TODO: 调用配网管理器开始扫描
|
||||
return self.async_create_entry(title="", data={})
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="start_scan",
|
||||
description_placeholders={"timeout": PROV_TIMEOUT},
|
||||
data_schema=vol.Schema(
|
||||
{
|
||||
vol.Required("confirm"): bool,
|
||||
}
|
||||
),
|
||||
)
|
||||
|
||||
async def async_step_stop_prov(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> FlowResult:
|
||||
"""停止配网."""
|
||||
if user_input is not None:
|
||||
# TODO: 调用配网管理器停止配网
|
||||
return self.async_create_entry(title="", data={})
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="stop_prov",
|
||||
data_schema=vol.Schema(
|
||||
{
|
||||
vol.Required("confirm"): bool,
|
||||
}
|
||||
),
|
||||
)
|
||||
|
||||
async def async_step_bind_appkey(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> FlowResult:
|
||||
"""绑定 App Key."""
|
||||
if user_input is not None:
|
||||
# TODO: 调用配网管理器绑定 App Key
|
||||
return self.async_create_entry(title="", data={})
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="bind_appkey",
|
||||
data_schema=vol.Schema(
|
||||
{
|
||||
vol.Required("device_address"): str,
|
||||
vol.Required("element_address", default=0): vol.Coerce(int),
|
||||
}
|
||||
),
|
||||
)
|
||||
|
||||
async def async_step_group_config(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> FlowResult:
|
||||
"""分组配置."""
|
||||
if user_input is not None:
|
||||
action = user_input.get("group_action")
|
||||
if action == "add_to_group":
|
||||
return await self.async_step_add_to_group()
|
||||
elif action == "remove_from_group":
|
||||
return await self.async_step_remove_from_group()
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="group_config",
|
||||
data_schema=vol.Schema(
|
||||
{
|
||||
vol.Required("group_action"): selector.SelectSelector(
|
||||
selector.SelectSelectorConfig(
|
||||
options=[
|
||||
selector.SelectOptionDict(value="add_to_group", label="添加到组"),
|
||||
selector.SelectOptionDict(value="remove_from_group", label="从组移除"),
|
||||
],
|
||||
),
|
||||
),
|
||||
}
|
||||
),
|
||||
)
|
||||
|
||||
async def async_step_add_to_group(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> FlowResult:
|
||||
"""添加设备到组."""
|
||||
if user_input is not None:
|
||||
# TODO: 调用配网管理器添加设备到组
|
||||
return self.async_create_entry(title="", data={})
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="add_to_group",
|
||||
data_schema=vol.Schema(
|
||||
{
|
||||
vol.Required("target_address"): str,
|
||||
vol.Required("element_address", default=0): vol.Coerce(int),
|
||||
vol.Required("group_address", default=hex(DEFAULT_GROUP_ADDRESS_START)): str,
|
||||
vol.Required("model_id", default=4352): vol.Coerce(int), # 默认 0x1100
|
||||
vol.Required("is_sig", default=True): bool,
|
||||
}
|
||||
),
|
||||
description_placeholders={
|
||||
"default_group": hex(DEFAULT_GROUP_ADDRESS_START),
|
||||
},
|
||||
)
|
||||
|
||||
async def async_step_remove_from_group(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> FlowResult:
|
||||
"""从组中移除设备."""
|
||||
if user_input is not None:
|
||||
# TODO: 调用配网管理器移除设备
|
||||
return self.async_create_entry(title="", data={})
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="remove_from_group",
|
||||
data_schema=vol.Schema(
|
||||
{
|
||||
vol.Required("target_address"): str,
|
||||
vol.Required("element_address", default=0): vol.Coerce(int),
|
||||
vol.Required("group_address", default=hex(DEFAULT_GROUP_ADDRESS_START)): str,
|
||||
vol.Required("model_id", default=4352): vol.Coerce(int),
|
||||
vol.Required("is_sig", default=True): bool,
|
||||
}
|
||||
),
|
||||
)
|
||||
|
||||
@ -16,6 +16,27 @@ DEFAULT_TIMEOUT = 5
|
||||
CONF_SERIAL_DEVICE = "serial_device"
|
||||
CONF_BAUDRATE = "baudrate"
|
||||
|
||||
# 配网配置(USB dongle 首次使用需要)
|
||||
CONF_NETWORK_KEY = "network_key" # 16 字节网络密钥
|
||||
CONF_APP_KEY = "app_key" # 16 字节应用密钥
|
||||
CONF_NETWORK_ID = "network_id" # 2 字节网络 ID
|
||||
CONF_IV_INDEX = "iv_index" # 4 字节 IV Index
|
||||
|
||||
# 默认配网参数
|
||||
DEFAULT_NETWORK_KEY = "00000000000000000000000000000000" # 32 字符十六进制
|
||||
DEFAULT_APP_KEY = "00000000000000000000000000000000"
|
||||
DEFAULT_NETWORK_ID = "0000"
|
||||
DEFAULT_IV_INDEX = 0
|
||||
|
||||
# 配网超时
|
||||
PROV_TIMEOUT = 180 # 配网超时时间(秒)
|
||||
PROV_POLL_INTERVAL = 2 # 配网轮询间隔(秒)
|
||||
|
||||
# 组地址配置
|
||||
CONF_GROUP_ADDRESS = "group_address"
|
||||
DEFAULT_GROUP_ADDRESS_START = 0xC000 # 组地址起始值
|
||||
DEFAULT_GROUP_ADDRESS_END = 0xCFFF # 组地址结束值
|
||||
|
||||
|
||||
class MeshModelId(IntEnum):
|
||||
"""蓝牙 Mesh 模型 ID."""
|
||||
@ -76,6 +97,33 @@ class MeshOpcode(IntEnum):
|
||||
# 电池
|
||||
BATTERY_STATUS = 0x820C
|
||||
|
||||
# 配网相关
|
||||
PROV_LINK_OPEN = 0x00
|
||||
PROV_LINK_CLOSE = 0x01
|
||||
PROV_INVITE = 0x02
|
||||
PROV_START = 0x03
|
||||
PROV_AUTH_START = 0x04
|
||||
PROV_COMPLETE = 0x05
|
||||
PROV_FAILED = 0x06
|
||||
|
||||
# 配置相关
|
||||
CFG_NETKEY_ADD = 0x8000
|
||||
CFG_NETKEY_UPDATE = 0x8001
|
||||
CFG_APPKEY_ADD = 0x8003
|
||||
CFG_APPKEY_UPDATE = 0x8004
|
||||
CFG_MODEL_APP_BIND = 0x8006
|
||||
CFG_MODEL_SUBSCRIBE = 0x8007
|
||||
CFG_MODEL_PUBLISH = 0x8008
|
||||
|
||||
|
||||
class MeshSigOp(IntEnum):
|
||||
"""SIG 操作码(用于分组等)。"""
|
||||
|
||||
# 组管理
|
||||
SIG_GROUP_ADD = 0x801B # 加入组
|
||||
SIG_GROUP_DELETE = 0x801D # 删除组
|
||||
SIG_GROUP_STATUS = 0x801C # 组状态(预留)
|
||||
|
||||
|
||||
class MeshPropertyId(IntEnum):
|
||||
"""Mesh 属性 ID - 用于解析传感器数据."""
|
||||
|
||||
@ -10,7 +10,20 @@ from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.debounce import Debouncer
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
from .const import DOMAIN
|
||||
from .const import (
|
||||
CONF_APP_KEY,
|
||||
CONF_GROUP_ADDRESS,
|
||||
CONF_IV_INDEX,
|
||||
CONF_NETWORK_ID,
|
||||
CONF_NETWORK_KEY,
|
||||
DEFAULT_APP_KEY,
|
||||
DEFAULT_GROUP_ADDRESS_START,
|
||||
DEFAULT_IV_INDEX,
|
||||
DEFAULT_NETWORK_ID,
|
||||
DEFAULT_NETWORK_KEY,
|
||||
DOMAIN,
|
||||
)
|
||||
from .provisioning import ProvDevice, ProvState, ProvisioningManager
|
||||
from .protocol_parser import (
|
||||
DeviceManager,
|
||||
DeviceState,
|
||||
@ -34,6 +47,11 @@ class SigMeshGatewayCoordinator(DataUpdateCoordinator[dict[str, DeviceState]]):
|
||||
hass: HomeAssistant,
|
||||
serial_reader: SerialReader,
|
||||
poll_interval: int = 30,
|
||||
network_key: str = DEFAULT_NETWORK_KEY,
|
||||
app_key: str = DEFAULT_APP_KEY,
|
||||
network_id: str = DEFAULT_NETWORK_ID,
|
||||
iv_index: int = DEFAULT_IV_INDEX,
|
||||
group_address_start: int = DEFAULT_GROUP_ADDRESS_START,
|
||||
) -> None:
|
||||
"""初始化协调器."""
|
||||
self.hass = hass
|
||||
@ -43,8 +61,17 @@ class SigMeshGatewayCoordinator(DataUpdateCoordinator[dict[str, DeviceState]]):
|
||||
self._parser = ProtocolParser()
|
||||
self._device_manager = DeviceManager()
|
||||
|
||||
# 初始化配网管理器
|
||||
self._prov_manager = ProvisioningManager(
|
||||
serial_reader=serial_reader,
|
||||
network_key=network_key,
|
||||
app_key=app_key,
|
||||
network_id=network_id,
|
||||
)
|
||||
|
||||
# 设置回调
|
||||
self._setup_callbacks()
|
||||
self._setup_prov_callbacks()
|
||||
|
||||
super().__init__(
|
||||
hass,
|
||||
@ -78,6 +105,36 @@ class SigMeshGatewayCoordinator(DataUpdateCoordinator[dict[str, DeviceState]]):
|
||||
on_disconnect=on_disconnect_handler,
|
||||
)
|
||||
|
||||
def _setup_prov_callbacks(self) -> None:
|
||||
"""设置配网管理器的回调函数."""
|
||||
def on_prov_state_change(state: ProvState) -> None:
|
||||
"""处理配网状态变更."""
|
||||
_LOGGER.info("配网状态变更:%s", state.value)
|
||||
# 通知 HA 刷新配置
|
||||
self.hass.async_create_task(self._notify_prov_state_change(state))
|
||||
|
||||
def on_device_found(device: ProvDevice) -> None:
|
||||
"""处理设备发现."""
|
||||
_LOGGER.info("发现新设备:%s", device.mac_address)
|
||||
|
||||
def on_prov_complete(device: ProvDevice) -> None:
|
||||
"""处理配网完成."""
|
||||
_LOGGER.info("配网完成:%s", device.mac_address)
|
||||
|
||||
self._prov_manager.set_callbacks(
|
||||
on_state_change=on_prov_state_change,
|
||||
on_device_found=on_device_found,
|
||||
on_prov_complete=on_prov_complete,
|
||||
)
|
||||
|
||||
async def _notify_prov_state_change(self, state: ProvState) -> None:
|
||||
"""通知配网状态变更(可选:触发 HA 事件)。"""
|
||||
# 触发 HA 自定义事件
|
||||
self.hass.bus.fire(
|
||||
f"{DOMAIN}_prov_state",
|
||||
{"state": state.value},
|
||||
)
|
||||
|
||||
async def _handle_mesh_message(self, event: MeshMessageEvent) -> None:
|
||||
"""处理 Mesh 消息事件."""
|
||||
try:
|
||||
@ -121,9 +178,15 @@ class SigMeshGatewayCoordinator(DataUpdateCoordinator[dict[str, DeviceState]]):
|
||||
self._device_manager.add_device(
|
||||
event.mac_address, event.element_count or 1
|
||||
)
|
||||
# 通知配网管理器
|
||||
await self._prov_manager.handle_device_joined(
|
||||
event.mac_address, event.element_count or 1
|
||||
)
|
||||
elif event.event_type == "left":
|
||||
_LOGGER.info("设备离开:%s", event.mac_address)
|
||||
self._device_manager.remove_device(event.mac_address)
|
||||
# 通知配网管理器
|
||||
self._prov_manager.handle_device_left(event.mac_address)
|
||||
|
||||
# 刷新状态
|
||||
self.async_update_listeners()
|
||||
@ -184,3 +247,90 @@ class SigMeshGatewayCoordinator(DataUpdateCoordinator[dict[str, DeviceState]]):
|
||||
for device in self._device_manager.get_all_devices()
|
||||
if device.model_id == model_id
|
||||
]
|
||||
|
||||
# ==================== 配网管理方法 ====================
|
||||
|
||||
@property
|
||||
def prov_manager(self) -> ProvisioningManager:
|
||||
"""获取配网管理器."""
|
||||
return self._prov_manager
|
||||
|
||||
@property
|
||||
def prov_state(self) -> ProvState:
|
||||
"""获取当前配网状态."""
|
||||
return self._prov_manager.state
|
||||
|
||||
async def start_scanning(self) -> None:
|
||||
"""开始扫描设备."""
|
||||
await self._prov_manager.start_scanning()
|
||||
|
||||
async def start_provisioning(self, device_address: str) -> None:
|
||||
"""开始配网指定设备。
|
||||
|
||||
Args:
|
||||
device_address: 设备地址(16 进制字符串)
|
||||
"""
|
||||
await self._prov_manager.start_provisioning(device_address)
|
||||
|
||||
async def stop_provisioning(self) -> None:
|
||||
"""停止配网."""
|
||||
await self._prov_manager.stop_provisioning()
|
||||
|
||||
async def bind_app_key(self, device_address: str, element_address: int) -> None:
|
||||
"""绑定 App Key。
|
||||
|
||||
Args:
|
||||
device_address: 设备地址
|
||||
element_address: 元素地址
|
||||
"""
|
||||
await self._prov_manager.bind_app_key(device_address, element_address)
|
||||
|
||||
async def add_device_to_group(
|
||||
self,
|
||||
target_address: str,
|
||||
element_address: int,
|
||||
group_address: int,
|
||||
model_id: int,
|
||||
is_sig: bool = True,
|
||||
) -> None:
|
||||
"""添加设备到组。
|
||||
|
||||
Args:
|
||||
target_address: 目标设备地址
|
||||
element_address: 元素地址
|
||||
group_address: 组地址
|
||||
model_id: Model ID
|
||||
is_sig: 是否为 SIG 标准分组
|
||||
"""
|
||||
await self._prov_manager.add_to_group(
|
||||
target_address, element_address, group_address, model_id, is_sig
|
||||
)
|
||||
|
||||
async def remove_device_from_group(
|
||||
self,
|
||||
target_address: str,
|
||||
element_address: int,
|
||||
group_address: int,
|
||||
model_id: int,
|
||||
is_sig: bool = True,
|
||||
) -> None:
|
||||
"""从组中移除设备。
|
||||
|
||||
Args:
|
||||
target_address: 目标设备地址
|
||||
element_address: 元素地址
|
||||
group_address: 组地址
|
||||
model_id: Model ID
|
||||
is_sig: 是否为 SIG 标准分组
|
||||
"""
|
||||
await self._prov_manager.remove_from_group(
|
||||
target_address, element_address, group_address, model_id, is_sig
|
||||
)
|
||||
|
||||
def get_next_group_address(self) -> int:
|
||||
"""获取下一个可用组地址."""
|
||||
return self._prov_manager.get_next_group_address()
|
||||
|
||||
def get_prov_devices(self) -> dict[str, ProvDevice]:
|
||||
"""获取配网设备列表."""
|
||||
return self._prov_manager.devices
|
||||
|
||||
472
custom_components/sigmesh_gateway/provisioning.py
Normal file
472
custom_components/sigmesh_gateway/provisioning.py
Normal file
@ -0,0 +1,472 @@
|
||||
"""SigMesh Gateway 配网管理模块."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
from enum import Enum
|
||||
from typing import Callable
|
||||
|
||||
from .const import (
|
||||
DEFAULT_APP_KEY,
|
||||
DEFAULT_GROUP_ADDRESS_START,
|
||||
DEFAULT_NETWORK_KEY,
|
||||
DEFAULT_NETWORK_ID,
|
||||
MeshSigOp,
|
||||
PROV_TIMEOUT,
|
||||
)
|
||||
from .serial_reader import SerialReader
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ProvState(Enum):
|
||||
"""配网状态."""
|
||||
|
||||
IDLE = "idle" # 空闲
|
||||
SCANNING = "scanning" # 扫描中
|
||||
PROV_STARTING = "prov_starting" # 配网启动中
|
||||
PROV_IN_PROGRESS = "prov_in_progress" # 配网进行中
|
||||
PROV_COMPLETED = "prov_completed" # 配网完成
|
||||
PROV_FAILED = "prov_failed" # 配网失败
|
||||
TIMEOUT = "timeout" # 超时
|
||||
|
||||
|
||||
@dataclass
|
||||
class ProvDevice:
|
||||
"""配网设备信息."""
|
||||
|
||||
mac_address: str
|
||||
element_count: int
|
||||
unicast_address: int | None = None
|
||||
model_id: int | None = None
|
||||
joined_at: datetime | None = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class GroupConfig:
|
||||
"""组配置信息."""
|
||||
|
||||
group_address: int
|
||||
model_id: int
|
||||
element_address: int
|
||||
|
||||
|
||||
class ProvisioningManager:
|
||||
"""配网管理器."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
serial_reader: SerialReader,
|
||||
network_key: str = DEFAULT_NETWORK_KEY,
|
||||
app_key: str = DEFAULT_APP_KEY,
|
||||
network_id: str = DEFAULT_NETWORK_ID,
|
||||
) -> None:
|
||||
"""初始化配网管理器."""
|
||||
self.serial_reader = serial_reader
|
||||
self.network_key = network_key
|
||||
self.app_key = app_key
|
||||
self.network_id = network_id
|
||||
|
||||
self._state = ProvState.IDLE
|
||||
self._devices: dict[str, ProvDevice] = {}
|
||||
self._group_configs: dict[int, list[GroupConfig]] = {}
|
||||
self._prov_timeout_handle: asyncio.TimerHandle | None = None
|
||||
self._scan_result: list[dict] | None = None
|
||||
|
||||
# 回调
|
||||
self._on_state_change_callback: Callable[[ProvState], None] | None = None
|
||||
self._on_device_found_callback: Callable[[ProvDevice], None] | None = None
|
||||
self._on_prov_complete_callback: Callable[[ProvDevice], None] | None = None
|
||||
|
||||
@property
|
||||
def state(self) -> ProvState:
|
||||
"""获取当前配网状态."""
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def devices(self) -> dict[str, ProvDevice]:
|
||||
"""获取已配网设备列表."""
|
||||
return self._devices
|
||||
|
||||
def set_callbacks(
|
||||
self,
|
||||
on_state_change: Callable[[ProvState], None] | None = None,
|
||||
on_device_found: Callable[[ProvDevice], None] | None = None,
|
||||
on_prov_complete: Callable[[ProvDevice], None] | None = None,
|
||||
) -> None:
|
||||
"""设置回调函数."""
|
||||
self._on_state_change_callback = on_state_change
|
||||
self._on_device_found_callback = on_device_found
|
||||
self._on_prov_complete_callback = on_prov_complete
|
||||
|
||||
def _set_state(self, state: ProvState) -> None:
|
||||
"""设置配网状态."""
|
||||
self._state = state
|
||||
_LOGGER.info("配网状态变更:%s", state.value)
|
||||
if self._on_state_change_callback:
|
||||
self._on_state_change_callback(state)
|
||||
|
||||
def _start_prov_timeout(self) -> None:
|
||||
"""启动配网超时计时器."""
|
||||
if self._prov_timeout_handle:
|
||||
self._prov_timeout_handle.cancel()
|
||||
|
||||
async def timeout_handler() -> None:
|
||||
_LOGGER.warning("配网超时(%d 秒)", PROV_TIMEOUT)
|
||||
self._set_state(ProvState.TIMEOUT)
|
||||
await self.stop_provisioning()
|
||||
|
||||
loop = asyncio.get_event_loop()
|
||||
self._prov_timeout_handle = loop.call_later(PROV_TIMEOUT, lambda: asyncio.create_task(timeout_handler()))
|
||||
|
||||
def _cancel_prov_timeout(self) -> None:
|
||||
"""取消配网超时计时器."""
|
||||
if self._prov_timeout_handle:
|
||||
self._prov_timeout_handle.cancel()
|
||||
self._prov_timeout_handle = None
|
||||
|
||||
async def start_scanning(self) -> None:
|
||||
"""开始扫描设备."""
|
||||
if self._state not in [ProvState.IDLE, ProvState.PROV_COMPLETED, ProvState.PROV_FAILED]:
|
||||
_LOGGER.warning("无法开始扫描,当前状态:%s", self._state.value)
|
||||
return
|
||||
|
||||
self._set_state(ProvState.SCANNING)
|
||||
self._devices = {}
|
||||
self._scan_result = []
|
||||
|
||||
# 发送扫描命令
|
||||
# 注意:实际扫描由网关自动广播触发,这里只需等待设备上报
|
||||
_LOGGER.info("开始扫描设备,等待设备上报...")
|
||||
|
||||
async def start_provisioning(self, device_address: str) -> None:
|
||||
"""开始配网指定设备。
|
||||
|
||||
Args:
|
||||
device_address: 设备地址(16 进制字符串)
|
||||
"""
|
||||
if self._state != ProvState.SCANNING:
|
||||
_LOGGER.warning("无法开始配网,当前状态:%s", self._state.value)
|
||||
return
|
||||
|
||||
self._set_state(ProvState.PROV_STARTING)
|
||||
|
||||
try:
|
||||
# 1. 发送配网启动命令
|
||||
# 格式:AT+PROV=START,<address>
|
||||
cmd = f"AT+PROV=START,{device_address}"
|
||||
await self.serial_reader.write_command(cmd)
|
||||
|
||||
# 2. 启动超时计时器
|
||||
self._start_prov_timeout()
|
||||
|
||||
# 3. 等待配网完成
|
||||
self._set_state(ProvState.PROV_IN_PROGRESS)
|
||||
|
||||
except Exception as e:
|
||||
_LOGGER.error("启动配网失败:%s", e)
|
||||
self._set_state(ProvState.PROV_FAILED)
|
||||
self._cancel_prov_timeout()
|
||||
|
||||
async def stop_provisioning(self) -> None:
|
||||
"""停止配网."""
|
||||
self._cancel_prov_timeout()
|
||||
|
||||
try:
|
||||
# 发送停止配网命令
|
||||
cmd = "AT+PROV=STOP"
|
||||
await self.serial_reader.write_command(cmd)
|
||||
except Exception as e:
|
||||
_LOGGER.warning("停止配网命令失败:%s", e)
|
||||
|
||||
self._set_state(ProvState.IDLE)
|
||||
|
||||
async def bind_app_key(self, device_address: str, element_address: int) -> None:
|
||||
"""绑定 App Key。
|
||||
|
||||
Args:
|
||||
device_address: 设备地址
|
||||
element_address: 元素地址
|
||||
"""
|
||||
try:
|
||||
# 发送绑定 App Key 命令
|
||||
# 格式:AT+PROV=BIND,<device_address>,<element_address>
|
||||
cmd = f"AT+PROV=BIND,{device_address},{element_address}"
|
||||
await self.serial_reader.write_command(cmd)
|
||||
_LOGGER.info("绑定 App Key 完成:%s, 元素:%d", device_address, element_address)
|
||||
except Exception as e:
|
||||
_LOGGER.error("绑定 App Key 失败:%s", e)
|
||||
|
||||
async def add_to_group(
|
||||
self,
|
||||
target_address: str,
|
||||
element_address: int,
|
||||
group_address: int,
|
||||
model_id: int,
|
||||
is_sig: bool = True,
|
||||
) -> None:
|
||||
"""添加设备到组。
|
||||
|
||||
Args:
|
||||
target_address: 目标设备地址
|
||||
element_address: 元素地址
|
||||
group_address: 组地址
|
||||
model_id: Model ID
|
||||
is_sig: 是否为 SIG 标准分组
|
||||
"""
|
||||
try:
|
||||
if is_sig:
|
||||
# SIG 分组命令
|
||||
# e8 ff 00 00 00 00 02 01 <target_addr> 80 1b <element_addr> <group_addr> <model_id>
|
||||
target_bytes = bytes.fromhex(target_address.zfill(4))
|
||||
element_bytes = element_address.to_bytes(2, "big")
|
||||
group_bytes = group_address.to_bytes(2, "big")
|
||||
model_bytes = model_id.to_bytes(2, "big")
|
||||
|
||||
# 构建命令帧
|
||||
cmd_frame = (
|
||||
b"\xe8\xff\x00\x00\x00\x00\x02\x01"
|
||||
+ target_bytes
|
||||
+ b"\x80\x1b"
|
||||
+ element_bytes
|
||||
+ group_bytes
|
||||
+ model_bytes
|
||||
+ b"\x00\x10"
|
||||
)
|
||||
|
||||
# 转换为 16 进制字符串发送
|
||||
cmd_hex = cmd_frame.hex().upper()
|
||||
cmd = f"AT+MESH=TX,{cmd_hex}"
|
||||
else:
|
||||
# VENDOR 分组命令
|
||||
# e8 ff 00 00 00 00 02 01 <target_addr> 80 1b <element_addr> <group_addr> <model_id> <fixed>
|
||||
target_bytes = bytes.fromhex(target_address.zfill(4))
|
||||
element_bytes = element_address.to_bytes(2, "big")
|
||||
group_bytes = group_address.to_bytes(2, "big")
|
||||
model_bytes = model_id.to_bytes(2, "big")
|
||||
|
||||
cmd_frame = (
|
||||
b"\xe8\xff\x00\x00\x00\x00\x02\x01"
|
||||
+ target_bytes
|
||||
+ b"\x80\x1b"
|
||||
+ element_bytes
|
||||
+ group_bytes
|
||||
+ model_bytes
|
||||
+ b"\x00\x00"
|
||||
)
|
||||
|
||||
cmd_hex = cmd_frame.hex().upper()
|
||||
cmd = f"AT+MESH=TX,{cmd_hex}"
|
||||
|
||||
await self.serial_reader.write_command(cmd)
|
||||
_LOGGER.info(
|
||||
"添加设备到组:%s, 元素:%d, 组地址:0x%04X, Model:0x%04X",
|
||||
target_address,
|
||||
element_address,
|
||||
group_address,
|
||||
model_id,
|
||||
)
|
||||
|
||||
# 记录组配置
|
||||
if group_address not in self._group_configs:
|
||||
self._group_configs[group_address] = []
|
||||
self._group_configs[group_address].append(
|
||||
GroupConfig(
|
||||
group_address=group_address,
|
||||
model_id=model_id,
|
||||
element_address=element_address,
|
||||
)
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
_LOGGER.error("添加设备到组失败:%s", e)
|
||||
|
||||
async def remove_from_group(
|
||||
self,
|
||||
target_address: str,
|
||||
element_address: int,
|
||||
group_address: int,
|
||||
model_id: int,
|
||||
is_sig: bool = True,
|
||||
) -> None:
|
||||
"""从组中移除设备。
|
||||
|
||||
Args:
|
||||
target_address: 目标设备地址
|
||||
element_address: 元素地址
|
||||
group_address: 组地址
|
||||
model_id: Model ID
|
||||
is_sig: 是否为 SIG 标准分组
|
||||
"""
|
||||
try:
|
||||
if is_sig:
|
||||
# SIG 删除组命令
|
||||
# e8 ff 00 00 00 00 02 01 <target_addr> 80 1d <element_addr> <group_addr> <model_id>
|
||||
target_bytes = bytes.fromhex(target_address.zfill(4))
|
||||
element_bytes = element_address.to_bytes(2, "big")
|
||||
group_bytes = group_address.to_bytes(2, "big")
|
||||
model_bytes = model_id.to_bytes(2, "big")
|
||||
|
||||
cmd_frame = (
|
||||
b"\xe8\xff\x00\x00\x00\x00\x02\x01"
|
||||
+ target_bytes
|
||||
+ b"\x80\x1d"
|
||||
+ element_bytes
|
||||
+ group_bytes
|
||||
+ model_bytes
|
||||
+ b"\x00\x10"
|
||||
)
|
||||
|
||||
cmd_hex = cmd_frame.hex().upper()
|
||||
cmd = f"AT+MESH=TX,{cmd_hex}"
|
||||
else:
|
||||
# VENDOR 删除组命令(类似,使用 80 1d 操作码)
|
||||
target_bytes = bytes.fromhex(target_address.zfill(4))
|
||||
element_bytes = element_address.to_bytes(2, "big")
|
||||
group_bytes = group_address.to_bytes(2, "big")
|
||||
model_bytes = model_id.to_bytes(2, "big")
|
||||
|
||||
cmd_frame = (
|
||||
b"\xe8\xff\x00\x00\x00\x00\x02\x01"
|
||||
+ target_bytes
|
||||
+ b"\x80\x1d"
|
||||
+ element_bytes
|
||||
+ group_bytes
|
||||
+ model_bytes
|
||||
+ b"\x00\x00"
|
||||
)
|
||||
|
||||
cmd_hex = cmd_frame.hex().upper()
|
||||
cmd = f"AT+MESH=TX,{cmd_hex}"
|
||||
|
||||
await self.serial_reader.write_command(cmd)
|
||||
_LOGGER.info(
|
||||
"从组中移除设备:%s, 元素:%d, 组地址:0x%04X, Model:0x%04X",
|
||||
target_address,
|
||||
element_address,
|
||||
group_address,
|
||||
model_id,
|
||||
)
|
||||
|
||||
# 移除组配置记录
|
||||
if group_address in self._group_configs:
|
||||
self._group_configs[group_address] = [
|
||||
cfg
|
||||
for cfg in self._group_configs[group_address]
|
||||
if cfg.element_address != element_address and cfg.model_id != model_id
|
||||
]
|
||||
if not self._group_configs[group_address]:
|
||||
del self._group_configs[group_address]
|
||||
|
||||
except Exception as e:
|
||||
_LOGGER.error("从组中移除设备失败:%s", e)
|
||||
|
||||
def get_next_group_address(self) -> int:
|
||||
"""获取下一个可用组地址."""
|
||||
used_addresses = set(self._group_configs.keys())
|
||||
for addr in range(DEFAULT_GROUP_ADDRESS_START, DEFAULT_GROUP_ADDRESS_END + 1):
|
||||
if addr not in used_addresses:
|
||||
return addr
|
||||
raise RuntimeError("组地址已用尽")
|
||||
|
||||
def get_group_config(self, group_address: int) -> list[GroupConfig] | None:
|
||||
"""获取指定组地址的配置。
|
||||
|
||||
Args:
|
||||
group_address: 组地址
|
||||
|
||||
Returns:
|
||||
组配置列表,None 表示未找到
|
||||
"""
|
||||
return self._group_configs.get(group_address)
|
||||
|
||||
async def send_vendor_command(
|
||||
self,
|
||||
target_address: str,
|
||||
element_address: int,
|
||||
opcode: int,
|
||||
payload: bytes,
|
||||
) -> None:
|
||||
"""发送 VENDOR 命令。
|
||||
|
||||
Args:
|
||||
target_address: 目标设备地址
|
||||
element_address: 元素地址
|
||||
opcode: 操作码
|
||||
payload: 数据负载
|
||||
"""
|
||||
try:
|
||||
# VENDOR 命令帧格式
|
||||
target_bytes = bytes.fromhex(target_address.zfill(4))
|
||||
element_bytes = element_address.to_bytes(2, "big")
|
||||
opcode_bytes = opcode.to_bytes(2, "big")
|
||||
|
||||
cmd_frame = (
|
||||
b"\xe8\xff\x00\x00\x00\x00\x02\x01"
|
||||
+ target_bytes
|
||||
+ opcode_bytes
|
||||
+ element_bytes
|
||||
+ payload
|
||||
)
|
||||
|
||||
cmd_hex = cmd_frame.hex().upper()
|
||||
cmd = f"AT+MESH=TX,{cmd_hex}"
|
||||
await self.serial_reader.write_command(cmd)
|
||||
_LOGGER.info(
|
||||
"发送 VENDOR 命令:目标=%s, 元素=%d, Opcode=0x%04X",
|
||||
target_address,
|
||||
element_address,
|
||||
opcode,
|
||||
)
|
||||
except Exception as e:
|
||||
_LOGGER.error("发送 VENDOR 命令失败:%s", e)
|
||||
|
||||
async def handle_device_joined(self, mac_address: str, element_count: int) -> None:
|
||||
"""处理设备加入事件。
|
||||
|
||||
Args:
|
||||
mac_address: 设备 MAC 地址
|
||||
element_count: 元素数量
|
||||
"""
|
||||
device = ProvDevice(
|
||||
mac_address=mac_address,
|
||||
element_count=element_count,
|
||||
joined_at=datetime.now(),
|
||||
)
|
||||
self._devices[mac_address] = device
|
||||
|
||||
_LOGGER.info("设备加入:%s, 元素数量:%d", mac_address, element_count)
|
||||
|
||||
if self._on_device_found_callback:
|
||||
self._on_device_found_callback(device)
|
||||
|
||||
# 配网完成,取消超时
|
||||
if self._state == ProvState.PROV_IN_PROGRESS:
|
||||
self._cancel_prov_timeout()
|
||||
self._set_state(ProvState.PROV_COMPLETED)
|
||||
if self._on_prov_complete_callback:
|
||||
self._on_prov_complete_callback(device)
|
||||
|
||||
def handle_device_left(self, mac_address: str) -> None:
|
||||
"""处理设备离开事件。
|
||||
|
||||
Args:
|
||||
mac_address: 设备 MAC 地址
|
||||
"""
|
||||
if mac_address in self._devices:
|
||||
del self._devices[mac_address]
|
||||
_LOGGER.info("设备离开:%s", mac_address)
|
||||
|
||||
def get_device(self, mac_address: str) -> ProvDevice | None:
|
||||
"""获取配网设备信息。
|
||||
|
||||
Args:
|
||||
mac_address: 设备 MAC 地址
|
||||
|
||||
Returns:
|
||||
配网设备信息,None 表示未找到
|
||||
"""
|
||||
return self._devices.get(mac_address)
|
||||
186
custom_components/sigmesh_gateway/services.py
Normal file
186
custom_components/sigmesh_gateway/services.py
Normal file
@ -0,0 +1,186 @@
|
||||
"""SigMesh Gateway 服务定义。"""
|
||||
|
||||
import voluptuous as vol
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
|
||||
from .const import CONF_GROUP_ADDRESS, CONF_NETWORK_KEY, DOMAIN
|
||||
|
||||
# 服务常量
|
||||
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"
|
||||
|
||||
# 服务 Schema
|
||||
SERVICE_SCHEMA_START_SCAN = {}
|
||||
|
||||
SERVICE_SCHEMA_STOP_PROVISIONING = {}
|
||||
|
||||
SERVICE_SCHEMA_START_PROVISIONING = {
|
||||
vol.Required("device_address"): cv.string,
|
||||
}
|
||||
|
||||
SERVICE_SCHEMA_BIND_APPKEY = {
|
||||
vol.Required("device_address"): cv.string,
|
||||
vol.Required("element_address", default=0): vol.Coerce(int),
|
||||
}
|
||||
|
||||
SERVICE_SCHEMA_ADD_TO_GROUP = {
|
||||
vol.Required("target_address"): cv.string,
|
||||
vol.Required("element_address", default=0): vol.Coerce(int),
|
||||
vol.Required("group_address"): cv.string,
|
||||
vol.Required("model_id", default=4352): vol.Coerce(int),
|
||||
vol.Optional("is_sig", default=True): cv.boolean,
|
||||
}
|
||||
|
||||
SERVICE_SCHEMA_REMOVE_FROM_GROUP = {
|
||||
vol.Required("target_address"): cv.string,
|
||||
vol.Required("element_address", default=0): vol.Coerce(int),
|
||||
vol.Required("group_address"): cv.string,
|
||||
vol.Required("model_id", default=4352): vol.Coerce(int),
|
||||
vol.Optional("is_sig", default=True): cv.boolean,
|
||||
}
|
||||
|
||||
SERVICE_SCHEMA_SEND_VENDOR_COMMAND = {
|
||||
vol.Required("target_address"): cv.string,
|
||||
vol.Required("element_address", default=0): vol.Coerce(int),
|
||||
vol.Required("opcode"): cv.string,
|
||||
vol.Required("payload"): cv.string,
|
||||
}
|
||||
|
||||
|
||||
def setup_services(hass, coordinators: dict) -> None:
|
||||
"""设置 HA 服务。
|
||||
|
||||
Args:
|
||||
hass: HomeAssistant 实例
|
||||
coordinators: 协调器字典 {entry_id: coordinator}
|
||||
"""
|
||||
|
||||
async def handle_start_scan(call) -> None:
|
||||
"""处理开始扫描服务调用."""
|
||||
for coordinator in coordinators.values():
|
||||
await coordinator.start_scanning()
|
||||
|
||||
async def handle_stop_provisioning(call) -> None:
|
||||
"""处理停止配网服务调用."""
|
||||
for coordinator in coordinators.values():
|
||||
await coordinator.stop_provisioning()
|
||||
|
||||
async def handle_start_provisioning(call) -> None:
|
||||
"""处理开始配网服务调用."""
|
||||
device_address = call.data.get("device_address")
|
||||
for coordinator in coordinators.values():
|
||||
await coordinator.start_provisioning(device_address)
|
||||
|
||||
async def handle_bind_appkey(call) -> None:
|
||||
"""处理绑定 App Key 服务调用."""
|
||||
device_address = call.data.get("device_address")
|
||||
element_address = call.data.get("element_address", 0)
|
||||
for coordinator in coordinators.values():
|
||||
await coordinator.bind_app_key(device_address, element_address)
|
||||
|
||||
async def handle_add_to_group(call) -> None:
|
||||
"""处理添加设备到组服务调用."""
|
||||
target_address = call.data.get("target_address")
|
||||
element_address = call.data.get("element_address", 0)
|
||||
group_address = call.data.get("group_address")
|
||||
model_id = call.data.get("model_id", 4352)
|
||||
is_sig = call.data.get("is_sig", True)
|
||||
|
||||
# 解析组地址(支持 hex 字符串)
|
||||
if isinstance(group_address, str):
|
||||
group_address = int(group_address, 16)
|
||||
|
||||
for coordinator in coordinators.values():
|
||||
await coordinator.add_device_to_group(
|
||||
target_address, element_address, group_address, model_id, is_sig
|
||||
)
|
||||
|
||||
async def handle_remove_from_group(call) -> None:
|
||||
"""处理从组中移除设备服务调用."""
|
||||
target_address = call.data.get("target_address")
|
||||
element_address = call.data.get("element_address", 0)
|
||||
group_address = call.data.get("group_address")
|
||||
model_id = call.data.get("model_id", 4352)
|
||||
is_sig = call.data.get("is_sig", True)
|
||||
|
||||
# 解析组地址(支持 hex 字符串)
|
||||
if isinstance(group_address, str):
|
||||
group_address = int(group_address, 16)
|
||||
|
||||
for coordinator in coordinators.values():
|
||||
await coordinator.remove_device_from_group(
|
||||
target_address, element_address, group_address, model_id, is_sig
|
||||
)
|
||||
|
||||
async def handle_send_vendor_command(call) -> None:
|
||||
"""处理发送 VENDOR 命令服务调用."""
|
||||
target_address = call.data.get("target_address")
|
||||
element_address = call.data.get("element_address", 0)
|
||||
opcode = call.data.get("opcode")
|
||||
payload = call.data.get("payload")
|
||||
|
||||
# 解析操作码和负载
|
||||
if isinstance(opcode, str):
|
||||
opcode = int(opcode, 16)
|
||||
if isinstance(payload, str):
|
||||
payload = bytes.fromhex(payload)
|
||||
|
||||
for coordinator in coordinators.values():
|
||||
await coordinator.prov_manager.send_vendor_command(
|
||||
target_address, element_address, opcode, payload
|
||||
)
|
||||
|
||||
# 注册服务
|
||||
hass.services.async_register(
|
||||
DOMAIN,
|
||||
SERVICE_START_SCAN,
|
||||
handle_start_scan,
|
||||
schema=vol.Schema(SERVICE_SCHEMA_START_SCAN),
|
||||
)
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN,
|
||||
SERVICE_STOP_PROVISIONING,
|
||||
handle_stop_provisioning,
|
||||
schema=vol.Schema(SERVICE_SCHEMA_STOP_PROVISIONING),
|
||||
)
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN,
|
||||
SERVICE_START_PROVISIONING,
|
||||
handle_start_provisioning,
|
||||
schema=vol.Schema(SERVICE_SCHEMA_START_PROVISIONING),
|
||||
)
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN,
|
||||
SERVICE_BIND_APPKEY,
|
||||
handle_bind_appkey,
|
||||
schema=vol.Schema(SERVICE_SCHEMA_BIND_APPKEY),
|
||||
)
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN,
|
||||
SERVICE_ADD_TO_GROUP,
|
||||
handle_add_to_group,
|
||||
schema=vol.Schema(SERVICE_SCHEMA_ADD_TO_GROUP),
|
||||
)
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN,
|
||||
SERVICE_REMOVE_FROM_GROUP,
|
||||
handle_remove_from_group,
|
||||
schema=vol.Schema(SERVICE_SCHEMA_REMOVE_FROM_GROUP),
|
||||
)
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN,
|
||||
SERVICE_SEND_VENDOR_COMMAND,
|
||||
handle_send_vendor_command,
|
||||
schema=vol.Schema(SERVICE_SCHEMA_SEND_VENDOR_COMMAND),
|
||||
)
|
||||
182
custom_components/sigmesh_gateway/services.yaml
Normal file
182
custom_components/sigmesh_gateway/services.yaml
Normal file
@ -0,0 +1,182 @@
|
||||
# SigMesh Gateway 服务定义
|
||||
|
||||
# 开始扫描设备
|
||||
start_scan:
|
||||
name: 开始扫描设备
|
||||
description: 开始扫描可用的 Bluetooth Mesh 设备
|
||||
fields: {}
|
||||
|
||||
# 停止配网
|
||||
stop_provisioning:
|
||||
name: 停止配网
|
||||
description: 停止当前的配网操作
|
||||
fields: {}
|
||||
|
||||
# 开始配网
|
||||
start_provisioning:
|
||||
name: 开始配网
|
||||
description: 开始配网指定的设备
|
||||
fields:
|
||||
device_address:
|
||||
name: 设备地址
|
||||
description: 要配网的设备地址(16 进制字符串)
|
||||
example: "001A"
|
||||
required: true
|
||||
selector:
|
||||
text:
|
||||
|
||||
# 绑定 App Key
|
||||
bind_appkey:
|
||||
name: 绑定 App Key
|
||||
description: 为已配网设备绑定 App Key
|
||||
fields:
|
||||
device_address:
|
||||
name: 设备地址
|
||||
description: 设备地址(16 进制字符串)
|
||||
example: "001A"
|
||||
required: true
|
||||
selector:
|
||||
text:
|
||||
element_address:
|
||||
name: 元素地址
|
||||
description: 元素地址(默认为 0)
|
||||
example: 0
|
||||
default: 0
|
||||
required: false
|
||||
selector:
|
||||
number:
|
||||
min: 0
|
||||
max: 255
|
||||
|
||||
# 添加设备到组
|
||||
add_to_group:
|
||||
name: 添加到组
|
||||
description: 将设备添加到指定的组地址
|
||||
fields:
|
||||
target_address:
|
||||
name: 目标设备地址
|
||||
description: 目标设备地址(16 进制字符串)
|
||||
example: "001A"
|
||||
required: true
|
||||
selector:
|
||||
text:
|
||||
element_address:
|
||||
name: 元素地址
|
||||
description: 元素地址(默认为 0)
|
||||
example: 0
|
||||
default: 0
|
||||
required: false
|
||||
selector:
|
||||
number:
|
||||
min: 0
|
||||
max: 255
|
||||
group_address:
|
||||
name: 组地址
|
||||
description: 组地址(16 进制字符串,建议使用 0xC000 以上)
|
||||
example: "C001"
|
||||
required: true
|
||||
selector:
|
||||
text:
|
||||
model_id:
|
||||
name: Model ID
|
||||
description: Model ID(16 进制,默认 0x1100 传感器)
|
||||
example: 4352
|
||||
default: 4352
|
||||
required: false
|
||||
selector:
|
||||
number:
|
||||
min: 0
|
||||
max: 65535
|
||||
is_sig:
|
||||
name: SIG 标准分组
|
||||
description: 是否为 SIG 标准分组(默认为 true)
|
||||
example: true
|
||||
default: true
|
||||
required: false
|
||||
selector:
|
||||
boolean:
|
||||
|
||||
# 从组中移除设备
|
||||
remove_from_group:
|
||||
name: 从组移除
|
||||
description: 将设备从指定的组地址移除
|
||||
fields:
|
||||
target_address:
|
||||
name: 目标设备地址
|
||||
description: 目标设备地址(16 进制字符串)
|
||||
example: "001A"
|
||||
required: true
|
||||
selector:
|
||||
text:
|
||||
element_address:
|
||||
name: 元素地址
|
||||
description: 元素地址(默认为 0)
|
||||
example: 0
|
||||
default: 0
|
||||
required: false
|
||||
selector:
|
||||
number:
|
||||
min: 0
|
||||
max: 255
|
||||
group_address:
|
||||
name: 组地址
|
||||
description: 组地址(16 进制字符串)
|
||||
example: "C001"
|
||||
required: true
|
||||
selector:
|
||||
text:
|
||||
model_id:
|
||||
name: Model ID
|
||||
description: Model ID(16 进制,默认 0x1100 传感器)
|
||||
example: 4352
|
||||
default: 4352
|
||||
required: false
|
||||
selector:
|
||||
number:
|
||||
min: 0
|
||||
max: 65535
|
||||
is_sig:
|
||||
name: SIG 标准分组
|
||||
description: 是否为 SIG 标准分组(默认为 true)
|
||||
example: true
|
||||
default: true
|
||||
required: false
|
||||
selector:
|
||||
boolean:
|
||||
|
||||
# 发送 VENDOR 命令
|
||||
send_vendor_command:
|
||||
name: 发送 VENDOR 命令
|
||||
description: 发送 VENDOR 自定义命令到设备
|
||||
fields:
|
||||
target_address:
|
||||
name: 目标设备地址
|
||||
description: 目标设备地址(16 进制字符串)
|
||||
example: "001A"
|
||||
required: true
|
||||
selector:
|
||||
text:
|
||||
element_address:
|
||||
name: 元素地址
|
||||
description: 元素地址(默认为 0)
|
||||
example: 0
|
||||
default: 0
|
||||
required: false
|
||||
selector:
|
||||
number:
|
||||
min: 0
|
||||
max: 255
|
||||
opcode:
|
||||
name: 操作码
|
||||
description: VENDOR 操作码(16 进制字符串)
|
||||
example: "1102"
|
||||
required: true
|
||||
selector:
|
||||
text:
|
||||
payload:
|
||||
name: 数据负载
|
||||
description: 数据负载(16 进制字符串,不含空格)
|
||||
example: "0000"
|
||||
required: true
|
||||
selector:
|
||||
text:
|
||||
Loading…
Reference in New Issue
Block a user