feat: 升级至v2.0.0,新增人员/人脸管理服务与摄像头支持

- 新增 create_person、delete_person、create_face、delete_face 服务
- 支持通过摄像头实体获取图片 (camera_entity_id 参数)
- 添加配置选项支持,可通过 Options Flow 修改配置
- 传感器采用 CoordinatorEntity 模式,优化数据更新机制
- 改进重试逻辑与错误处理
- 重构代码结构,清理冗余代码
This commit is contained in:
2026-05-01 23:14:39 +08:00
parent f462bb1750
commit d081a51aa0
12 changed files with 1711 additions and 1000 deletions
+99 -114
View File
@@ -18,8 +18,6 @@ from .const import (
CONF_REGION,
CONF_PERSON_GROUP_ID,
DEFAULT_REGION,
ERROR_INVALID_CONFIG,
ERROR_API_ERROR,
)
from .errors import (
validate_config,
@@ -33,20 +31,6 @@ from .tencent_cloud_client import TencentCloudClient
_LOGGER = logging.getLogger(__name__)
# 腾讯云区域列表
AVAILABLE_REGIONS = [
"ap-beijing", "ap-shanghai", "ap-guangzhou", "ap-chengdu", "ap-chongqing", "ap-nanjing", "ap-hangzhou"
]
# Secret ID 格式验证
SECRET_ID_REGEX = re.compile(r'^AKID[a-zA-Z0-9\-]{36,}$')
# Secret Key 格式验证
SECRET_KEY_REGEX = re.compile(r'^[a-zA-Z0-9+/]{20,}$')
# 人员库ID格式验证
PERSON_GROUP_ID_REGEX = re.compile(r'^[a-zA-Z0-9\-_]{1,64}$')
class TencentFaceRecognitionConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""腾讯云人脸识别配置流程"""
@@ -54,6 +38,15 @@ class TencentFaceRecognitionConfigFlow(config_entries.ConfigFlow, domain=DOMAIN)
VERSION = 1
CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL
AVAILABLE_REGIONS = [
"ap-beijing", "ap-shanghai", "ap-guangzhou", "ap-chengdu",
"ap-chongqing", "ap-nanjing", "ap-hangzhou"
]
SECRET_ID_REGEX = re.compile(r'^AKID[a-zA-Z0-9\-]{36,}$')
SECRET_KEY_REGEX = re.compile(r'^[a-zA-Z0-9+/]{20,}$')
PERSON_GROUP_ID_REGEX = re.compile(r'^[a-zA-Z0-9\-_]{1,64}$')
def __init__(self):
"""初始化配置流程"""
self._client = None
@@ -64,38 +57,32 @@ class TencentFaceRecognitionConfigFlow(config_entries.ConfigFlow, domain=DOMAIN)
errors = {}
if user_input is not None:
# 验证配置
try:
# 验证输入格式
self._validate_input_format(user_input, errors)
if errors:
return self._show_user_form(errors)
# 基本配置验证
validate_config(user_input)
# 尝试创建腾讯云客户端以验证凭据
validation_result = await self._validate_tencent_cloud_credentials(user_input)
if not validation_result["success"]:
errors = validation_result["errors"]
return self._show_user_form(errors)
# 保存配置并创建配置项
return self.async_create_entry(
title=user_input.get(CONF_NAME, "腾讯云人脸识别"),
data=user_input
)
except InvalidConfigError as ex:
_LOGGER.error("配置验证失败: %s", ex)
errors["base"] = str(ex)
except Exception as ex:
_LOGGER.error("配置验证失败: %s", ex, exc_info=True)
_LOGGER.error("配置验证失败: %s", ex)
errors["base"] = f"配置验证失败: {str(ex)}"
# 显示配置表单
return self._show_user_form(errors)
def _show_user_form(self, errors: Dict[str, str]) -> dict:
@@ -107,7 +94,7 @@ class TencentFaceRecognitionConfigFlow(config_entries.ConfigFlow, domain=DOMAIN)
vol.Required(CONF_SECRET_KEY): vol.All(str, vol.Length(min=1)),
vol.Optional(CONF_REGION, default=DEFAULT_REGION): selector({
"select": {
"options": [{"label": region, "value": region} for region in AVAILABLE_REGIONS],
"options": [{"label": region, "value": region} for region in self.AVAILABLE_REGIONS],
"mode": "dropdown"
}
}),
@@ -128,47 +115,42 @@ class TencentFaceRecognitionConfigFlow(config_entries.ConfigFlow, domain=DOMAIN)
def _validate_input_format(self, user_input: Dict[str, Any], errors: Dict[str, str]) -> None:
"""验证输入格式"""
# 验证Secret ID格式
secret_id = user_input.get(CONF_SECRET_ID, "")
if not SECRET_ID_REGEX.match(secret_id):
if not self.SECRET_ID_REGEX.match(secret_id):
errors[CONF_SECRET_ID] = "Secret ID格式不正确,应以AKID开头,后跟36个以上字符"
# 验证Secret Key格式
secret_key = user_input.get(CONF_SECRET_KEY, "")
if not SECRET_KEY_REGEX.match(secret_key):
if not self.SECRET_KEY_REGEX.match(secret_key):
errors[CONF_SECRET_KEY] = "Secret Key格式不正确,应为20个以上字符"
# 验证区域
region = user_input.get(CONF_REGION, DEFAULT_REGION)
if region not in AVAILABLE_REGIONS:
errors[CONF_REGION] = f"无效的区域,请从列表中选择有效的区域"
# 验证人员库ID格式
if region not in self.AVAILABLE_REGIONS:
errors[CONF_REGION] = "无效的区域,请从列表中选择有效的区域"
person_group_id = user_input.get(CONF_PERSON_GROUP_ID, "Hass")
if not PERSON_GROUP_ID_REGEX.match(person_group_id):
if not self.PERSON_GROUP_ID_REGEX.match(person_group_id):
errors[CONF_PERSON_GROUP_ID] = "人员库ID只能包含字母、数字、连字符和下划线,长度1-64个字符"
# 验证名称
name = user_input.get(CONF_NAME, "腾讯云人脸识别")
if len(name) > 50:
errors[CONF_NAME] = "名称长度不能超过50个字符"
async def _validate_tencent_cloud_credentials(self, user_input: Dict[str, Any]) -> Dict[str, Any]:
"""验证腾讯云凭据"""
"""验证腾讯云凭据 - 通过真实API调用验证"""
try:
_LOGGER.info("开始验证腾讯云凭据")
# 创建腾讯云客户端
self._client = TencentCloudClient(
client = TencentCloudClient(
user_input[CONF_SECRET_ID],
user_input[CONF_SECRET_KEY],
user_input.get(CONF_REGION, DEFAULT_REGION)
)
_LOGGER.info("腾讯云客户端创建成功,凭据验证通过")
await self.hass.async_add_executor_job(client.verify_credentials)
_LOGGER.info("腾讯云凭据验证通过")
return {"success": True}
except AuthenticationError as ex:
_LOGGER.error("腾讯云认证失败: %s", ex)
return {
@@ -197,7 +179,7 @@ class TencentFaceRecognitionConfigFlow(config_entries.ConfigFlow, domain=DOMAIN)
}
}
except Exception as ex:
_LOGGER.error("腾讯云凭据验证失败: %s", ex, exc_info=True)
_LOGGER.error("腾讯云凭据验证失败: %s", ex)
return {
"success": False,
"errors": {
@@ -215,9 +197,10 @@ class TencentFaceRecognitionConfigFlow(config_entries.ConfigFlow, domain=DOMAIN)
class TencentFaceRecognitionOptionsFlow(config_entries.OptionsFlow):
"""腾讯云人脸识别选项流程"""
AVAILABLE_REGIONS = TencentFaceRecognitionConfigFlow.AVAILABLE_REGIONS
def __init__(self, config_entry):
"""初始化选项流程"""
# 存储需要的数据而不是整个 config_entry 实例
self._entry_id = config_entry.entry_id
self._title = config_entry.title
self._data = dict(config_entry.data)
@@ -227,17 +210,16 @@ class TencentFaceRecognitionOptionsFlow(config_entries.OptionsFlow):
"""管理选项流程"""
if user_input is not None:
action = user_input.get("action")
if action == "config":
return await self.async_step_config()
elif action == "test_connection":
return await self.async_step_test_connection()
elif action == "reauth":
return await self.async_step_reauth()
return self.async_create_entry(title="", data=user_input)
# 显示管理菜单
return self.async_show_form(
step_id="init",
data_schema=vol.Schema({
@@ -259,28 +241,41 @@ class TencentFaceRecognitionOptionsFlow(config_entries.OptionsFlow):
async def async_step_config(self, user_input=None):
"""配置设置步骤"""
errors = {}
if user_input is not None:
# 验证区域
region = user_input.get(CONF_REGION)
if region not in AVAILABLE_REGIONS:
errors[CONF_REGION] = f"无效的区域,请从列表中选择有效的区域"
if region not in self.AVAILABLE_REGIONS:
errors[CONF_REGION] = "无效的区域,请从列表中选择有效的区域"
person_group_id = user_input.get(CONF_PERSON_GROUP_ID, "")
if person_group_id and not re.match(r'^[a-zA-Z0-9\-_]{1,64}$', person_group_id):
errors[CONF_PERSON_GROUP_ID] = "人员库ID只能包含字母、数字、连字符和下划线,长度1-64个字符"
if not errors:
# 验证新配置是否有效
try:
# 创建临时客户端进行验证
temp_client = TencentCloudClient(
client = TencentCloudClient(
self._data.get(CONF_SECRET_ID),
self._data.get(CONF_SECRET_KEY),
region
)
await self.hass.async_add_executor_job(client.verify_credentials)
_LOGGER.info("配置验证成功")
# 保存配置
return self.async_create_entry(title="", data=user_input)
new_data = dict(self._data)
new_data[CONF_REGION] = region
if person_group_id:
new_data[CONF_PERSON_GROUP_ID] = person_group_id
entry = self.hass.config_entries.async_get_entry(self._entry_id)
self.hass.config_entries.async_update_entry(entry, data=new_data)
return self.async_create_entry(title="", data={
CONF_REGION: region,
CONF_PERSON_GROUP_ID: person_group_id,
})
except AuthenticationError as ex:
_LOGGER.error("腾讯云认证失败: %s", ex)
errors[CONF_REGION] = "认证失败,请检查区域设置"
@@ -290,7 +285,7 @@ class TencentFaceRecognitionOptionsFlow(config_entries.OptionsFlow):
errors[CONF_REGION] = "网络错误,请检查区域设置"
errors["base"] = "连接腾讯云失败,请检查网络连接和区域设置"
except Exception as ex:
_LOGGER.error("配置验证失败: %s", ex, exc_info=True)
_LOGGER.error("配置验证失败: %s", ex)
errors["base"] = f"配置验证失败: {str(ex)}"
options = {
@@ -299,7 +294,7 @@ class TencentFaceRecognitionOptionsFlow(config_entries.OptionsFlow):
default=self._options.get(CONF_REGION, self._data.get(CONF_REGION, DEFAULT_REGION))
): selector({
"select": {
"options": [{"label": region, "value": region} for region in AVAILABLE_REGIONS],
"options": [{"label": region, "value": region} for region in self.AVAILABLE_REGIONS],
"mode": "dropdown"
}
}),
@@ -314,36 +309,31 @@ class TencentFaceRecognitionOptionsFlow(config_entries.OptionsFlow):
data_schema=vol.Schema(options),
errors=errors,
)
async def async_step_test_connection(self, user_input=None):
"""测试连接步骤"""
errors = {}
connection_status = "未知"
if user_input is None:
# 尝试测试连接
try:
# 使用当前配置创建客户端
client = TencentCloudClient(
self._data.get(CONF_SECRET_ID),
self._data.get(CONF_SECRET_KEY),
self._data.get(CONF_REGION, DEFAULT_REGION)
)
# 使用 GetGroupInfo 测试连接
group_id = self._data.get(CONF_PERSON_GROUP_ID, "Hass")
success = await self.hass.async_add_executor_job(
client._test_api_connection,
group_id
self._data.get(CONF_PERSON_GROUP_ID, "Hass")
)
if not success:
raise Exception("连接测试失败,请检查人员库ID是否正确")
connection_status = "连接成功"
_LOGGER.info("连接测试成功")
except AuthenticationError as ex:
connection_status = f"认证失败: {str(ex)}"
errors["base"] = "认证失败,请检查Secret ID和Secret Key"
@@ -355,8 +345,8 @@ class TencentFaceRecognitionOptionsFlow(config_entries.OptionsFlow):
except Exception as ex:
connection_status = f"测试失败: {str(ex)}"
errors["base"] = f"连接测试失败: {str(ex)}"
_LOGGER.error("连接测试失败: %s", ex, exc_info=True)
_LOGGER.error("连接测试失败: %s", ex)
return self.async_show_form(
step_id="test_connection",
data_schema=vol.Schema({}),
@@ -365,18 +355,16 @@ class TencentFaceRecognitionOptionsFlow(config_entries.OptionsFlow):
"connection_status": connection_status
}
)
async def async_step_reauth(self, user_input=None):
"""重新认证步骤"""
errors = {}
if user_input is not None:
# 验证新凭据
try:
# 验证输入格式
temp_errors = {}
self._validate_input_format(user_input, temp_errors)
if temp_errors:
errors = temp_errors
return self.async_show_form(
@@ -387,28 +375,27 @@ class TencentFaceRecognitionOptionsFlow(config_entries.OptionsFlow):
}),
errors=errors,
)
# 创建腾讯云客户端验证新凭据
new_client = TencentCloudClient(
user_input[CONF_SECRET_ID],
user_input[CONF_SECRET_KEY],
self._data.get(CONF_REGION, DEFAULT_REGION)
)
await self.hass.async_add_executor_job(new_client.verify_credentials)
_LOGGER.info("重新认证成功")
# 更新配置
new_data = dict(self._data)
new_data.update(user_input)
# 重新创建配置项
self.hass.config_entries.async_update_entry(
self.hass.config_entries.async_get_entry(self._entry_id),
data=new_data
)
entry = self.hass.config_entries.async_get_entry(self._entry_id)
self.hass.config_entries.async_update_entry(entry, data=new_data)
await self.hass.config_entries.async_reload(entry.entry_id)
return self.async_create_entry(title="", data={})
except AuthenticationError as ex:
_LOGGER.error("重新认证失败: %s", ex)
errors["base"] = "认证失败,请检查Secret ID和Secret Key"
@@ -416,9 +403,9 @@ class TencentFaceRecognitionOptionsFlow(config_entries.OptionsFlow):
_LOGGER.error("重新认证网络错误: %s", ex)
errors["base"] = "网络错误,请检查网络连接和区域设置"
except Exception as ex:
_LOGGER.error("重新认证失败: %s", ex, exc_info=True)
_LOGGER.error("重新认证失败: %s", ex)
errors["base"] = f"重新认证失败: {str(ex)}"
return self.async_show_form(
step_id="reauth",
data_schema=vol.Schema({
@@ -427,19 +414,17 @@ class TencentFaceRecognitionOptionsFlow(config_entries.OptionsFlow):
}),
errors=errors,
description_placeholders={
"current_secret_id": self._data.get(CONF_SECRET_ID, ""),
"current_secret_id": self._data.get(CONF_SECRET_ID, "")[:8] + "***" if self._data.get(CONF_SECRET_ID) else "",
"current_region": self._data.get(CONF_REGION, DEFAULT_REGION)
}
)
def _validate_input_format(self, user_input: Dict[str, Any], errors: Dict[str, str]) -> None:
"""验证输入格式"""
# 验证Secret ID格式
secret_id = user_input.get(CONF_SECRET_ID, "")
if not SECRET_ID_REGEX.match(secret_id):
if not TencentFaceRecognitionConfigFlow.SECRET_ID_REGEX.match(secret_id):
errors[CONF_SECRET_ID] = "Secret ID格式不正确,应以AKID开头,后跟36个以上字符"
# 验证Secret Key格式
secret_key = user_input.get(CONF_SECRET_KEY, "")
if not SECRET_KEY_REGEX.match(secret_key):
errors[CONF_SECRET_KEY] = "Secret Key格式不正确,应为20个以上字符"
if not TencentFaceRecognitionConfigFlow.SECRET_KEY_REGEX.match(secret_key):
errors[CONF_SECRET_KEY] = "Secret Key格式不正确,应为20个以上字符"