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
|
async def async_request_refresh() -> None
|
||||||
```
|
```
|
||||||
|
|
||||||
### 8.2 服务调用接口(TODO)
|
### 8.2 服务调用接口
|
||||||
|
|
||||||
|
已实现的服务调用接口:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
# service.yaml
|
# sigmesh_gateway.start_scan - 开始扫描设备
|
||||||
sigmesh_gateway.send_command:
|
start_scan:
|
||||||
|
name: 开始扫描设备
|
||||||
|
description: 开始扫描可用的 Bluetooth Mesh 设备
|
||||||
|
|
||||||
|
# sigmesh_gateway.stop_provisioning - 停止配网
|
||||||
|
stop_provisioning:
|
||||||
|
name: 停止配网
|
||||||
|
description: 停止当前的配网操作
|
||||||
|
|
||||||
|
# sigmesh_gateway.start_provisioning - 开始配网
|
||||||
|
start_provisioning:
|
||||||
fields:
|
fields:
|
||||||
device_id:
|
device_address:
|
||||||
description: 设备 ID
|
description: 要配网的设备地址(16 进制字符串)
|
||||||
example: "AA:BB:CC:DD:EE:FF"
|
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:
|
opcode:
|
||||||
description: 操作码
|
description: VENDOR 操作码
|
||||||
example: "0x8202"
|
example: "1102"
|
||||||
payload:
|
payload:
|
||||||
description: 数据负载
|
description: 数据负载(16 进制)
|
||||||
example: "01"
|
example: "0000"
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
@ -596,6 +666,11 @@ sigmesh_gateway.send_command:
|
|||||||
|------|------|------|--------|------|
|
|------|------|------|--------|------|
|
||||||
| serial_device | string | 是 | /dev/ttyUSB0 | 串口设备路径 |
|
| serial_device | string | 是 | /dev/ttyUSB0 | 串口设备路径 |
|
||||||
| baudrate | int | 否 | 115200 | 波特率 |
|
| 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 选项参数
|
### 9.2 选项参数
|
||||||
|
|
||||||
|
|||||||
@ -8,9 +8,22 @@ from homeassistant.config_entries import ConfigEntry
|
|||||||
from homeassistant.const import Platform
|
from homeassistant.const import Platform
|
||||||
from homeassistant.core import HomeAssistant
|
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 .coordinator import SigMeshGatewayCoordinator
|
||||||
from .serial_reader import SerialReader
|
from .serial_reader import SerialReader
|
||||||
|
from .services import setup_services
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -22,6 +35,9 @@ PLATFORMS: list[Platform] = [
|
|||||||
Platform.DEVICE_TRACKER,
|
Platform.DEVICE_TRACKER,
|
||||||
]
|
]
|
||||||
|
|
||||||
|
# 全局协调器字典(用于服务调用)
|
||||||
|
_coordinators: dict[str, SigMeshGatewayCoordinator] = {}
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||||
"""设置 SigMesh Gateway 配置入口."""
|
"""设置 SigMesh Gateway 配置入口."""
|
||||||
@ -32,6 +48,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||||||
baudrate = entry.data.get("baudrate", 115200)
|
baudrate = entry.data.get("baudrate", 115200)
|
||||||
poll_interval = entry.options.get("poll_interval", 30)
|
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(
|
serial_reader = SerialReader(
|
||||||
device=device,
|
device=device,
|
||||||
@ -43,6 +68,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||||||
hass=hass,
|
hass=hass,
|
||||||
serial_reader=serial_reader,
|
serial_reader=serial_reader,
|
||||||
poll_interval=poll_interval,
|
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
|
# 存储 coordinator
|
||||||
@ -52,6 +82,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||||||
"serial_reader": serial_reader,
|
"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()
|
await coordinator.start()
|
||||||
|
|
||||||
@ -85,6 +122,19 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||||||
# 停止协调器
|
# 停止协调器
|
||||||
await coordinator.stop()
|
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
|
return unload_ok
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -11,10 +11,22 @@ from homeassistant.data_entry_flow import FlowResult
|
|||||||
from homeassistant.helpers import selector
|
from homeassistant.helpers import selector
|
||||||
|
|
||||||
from .const import (
|
from .const import (
|
||||||
|
CONF_APP_KEY,
|
||||||
|
CONF_BAUDRATE,
|
||||||
|
CONF_GROUP_ADDRESS,
|
||||||
|
CONF_IV_INDEX,
|
||||||
|
CONF_NETWORK_ID,
|
||||||
|
CONF_NETWORK_KEY,
|
||||||
CONF_SERIAL_DEVICE,
|
CONF_SERIAL_DEVICE,
|
||||||
|
DEFAULT_APP_KEY,
|
||||||
DEFAULT_BAUDRATE,
|
DEFAULT_BAUDRATE,
|
||||||
|
DEFAULT_GROUP_ADDRESS_START,
|
||||||
|
DEFAULT_IV_INDEX,
|
||||||
DEFAULT_NAME,
|
DEFAULT_NAME,
|
||||||
|
DEFAULT_NETWORK_ID,
|
||||||
|
DEFAULT_NETWORK_KEY,
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
|
PROV_TIMEOUT,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -23,18 +35,36 @@ class SigMeshGatewayConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
|||||||
|
|
||||||
VERSION = 1
|
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(
|
async def async_step_user(
|
||||||
self, user_input: dict[str, Any] | None = None
|
self, user_input: dict[str, Any] | None = None
|
||||||
) -> FlowResult:
|
) -> FlowResult:
|
||||||
"""处理用户配置步骤."""
|
"""处理用户配置步骤 - 串口配置."""
|
||||||
errors = {}
|
self._errors = {}
|
||||||
|
|
||||||
if user_input is not None:
|
if user_input is not None:
|
||||||
# TODO: 验证串口连接
|
# 验证串口连接
|
||||||
return self.async_create_entry(
|
try:
|
||||||
title=user_input.get(CONF_SERIAL_DEVICE, DEFAULT_NAME),
|
import serial
|
||||||
data=user_input,
|
|
||||||
|
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:
|
try:
|
||||||
@ -59,10 +89,55 @@ class SigMeshGatewayConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
|||||||
vol.Required(CONF_SERIAL_DEVICE, default="/dev/ttyUSB0"): selector.SelectSelector(
|
vol.Required(CONF_SERIAL_DEVICE, default="/dev/ttyUSB0"): selector.SelectSelector(
|
||||||
selector.SelectSelectorConfig(options=port_list),
|
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
|
@callback
|
||||||
@ -79,14 +154,27 @@ class SigMeshGatewayOptionsFlow(config_entries.OptionsFlow):
|
|||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
"""初始化选项流程."""
|
"""初始化选项流程."""
|
||||||
self._errors: dict[str, str] = {}
|
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:
|
if user_input is not None:
|
||||||
return self.async_create_entry(title="", data=user_input)
|
return self.async_create_entry(title="", data=user_input)
|
||||||
|
|
||||||
return self.async_show_form(
|
return self.async_show_form(
|
||||||
step_id="init",
|
step_id="poll_config",
|
||||||
data_schema=vol.Schema(
|
data_schema=vol.Schema(
|
||||||
{
|
{
|
||||||
vol.Required(
|
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_SERIAL_DEVICE = "serial_device"
|
||||||
CONF_BAUDRATE = "baudrate"
|
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):
|
class MeshModelId(IntEnum):
|
||||||
"""蓝牙 Mesh 模型 ID."""
|
"""蓝牙 Mesh 模型 ID."""
|
||||||
@ -76,6 +97,33 @@ class MeshOpcode(IntEnum):
|
|||||||
# 电池
|
# 电池
|
||||||
BATTERY_STATUS = 0x820C
|
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):
|
class MeshPropertyId(IntEnum):
|
||||||
"""Mesh 属性 ID - 用于解析传感器数据."""
|
"""Mesh 属性 ID - 用于解析传感器数据."""
|
||||||
|
|||||||
@ -10,7 +10,20 @@ from homeassistant.core import HomeAssistant
|
|||||||
from homeassistant.helpers.debounce import Debouncer
|
from homeassistant.helpers.debounce import Debouncer
|
||||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
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 (
|
from .protocol_parser import (
|
||||||
DeviceManager,
|
DeviceManager,
|
||||||
DeviceState,
|
DeviceState,
|
||||||
@ -34,6 +47,11 @@ class SigMeshGatewayCoordinator(DataUpdateCoordinator[dict[str, DeviceState]]):
|
|||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
serial_reader: SerialReader,
|
serial_reader: SerialReader,
|
||||||
poll_interval: int = 30,
|
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:
|
) -> None:
|
||||||
"""初始化协调器."""
|
"""初始化协调器."""
|
||||||
self.hass = hass
|
self.hass = hass
|
||||||
@ -43,8 +61,17 @@ class SigMeshGatewayCoordinator(DataUpdateCoordinator[dict[str, DeviceState]]):
|
|||||||
self._parser = ProtocolParser()
|
self._parser = ProtocolParser()
|
||||||
self._device_manager = DeviceManager()
|
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_callbacks()
|
||||||
|
self._setup_prov_callbacks()
|
||||||
|
|
||||||
super().__init__(
|
super().__init__(
|
||||||
hass,
|
hass,
|
||||||
@ -78,6 +105,36 @@ class SigMeshGatewayCoordinator(DataUpdateCoordinator[dict[str, DeviceState]]):
|
|||||||
on_disconnect=on_disconnect_handler,
|
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:
|
async def _handle_mesh_message(self, event: MeshMessageEvent) -> None:
|
||||||
"""处理 Mesh 消息事件."""
|
"""处理 Mesh 消息事件."""
|
||||||
try:
|
try:
|
||||||
@ -121,9 +178,15 @@ class SigMeshGatewayCoordinator(DataUpdateCoordinator[dict[str, DeviceState]]):
|
|||||||
self._device_manager.add_device(
|
self._device_manager.add_device(
|
||||||
event.mac_address, event.element_count or 1
|
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":
|
elif event.event_type == "left":
|
||||||
_LOGGER.info("设备离开:%s", event.mac_address)
|
_LOGGER.info("设备离开:%s", event.mac_address)
|
||||||
self._device_manager.remove_device(event.mac_address)
|
self._device_manager.remove_device(event.mac_address)
|
||||||
|
# 通知配网管理器
|
||||||
|
self._prov_manager.handle_device_left(event.mac_address)
|
||||||
|
|
||||||
# 刷新状态
|
# 刷新状态
|
||||||
self.async_update_listeners()
|
self.async_update_listeners()
|
||||||
@ -184,3 +247,90 @@ class SigMeshGatewayCoordinator(DataUpdateCoordinator[dict[str, DeviceState]]):
|
|||||||
for device in self._device_manager.get_all_devices()
|
for device in self._device_manager.get_all_devices()
|
||||||
if device.model_id == model_id
|
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