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
+20 -46
View File
@@ -4,12 +4,9 @@
import logging import logging
from typing import Dict, Any from typing import Dict, Any
from homeassistant.components.http import StaticPathConfig
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import ( from .const import (
DOMAIN, DOMAIN,
@@ -17,7 +14,8 @@ from .const import (
CONF_SECRET_ID, CONF_SECRET_ID,
CONF_SECRET_KEY, CONF_SECRET_KEY,
CONF_REGION, CONF_REGION,
CONF_PERSON_GROUP_ID CONF_PERSON_GROUP_ID,
get_entry_value,
) )
from .tencent_cloud_client import TencentCloudClient from .tencent_cloud_client import TencentCloudClient
from .face_recognition import FaceRecognition from .face_recognition import FaceRecognition
@@ -25,30 +23,24 @@ from .services import async_setup_services, async_unload_services
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
# 添加传感器平台
PLATFORMS.append("sensor")
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""设置配置项""" """设置配置项"""
_LOGGER.info("设置腾讯云人脸识别插件") _LOGGER.info("设置腾讯云人脸识别插件")
# 获取配置
secret_id = entry.data.get(CONF_SECRET_ID) secret_id = entry.data.get(CONF_SECRET_ID)
secret_key = entry.data.get(CONF_SECRET_KEY) secret_key = entry.data.get(CONF_SECRET_KEY)
region = entry.data.get(CONF_REGION, "ap-shanghai") region = get_entry_value(entry, CONF_REGION, "ap-shanghai")
person_group_id = entry.data.get(CONF_PERSON_GROUP_ID, "Hass") person_group_id = get_entry_value(entry, CONF_PERSON_GROUP_ID, "Hass")
# 创建腾讯云客户端
try: try:
client = TencentCloudClient(secret_id, secret_key, region) client = TencentCloudClient(secret_id, secret_key, region)
except Exception as ex: except Exception as ex:
_LOGGER.error("创建腾讯云客户端失败: %s", ex) _LOGGER.error("创建腾讯云客户端失败")
raise ConfigEntryNotReady(f"创建腾讯云客户端失败: {ex}") raise ConfigEntryNotReady(f"创建腾讯云客户端失败: {ex}")
# 创建人脸识别实例
face_recognition = FaceRecognition(hass, client) face_recognition = FaceRecognition(hass, client)
# 存储配置和实例
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = { hass.data.setdefault(DOMAIN, {})[entry.entry_id] = {
"entry": entry, "entry": entry,
"client": client, "client": client,
@@ -58,46 +50,28 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"region": region, "region": region,
"person_group_id": person_group_id "person_group_id": person_group_id
} }
# 设置服务
await async_setup_services(hass) await async_setup_services(hass)
# 设置平台 await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
# 转发配置项到平台
for platform in PLATFORMS:
hass.async_create_task(
hass.config_entries.async_forward_entry_setups(entry, [platform])
)
# 不再注册前端面板,改为使用配置流管理
return True return True
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""卸载配置项""" """卸载配置项"""
_LOGGER.info("卸载腾讯云人脸识别插件") _LOGGER.info("卸载腾讯云人脸识别插件")
# 注销自定义面板 unload_ok = await hass.config_entries.async_forward_entry_unload(entry, "sensor")
# 注意:在新版本的 Home Assistant 中,面板注册方式已更改
# 不需要手动注销面板,系统会自动处理
# 卸载平台
unload_ok = True
for platform in PLATFORMS:
result = await hass.config_entries.async_forward_entry_unload(entry, platform)
unload_ok = unload_ok and result
if unload_ok: if unload_ok:
# 卸载服务
await async_unload_services(hass) await async_unload_services(hass)
# 移除数据
hass.data[DOMAIN].pop(entry.entry_id) hass.data[DOMAIN].pop(entry.entry_id)
return unload_ok return unload_ok
async def async_remove_entry(hass: HomeAssistant, entry: ConfigEntry) -> None: async def async_remove_entry(hass: HomeAssistant, entry: ConfigEntry) -> None:
"""移除配置项""" """移除配置项"""
_LOGGER.info("移除腾讯云人脸识别插件配置") _LOGGER.info("移除腾讯云人脸识别插件配置")
+99 -114
View File
@@ -18,8 +18,6 @@ from .const import (
CONF_REGION, CONF_REGION,
CONF_PERSON_GROUP_ID, CONF_PERSON_GROUP_ID,
DEFAULT_REGION, DEFAULT_REGION,
ERROR_INVALID_CONFIG,
ERROR_API_ERROR,
) )
from .errors import ( from .errors import (
validate_config, validate_config,
@@ -33,20 +31,6 @@ from .tencent_cloud_client import TencentCloudClient
_LOGGER = logging.getLogger(__name__) _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): class TencentFaceRecognitionConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""腾讯云人脸识别配置流程""" """腾讯云人脸识别配置流程"""
@@ -54,6 +38,15 @@ class TencentFaceRecognitionConfigFlow(config_entries.ConfigFlow, domain=DOMAIN)
VERSION = 1 VERSION = 1
CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL 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): def __init__(self):
"""初始化配置流程""" """初始化配置流程"""
self._client = None self._client = None
@@ -64,38 +57,32 @@ class TencentFaceRecognitionConfigFlow(config_entries.ConfigFlow, domain=DOMAIN)
errors = {} errors = {}
if user_input is not None: if user_input is not None:
# 验证配置
try: try:
# 验证输入格式
self._validate_input_format(user_input, errors) self._validate_input_format(user_input, errors)
if errors: if errors:
return self._show_user_form(errors) return self._show_user_form(errors)
# 基本配置验证
validate_config(user_input) validate_config(user_input)
# 尝试创建腾讯云客户端以验证凭据
validation_result = await self._validate_tencent_cloud_credentials(user_input) validation_result = await self._validate_tencent_cloud_credentials(user_input)
if not validation_result["success"]: if not validation_result["success"]:
errors = validation_result["errors"] errors = validation_result["errors"]
return self._show_user_form(errors) return self._show_user_form(errors)
# 保存配置并创建配置项
return self.async_create_entry( return self.async_create_entry(
title=user_input.get(CONF_NAME, "腾讯云人脸识别"), title=user_input.get(CONF_NAME, "腾讯云人脸识别"),
data=user_input data=user_input
) )
except InvalidConfigError as ex: except InvalidConfigError as ex:
_LOGGER.error("配置验证失败: %s", ex) _LOGGER.error("配置验证失败: %s", ex)
errors["base"] = str(ex) errors["base"] = str(ex)
except Exception as ex: except Exception as ex:
_LOGGER.error("配置验证失败: %s", ex, exc_info=True) _LOGGER.error("配置验证失败: %s", ex)
errors["base"] = f"配置验证失败: {str(ex)}" errors["base"] = f"配置验证失败: {str(ex)}"
# 显示配置表单
return self._show_user_form(errors) return self._show_user_form(errors)
def _show_user_form(self, errors: Dict[str, str]) -> dict: 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.Required(CONF_SECRET_KEY): vol.All(str, vol.Length(min=1)),
vol.Optional(CONF_REGION, default=DEFAULT_REGION): selector({ vol.Optional(CONF_REGION, default=DEFAULT_REGION): selector({
"select": { "select": {
"options": [{"label": region, "value": region} for region in AVAILABLE_REGIONS], "options": [{"label": region, "value": region} for region in self.AVAILABLE_REGIONS],
"mode": "dropdown" "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: 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, "") 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个以上字符" errors[CONF_SECRET_ID] = "Secret ID格式不正确,应以AKID开头,后跟36个以上字符"
# 验证Secret Key格式
secret_key = user_input.get(CONF_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个以上字符" errors[CONF_SECRET_KEY] = "Secret Key格式不正确,应为20个以上字符"
# 验证区域
region = user_input.get(CONF_REGION, DEFAULT_REGION) region = user_input.get(CONF_REGION, DEFAULT_REGION)
if region not in AVAILABLE_REGIONS: if region not in self.AVAILABLE_REGIONS:
errors[CONF_REGION] = f"无效的区域,请从列表中选择有效的区域" errors[CONF_REGION] = "无效的区域,请从列表中选择有效的区域"
# 验证人员库ID格式
person_group_id = user_input.get(CONF_PERSON_GROUP_ID, "Hass") 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个字符" errors[CONF_PERSON_GROUP_ID] = "人员库ID只能包含字母、数字、连字符和下划线,长度1-64个字符"
# 验证名称
name = user_input.get(CONF_NAME, "腾讯云人脸识别") name = user_input.get(CONF_NAME, "腾讯云人脸识别")
if len(name) > 50: if len(name) > 50:
errors[CONF_NAME] = "名称长度不能超过50个字符" errors[CONF_NAME] = "名称长度不能超过50个字符"
async def _validate_tencent_cloud_credentials(self, user_input: Dict[str, Any]) -> Dict[str, Any]: async def _validate_tencent_cloud_credentials(self, user_input: Dict[str, Any]) -> Dict[str, Any]:
"""验证腾讯云凭据""" """验证腾讯云凭据 - 通过真实API调用验证"""
try: try:
_LOGGER.info("开始验证腾讯云凭据") _LOGGER.info("开始验证腾讯云凭据")
# 创建腾讯云客户端 client = TencentCloudClient(
self._client = TencentCloudClient(
user_input[CONF_SECRET_ID], user_input[CONF_SECRET_ID],
user_input[CONF_SECRET_KEY], user_input[CONF_SECRET_KEY],
user_input.get(CONF_REGION, DEFAULT_REGION) user_input.get(CONF_REGION, DEFAULT_REGION)
) )
_LOGGER.info("腾讯云客户端创建成功,凭据验证通过") await self.hass.async_add_executor_job(client.verify_credentials)
_LOGGER.info("腾讯云凭据验证通过")
return {"success": True} return {"success": True}
except AuthenticationError as ex: except AuthenticationError as ex:
_LOGGER.error("腾讯云认证失败: %s", ex) _LOGGER.error("腾讯云认证失败: %s", ex)
return { return {
@@ -197,7 +179,7 @@ class TencentFaceRecognitionConfigFlow(config_entries.ConfigFlow, domain=DOMAIN)
} }
} }
except Exception as ex: except Exception as ex:
_LOGGER.error("腾讯云凭据验证失败: %s", ex, exc_info=True) _LOGGER.error("腾讯云凭据验证失败: %s", ex)
return { return {
"success": False, "success": False,
"errors": { "errors": {
@@ -215,9 +197,10 @@ class TencentFaceRecognitionConfigFlow(config_entries.ConfigFlow, domain=DOMAIN)
class TencentFaceRecognitionOptionsFlow(config_entries.OptionsFlow): class TencentFaceRecognitionOptionsFlow(config_entries.OptionsFlow):
"""腾讯云人脸识别选项流程""" """腾讯云人脸识别选项流程"""
AVAILABLE_REGIONS = TencentFaceRecognitionConfigFlow.AVAILABLE_REGIONS
def __init__(self, config_entry): def __init__(self, config_entry):
"""初始化选项流程""" """初始化选项流程"""
# 存储需要的数据而不是整个 config_entry 实例
self._entry_id = config_entry.entry_id self._entry_id = config_entry.entry_id
self._title = config_entry.title self._title = config_entry.title
self._data = dict(config_entry.data) self._data = dict(config_entry.data)
@@ -227,17 +210,16 @@ class TencentFaceRecognitionOptionsFlow(config_entries.OptionsFlow):
"""管理选项流程""" """管理选项流程"""
if user_input is not None: if user_input is not None:
action = user_input.get("action") action = user_input.get("action")
if action == "config": if action == "config":
return await self.async_step_config() return await self.async_step_config()
elif action == "test_connection": elif action == "test_connection":
return await self.async_step_test_connection() return await self.async_step_test_connection()
elif action == "reauth": elif action == "reauth":
return await self.async_step_reauth() return await self.async_step_reauth()
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="init",
data_schema=vol.Schema({ data_schema=vol.Schema({
@@ -259,28 +241,41 @@ class TencentFaceRecognitionOptionsFlow(config_entries.OptionsFlow):
async def async_step_config(self, user_input=None): async def async_step_config(self, user_input=None):
"""配置设置步骤""" """配置设置步骤"""
errors = {} errors = {}
if user_input is not None: if user_input is not None:
# 验证区域
region = user_input.get(CONF_REGION) region = user_input.get(CONF_REGION)
if region not in AVAILABLE_REGIONS: if region not in self.AVAILABLE_REGIONS:
errors[CONF_REGION] = f"无效的区域,请从列表中选择有效的区域" 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: if not errors:
# 验证新配置是否有效
try: try:
# 创建临时客户端进行验证 client = TencentCloudClient(
temp_client = TencentCloudClient(
self._data.get(CONF_SECRET_ID), self._data.get(CONF_SECRET_ID),
self._data.get(CONF_SECRET_KEY), self._data.get(CONF_SECRET_KEY),
region region
) )
await self.hass.async_add_executor_job(client.verify_credentials)
_LOGGER.info("配置验证成功") _LOGGER.info("配置验证成功")
# 保存配置 new_data = dict(self._data)
return self.async_create_entry(title="", data=user_input) 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: except AuthenticationError as ex:
_LOGGER.error("腾讯云认证失败: %s", ex) _LOGGER.error("腾讯云认证失败: %s", ex)
errors[CONF_REGION] = "认证失败,请检查区域设置" errors[CONF_REGION] = "认证失败,请检查区域设置"
@@ -290,7 +285,7 @@ class TencentFaceRecognitionOptionsFlow(config_entries.OptionsFlow):
errors[CONF_REGION] = "网络错误,请检查区域设置" errors[CONF_REGION] = "网络错误,请检查区域设置"
errors["base"] = "连接腾讯云失败,请检查网络连接和区域设置" errors["base"] = "连接腾讯云失败,请检查网络连接和区域设置"
except Exception as ex: except Exception as ex:
_LOGGER.error("配置验证失败: %s", ex, exc_info=True) _LOGGER.error("配置验证失败: %s", ex)
errors["base"] = f"配置验证失败: {str(ex)}" errors["base"] = f"配置验证失败: {str(ex)}"
options = { options = {
@@ -299,7 +294,7 @@ class TencentFaceRecognitionOptionsFlow(config_entries.OptionsFlow):
default=self._options.get(CONF_REGION, self._data.get(CONF_REGION, DEFAULT_REGION)) default=self._options.get(CONF_REGION, self._data.get(CONF_REGION, DEFAULT_REGION))
): selector({ ): selector({
"select": { "select": {
"options": [{"label": region, "value": region} for region in AVAILABLE_REGIONS], "options": [{"label": region, "value": region} for region in self.AVAILABLE_REGIONS],
"mode": "dropdown" "mode": "dropdown"
} }
}), }),
@@ -314,36 +309,31 @@ class TencentFaceRecognitionOptionsFlow(config_entries.OptionsFlow):
data_schema=vol.Schema(options), data_schema=vol.Schema(options),
errors=errors, errors=errors,
) )
async def async_step_test_connection(self, user_input=None): async def async_step_test_connection(self, user_input=None):
"""测试连接步骤""" """测试连接步骤"""
errors = {} errors = {}
connection_status = "未知" connection_status = "未知"
if user_input is None: if user_input is None:
# 尝试测试连接
try: try:
# 使用当前配置创建客户端
client = TencentCloudClient( client = TencentCloudClient(
self._data.get(CONF_SECRET_ID), self._data.get(CONF_SECRET_ID),
self._data.get(CONF_SECRET_KEY), self._data.get(CONF_SECRET_KEY),
self._data.get(CONF_REGION, DEFAULT_REGION) 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( success = await self.hass.async_add_executor_job(
client._test_api_connection, client._test_api_connection,
group_id self._data.get(CONF_PERSON_GROUP_ID, "Hass")
) )
if not success: if not success:
raise Exception("连接测试失败,请检查人员库ID是否正确") raise Exception("连接测试失败,请检查人员库ID是否正确")
connection_status = "连接成功" connection_status = "连接成功"
_LOGGER.info("连接测试成功") _LOGGER.info("连接测试成功")
except AuthenticationError as ex: except AuthenticationError as ex:
connection_status = f"认证失败: {str(ex)}" connection_status = f"认证失败: {str(ex)}"
errors["base"] = "认证失败,请检查Secret ID和Secret Key" errors["base"] = "认证失败,请检查Secret ID和Secret Key"
@@ -355,8 +345,8 @@ class TencentFaceRecognitionOptionsFlow(config_entries.OptionsFlow):
except Exception as ex: except Exception as ex:
connection_status = f"测试失败: {str(ex)}" connection_status = f"测试失败: {str(ex)}"
errors["base"] = f"连接测试失败: {str(ex)}" errors["base"] = f"连接测试失败: {str(ex)}"
_LOGGER.error("连接测试失败: %s", ex, exc_info=True) _LOGGER.error("连接测试失败: %s", ex)
return self.async_show_form( return self.async_show_form(
step_id="test_connection", step_id="test_connection",
data_schema=vol.Schema({}), data_schema=vol.Schema({}),
@@ -365,18 +355,16 @@ class TencentFaceRecognitionOptionsFlow(config_entries.OptionsFlow):
"connection_status": connection_status "connection_status": connection_status
} }
) )
async def async_step_reauth(self, user_input=None): async def async_step_reauth(self, user_input=None):
"""重新认证步骤""" """重新认证步骤"""
errors = {} errors = {}
if user_input is not None: if user_input is not None:
# 验证新凭据
try: try:
# 验证输入格式
temp_errors = {} temp_errors = {}
self._validate_input_format(user_input, temp_errors) self._validate_input_format(user_input, temp_errors)
if temp_errors: if temp_errors:
errors = temp_errors errors = temp_errors
return self.async_show_form( return self.async_show_form(
@@ -387,28 +375,27 @@ class TencentFaceRecognitionOptionsFlow(config_entries.OptionsFlow):
}), }),
errors=errors, errors=errors,
) )
# 创建腾讯云客户端验证新凭据
new_client = TencentCloudClient( new_client = TencentCloudClient(
user_input[CONF_SECRET_ID], user_input[CONF_SECRET_ID],
user_input[CONF_SECRET_KEY], user_input[CONF_SECRET_KEY],
self._data.get(CONF_REGION, DEFAULT_REGION) self._data.get(CONF_REGION, DEFAULT_REGION)
) )
await self.hass.async_add_executor_job(new_client.verify_credentials)
_LOGGER.info("重新认证成功") _LOGGER.info("重新认证成功")
# 更新配置
new_data = dict(self._data) new_data = dict(self._data)
new_data.update(user_input) new_data.update(user_input)
# 重新创建配置项 entry = self.hass.config_entries.async_get_entry(self._entry_id)
self.hass.config_entries.async_update_entry( self.hass.config_entries.async_update_entry(entry, data=new_data)
self.hass.config_entries.async_get_entry(self._entry_id),
data=new_data await self.hass.config_entries.async_reload(entry.entry_id)
)
return self.async_create_entry(title="", data={}) return self.async_create_entry(title="", data={})
except AuthenticationError as ex: except AuthenticationError as ex:
_LOGGER.error("重新认证失败: %s", ex) _LOGGER.error("重新认证失败: %s", ex)
errors["base"] = "认证失败,请检查Secret ID和Secret Key" errors["base"] = "认证失败,请检查Secret ID和Secret Key"
@@ -416,9 +403,9 @@ class TencentFaceRecognitionOptionsFlow(config_entries.OptionsFlow):
_LOGGER.error("重新认证网络错误: %s", ex) _LOGGER.error("重新认证网络错误: %s", ex)
errors["base"] = "网络错误,请检查网络连接和区域设置" errors["base"] = "网络错误,请检查网络连接和区域设置"
except Exception as ex: except Exception as ex:
_LOGGER.error("重新认证失败: %s", ex, exc_info=True) _LOGGER.error("重新认证失败: %s", ex)
errors["base"] = f"重新认证失败: {str(ex)}" errors["base"] = f"重新认证失败: {str(ex)}"
return self.async_show_form( return self.async_show_form(
step_id="reauth", step_id="reauth",
data_schema=vol.Schema({ data_schema=vol.Schema({
@@ -427,19 +414,17 @@ class TencentFaceRecognitionOptionsFlow(config_entries.OptionsFlow):
}), }),
errors=errors, errors=errors,
description_placeholders={ 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) "current_region": self._data.get(CONF_REGION, DEFAULT_REGION)
} }
) )
def _validate_input_format(self, user_input: Dict[str, Any], errors: Dict[str, str]) -> None: 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, "") 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个以上字符" errors[CONF_SECRET_ID] = "Secret ID格式不正确,应以AKID开头,后跟36个以上字符"
# 验证Secret Key格式
secret_key = user_input.get(CONF_SECRET_KEY, "") secret_key = user_input.get(CONF_SECRET_KEY, "")
if not SECRET_KEY_REGEX.match(secret_key): if not TencentFaceRecognitionConfigFlow.SECRET_KEY_REGEX.match(secret_key):
errors[CONF_SECRET_KEY] = "Secret Key格式不正确,应为20个以上字符" errors[CONF_SECRET_KEY] = "Secret Key格式不正确,应为20个以上字符"
+19 -12
View File
@@ -1,15 +1,13 @@
"""腾讯云人脸识别插件常量定义""" """腾讯云人脸识别插件常量定义"""
DOMAIN = "tencent_face_recognition" DOMAIN = "tencent_face_recognition"
PLATFORMS = [] PLATFORMS = ["sensor"]
# 配置项键
CONF_SECRET_ID = "secret_id" CONF_SECRET_ID = "secret_id"
CONF_SECRET_KEY = "secret_key" CONF_SECRET_KEY = "secret_key"
CONF_REGION = "region" CONF_REGION = "region"
CONF_PERSON_GROUP_ID = "person_group_id" CONF_PERSON_GROUP_ID = "person_group_id"
# 默认值
DEFAULT_REGION = "ap-shanghai" DEFAULT_REGION = "ap-shanghai"
DEFAULT_PERSON_GROUP_ID = "Hass" DEFAULT_PERSON_GROUP_ID = "Hass"
DEFAULT_MAX_FACE_NUM = 1 DEFAULT_MAX_FACE_NUM = 1
@@ -19,37 +17,46 @@ DEFAULT_QUALITY_CONTROL = 1
DEFAULT_NEED_ROTATE_CHECK = 1 DEFAULT_NEED_ROTATE_CHECK = 1
DEFAULT_FACE_MATCH_THRESHOLD = 60.0 DEFAULT_FACE_MATCH_THRESHOLD = 60.0
# 服务
SERVICE_FACE_SEARCH = "face_search" SERVICE_FACE_SEARCH = "face_search"
SERVICE_DETECT_FACE = "detect_face" SERVICE_DETECT_FACE = "detect_face"
SERVICE_GET_FACE_ATTRIBUTES = "get_face_attributes" SERVICE_GET_FACE_ATTRIBUTES = "get_face_attributes"
SERVICE_CREATE_PERSON = "create_person"
SERVICE_DELETE_PERSON = "delete_person"
SERVICE_CREATE_FACE = "create_face"
SERVICE_DELETE_FACE = "delete_face"
# 服务参数
ATTR_IMAGE_URL = "image_url" ATTR_IMAGE_URL = "image_url"
ATTR_IMAGE_PATH = "image_path" ATTR_IMAGE_PATH = "image_path"
ATTR_IMAGE_FILE = "image_file" ATTR_IMAGE_FILE = "image_file"
ATTR_CAMERA_ENTITY_ID = "camera_entity_id"
ATTR_CONFIG_ENTRY_ID = "config_entry_id"
ATTR_GROUP_ID = "group_id"
ATTR_MAX_FACE_NUM = "max_face_num" ATTR_MAX_FACE_NUM = "max_face_num"
ATTR_MIN_FACE_SIZE = "min_face_size" ATTR_MIN_FACE_SIZE = "min_face_size"
ATTR_MAX_USER_NUM = "max_user_num" ATTR_MAX_USER_NUM = "max_user_num"
ATTR_QUALITY_CONTROL = "quality_control" ATTR_QUALITY_CONTROL = "quality_control"
ATTR_NEED_ROTATE_CHECK = "need_rotate_check" ATTR_NEED_ROTATE_CHECK = "need_rotate_check"
ATTR_FACE_MATCH_THRESHOLD = "face_match_threshold" ATTR_FACE_MATCH_THRESHOLD = "face_match_threshold"
ATTR_PERSON_ID = "person_id"
ATTR_PERSON_NAME = "person_name"
ATTR_GENDER = "gender"
ATTR_FACE_ID = "face_id"
ATTR_PERSON_TAG = "person_tag"
EVENT_FACE_DETECTED = "face_detected"
# 错误消息
ERROR_INVALID_CONFIG = "无效的配置" ERROR_INVALID_CONFIG = "无效的配置"
ERROR_API_ERROR = "API调用错误" ERROR_API_ERROR = "API调用错误"
ERROR_IMAGE_NOT_FOUND = "图片未找到" ERROR_IMAGE_NOT_FOUND = "图片未找到"
ERROR_FACE_NOT_DETECTED = "未检测到人脸" ERROR_FACE_NOT_DETECTED = "未检测到人脸"
# 状态
STATE_CONNECTED = "已连接" STATE_CONNECTED = "已连接"
STATE_DISCONNECTED = "未连接" STATE_DISCONNECTED = "未连接"
STATE_CONNECTING = "连接中" STATE_CONNECTING = "连接中"
STATE_ERROR = "连接错误" STATE_ERROR = "连接错误"
STATE_PARTIALLY_CONNECTED = "部分连接" STATE_PARTIALLY_CONNECTED = "部分连接"
# 日志级别
LOG_LEVEL_DEBUG = "DEBUG" def get_entry_value(entry, key, default=None):
LOG_LEVEL_INFO = "INFO" """Get a config value from entry.options with fallback to entry.data."""
LOG_LEVEL_WARNING = "WARNING" return entry.options.get(key, entry.data.get(key, default))
LOG_LEVEL_ERROR = "ERROR"
+57 -86
View File
@@ -5,19 +5,18 @@ from typing import Dict, Any, Optional
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError
from .const import CONF_SECRET_ID, CONF_SECRET_KEY, CONF_REGION, CONF_PERSON_GROUP_ID
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
# 错误消息本地化
ERROR_MESSAGES = { ERROR_MESSAGES = {
"zh_CN": { "zh_CN": {
# 配置错误
"missing_config": "缺少必需的配置项: {key}", "missing_config": "缺少必需的配置项: {key}",
"invalid_secret_id": "Secret ID 必须是非空字符串,且应以 AKID 开头", "invalid_secret_id": "Secret ID 必须是非空字符串,且应以 AKID 开头",
"invalid_secret_key": "Secret Key 必须是非空字符串", "invalid_secret_key": "Secret Key 必须是非空字符串",
"invalid_region": "区域必须是非空字符串,且为有效的腾讯云区域", "invalid_region": "区域必须是非空字符串,且为有效的腾讯云区域",
"invalid_person_group_id": "人员库ID必须是非空字符串,只能包含字母、数字、连字符和下划线", "invalid_person_group_id": "人员库ID必须是非空字符串,只能包含字母、数字、连字符和下划线",
# 图片错误
"image_not_found": "图片未找到: {path}", "image_not_found": "图片未找到: {path}",
"image_too_large": "图片文件过大: {path}, 大小: {size} 字节,最大支持: {max_size}", "image_too_large": "图片文件过大: {path}, 大小: {size} 字节,最大支持: {max_size}",
"image_format_not_supported": "不支持的图片格式: {format}, 支持的格式: {supported_formats}", "image_format_not_supported": "不支持的图片格式: {format}, 支持的格式: {supported_formats}",
@@ -26,43 +25,35 @@ ERROR_MESSAGES = {
"image_download_error": "下载图片失败: {url}, 错误: {error}", "image_download_error": "下载图片失败: {url}, 错误: {error}",
"invalid_image_url": "无效的图片URL: {url}", "invalid_image_url": "无效的图片URL: {url}",
"permission_denied": "没有权限读取图片文件: {path}", "permission_denied": "没有权限读取图片文件: {path}",
# 人脸检测错误
"face_not_detected": "未检测到人脸,请确保图片中包含清晰的人脸", "face_not_detected": "未检测到人脸,请确保图片中包含清晰的人脸",
"face_too_small": "人脸太小,请使用包含更大人脸的图片", "face_too_small": "人脸太小,请使用包含更大人脸的图片",
"face_quality_poor": "人脸质量较差,请使用更清晰的图片", "face_quality_poor": "人脸质量较差,请使用更清晰的图片",
# API错误
"api_error": "API调用失败: {code} - {message}", "api_error": "API调用失败: {code} - {message}",
"person_not_found": "人员未找到: {person_id}", "person_not_found": "人员未找到: {person_id}",
"group_not_found": "人员库未找到: {group_id}", "group_not_found": "人员库未找到: {group_id}",
"face_not_found": "人脸未找到: {face_id}", "face_not_found": "人脸未找到: {face_id}",
# 网络错误
"network_error": "网络连接错误,请检查网络连接", "network_error": "网络连接错误,请检查网络连接",
"timeout_error": "请求超时,请稍后重试", "timeout_error": "请求超时,请稍后重试",
# 认证错误
"auth_failure": "认证失败,请检查Secret ID和Secret Key是否正确", "auth_failure": "认证失败,请检查Secret ID和Secret Key是否正确",
"auth_expired": "认证已过期,请更新凭证", "auth_expired": "认证已过期,请更新凭证",
# 限制错误
"rate_limit_exceeded": "API调用频率超限,请稍后重试", "rate_limit_exceeded": "API调用频率超限,请稍后重试",
"quota_exceeded": "API调用配额已用完,请等待配额重置或升级套餐", "quota_exceeded": "API调用配额已用完,请等待配额重置或升级套餐",
# 通用错误
"unknown_error": "未知错误: {message}", "unknown_error": "未知错误: {message}",
"invalid_parameter": "无效参数: {parameter}", "invalid_parameter": "无效参数: {parameter}",
}, },
"en_US": { "en_US": {
# Configuration errors
"missing_config": "Missing required configuration: {key}", "missing_config": "Missing required configuration: {key}",
"invalid_secret_id": "Secret ID must be a non-empty string starting with AKID", "invalid_secret_id": "Secret ID must be a non-empty string starting with AKID",
"invalid_secret_key": "Secret Key must be a non-empty string", "invalid_secret_key": "Secret Key must be a non-empty string",
"invalid_region": "Region must be a non-empty string and a valid Tencent Cloud region", "invalid_region": "Region must be a non-empty string and a valid Tencent Cloud region",
"invalid_person_group_id": "Person Group ID must be a non-empty string containing only letters, numbers, hyphens, and underscores", "invalid_person_group_id": "Person Group ID must be a non-empty string containing only letters, numbers, hyphens, and underscores",
# Image errors
"image_not_found": "Image not found: {path}", "image_not_found": "Image not found: {path}",
"image_too_large": "Image file too large: {path}, size: {size} bytes, maximum supported: {max_size}", "image_too_large": "Image file too large: {path}, size: {size} bytes, maximum supported: {max_size}",
"image_format_not_supported": "Unsupported image format: {format}, supported formats: {supported_formats}", "image_format_not_supported": "Unsupported image format: {format}, supported formats: {supported_formats}",
@@ -71,37 +62,30 @@ ERROR_MESSAGES = {
"image_download_error": "Image download failed: {url}, error: {error}", "image_download_error": "Image download failed: {url}, error: {error}",
"invalid_image_url": "Invalid image URL: {url}", "invalid_image_url": "Invalid image URL: {url}",
"permission_denied": "Permission denied when reading image file: {path}", "permission_denied": "Permission denied when reading image file: {path}",
# Face detection errors
"face_not_detected": "No face detected, please ensure the image contains clear faces", "face_not_detected": "No face detected, please ensure the image contains clear faces",
"face_too_small": "Face too small, please use an image with larger faces", "face_too_small": "Face too small, please use an image with larger faces",
"face_quality_poor": "Poor face quality, please use a clearer image", "face_quality_poor": "Poor face quality, please use a clearer image",
# API errors
"api_error": "API call failed: {code} - {message}", "api_error": "API call failed: {code} - {message}",
"person_not_found": "Person not found: {person_id}", "person_not_found": "Person not found: {person_id}",
"group_not_found": "Person group not found: {group_id}", "group_not_found": "Person group not found: {group_id}",
"face_not_found": "Face not found: {face_id}", "face_not_found": "Face not found: {face_id}",
# Network errors
"network_error": "Network connection error, please check your network connection", "network_error": "Network connection error, please check your network connection",
"timeout_error": "Request timeout, please try again later", "timeout_error": "Request timeout, please try again later",
# Authentication errors
"auth_failure": "Authentication failed, please check if Secret ID and Secret Key are correct", "auth_failure": "Authentication failed, please check if Secret ID and Secret Key are correct",
"auth_expired": "Authentication expired, please update credentials", "auth_expired": "Authentication expired, please update credentials",
# Limit errors
"rate_limit_exceeded": "API call rate limit exceeded, please try again later", "rate_limit_exceeded": "API call rate limit exceeded, please try again later",
"quota_exceeded": "API call quota used up, please wait for quota reset or upgrade plan", "quota_exceeded": "API call quota used up, please wait for quota reset or upgrade plan",
# General errors
"unknown_error": "Unknown error: {message}", "unknown_error": "Unknown error: {message}",
"invalid_parameter": "Invalid parameter: {parameter}", "invalid_parameter": "Invalid parameter: {parameter}",
} }
} }
# 错误解决建议
ERROR_SOLUTIONS = { ERROR_SOLUTIONS = {
"zh_CN": { "zh_CN": {
"missing_config": "请在配置中添加缺少的配置项", "missing_config": "请在配置中添加缺少的配置项",
@@ -109,7 +93,7 @@ ERROR_SOLUTIONS = {
"invalid_secret_key": "请检查Secret Key格式,确保输入正确", "invalid_secret_key": "请检查Secret Key格式,确保输入正确",
"invalid_region": "请选择有效的腾讯云区域,如ap-shanghai", "invalid_region": "请选择有效的腾讯云区域,如ap-shanghai",
"invalid_person_group_id": "请使用字母、数字、连字符和下划线组合", "invalid_person_group_id": "请使用字母、数字、连字符和下划线组合",
"image_not_found": "请检查图片路径是否正确", "image_not_found": "请检查图片路径是否正确",
"image_too_large": "请使用小于10MB的图片,或对图片进行压缩", "image_too_large": "请使用小于10MB的图片,或对图片进行压缩",
"image_format_not_supported": "请将图片转换为JPG、PNG、BMP或GIF格式", "image_format_not_supported": "请将图片转换为JPG、PNG、BMP或GIF格式",
@@ -118,25 +102,25 @@ ERROR_SOLUTIONS = {
"image_download_error": "请检查图片URL是否有效,或稍后重试", "image_download_error": "请检查图片URL是否有效,或稍后重试",
"invalid_image_url": "请提供有效的图片URL", "invalid_image_url": "请提供有效的图片URL",
"permission_denied": "请检查文件权限,或使用其他图片", "permission_denied": "请检查文件权限,或使用其他图片",
"face_not_detected": "请使用包含清晰人脸的图片", "face_not_detected": "请使用包含清晰人脸的图片",
"face_too_small": "请使用人脸更大的图片,或调整拍摄距离", "face_too_small": "请使用人脸更大的图片,或调整拍摄距离",
"face_quality_poor": "请使用光线充足、对焦清晰的图片", "face_quality_poor": "请使用光线充足、对焦清晰的图片",
"api_error": "请稍后重试,如果问题持续,请联系技术支持", "api_error": "请稍后重试,如果问题持续,请联系技术支持",
"person_not_found": "请检查人员ID是否正确,或确保人员已添加到人员库", "person_not_found": "请检查人员ID是否正确,或确保人员已添加到人员库",
"group_not_found": "请检查人员库ID是否正确,或创建新的人员库", "group_not_found": "请检查人员库ID是否正确,或创建新的人员库",
"face_not_found": "请检查人脸ID是否正确", "face_not_found": "请检查人脸ID是否正确",
"network_error": "请检查网络连接,确保网络通畅", "network_error": "请检查网络连接,确保网络通畅",
"timeout_error": "请稍后重试,或在网络状况良好时使用", "timeout_error": "请稍后重试,或在网络状况良好时使用",
"auth_failure": "请检查Secret ID和Secret Key是否正确", "auth_failure": "请检查Secret ID和Secret Key是否正确",
"auth_expired": "请更新腾讯云凭证", "auth_expired": "请更新腾讯云凭证",
"rate_limit_exceeded": "请降低调用频率,稍后重试", "rate_limit_exceeded": "请降低调用频率,稍后重试",
"quota_exceeded": "请等待配额重置,或升级腾讯云套餐", "quota_exceeded": "请等待配额重置,或升级腾讯云套餐",
"unknown_error": "请记录错误信息并联系技术支持", "unknown_error": "请记录错误信息并联系技术支持",
"invalid_parameter": "请检查API调用参数是否正确", "invalid_parameter": "请检查API调用参数是否正确",
}, },
@@ -146,7 +130,7 @@ ERROR_SOLUTIONS = {
"invalid_secret_key": "Please check the Secret Key format, ensure it's entered correctly", "invalid_secret_key": "Please check the Secret Key format, ensure it's entered correctly",
"invalid_region": "Please select a valid Tencent Cloud region, such as ap-shanghai", "invalid_region": "Please select a valid Tencent Cloud region, such as ap-shanghai",
"invalid_person_group_id": "Please use a combination of letters, numbers, hyphens, and underscores", "invalid_person_group_id": "Please use a combination of letters, numbers, hyphens, and underscores",
"image_not_found": "Please check if the image path is correct", "image_not_found": "Please check if the image path is correct",
"image_too_large": "Please use an image smaller than 10MB, or compress the image", "image_too_large": "Please use an image smaller than 10MB, or compress the image",
"image_format_not_supported": "Please convert the image to JPG, PNG, BMP, or GIF format", "image_format_not_supported": "Please convert the image to JPG, PNG, BMP, or GIF format",
@@ -155,25 +139,25 @@ ERROR_SOLUTIONS = {
"image_download_error": "Please check if the image URL is valid, or try again later", "image_download_error": "Please check if the image URL is valid, or try again later",
"invalid_image_url": "Please provide a valid image URL", "invalid_image_url": "Please provide a valid image URL",
"permission_denied": "Please check file permissions, or use a different image", "permission_denied": "Please check file permissions, or use a different image",
"face_not_detected": "Please use an image that contains clear faces", "face_not_detected": "Please use an image that contains clear faces",
"face_too_small": "Please use an image with larger faces, or adjust the shooting distance", "face_too_small": "Please use an image with larger faces, or adjust the shooting distance",
"face_quality_poor": "Please use a well-lit and focused image", "face_quality_poor": "Please use a well-lit and focused image",
"api_error": "Please try again later, if the problem persists, please contact technical support", "api_error": "Please try again later, if the problem persists, please contact technical support",
"person_not_found": "Please check if the person ID is correct, or ensure the person has been added to the person group", "person_not_found": "Please check if the person ID is correct, or ensure the person has been added to the person group",
"group_not_found": "Please check if the person group ID is correct, or create a new person group", "group_not_found": "Please check if the person group ID is correct, or create a new person group",
"face_not_found": "Please check if the face ID is correct", "face_not_found": "Please check if the face ID is correct",
"network_error": "Please check your network connection, ensure it's working properly", "network_error": "Please check your network connection, ensure it's working properly",
"timeout_error": "Please try again later, or use when network conditions are good", "timeout_error": "Please try again later, or use when network conditions are good",
"auth_failure": "Please check if Secret ID and Secret Key are correct", "auth_failure": "Please check if Secret ID and Secret Key are correct",
"auth_expired": "Please update Tencent Cloud credentials", "auth_expired": "Please update Tencent Cloud credentials",
"rate_limit_exceeded": "Please reduce call frequency and try again later", "rate_limit_exceeded": "Please reduce call frequency and try again later",
"quota_exceeded": "Please wait for quota reset, or upgrade Tencent Cloud plan", "quota_exceeded": "Please wait for quota reset, or upgrade Tencent Cloud plan",
"unknown_error": "Please record the error information and contact technical support", "unknown_error": "Please record the error information and contact technical support",
"invalid_parameter": "Please check if the API call parameters are correct", "invalid_parameter": "Please check if the API call parameters are correct",
} }
@@ -182,7 +166,7 @@ ERROR_SOLUTIONS = {
class TencentFaceRecognitionError(HomeAssistantError): class TencentFaceRecognitionError(HomeAssistantError):
"""腾讯云人脸识别基础错误类""" """腾讯云人脸识别基础错误类"""
def __init__( def __init__(
self, self,
message: str, message: str,
@@ -190,37 +174,30 @@ class TencentFaceRecognitionError(HomeAssistantError):
error_details: Optional[Dict[str, Any]] = None, error_details: Optional[Dict[str, Any]] = None,
locale: str = "zh_CN" locale: str = "zh_CN"
): ):
"""初始化错误"""
super().__init__(message) super().__init__(message)
self.error_key = error_key self.error_key = error_key
self.error_details = error_details or {} self.error_details = error_details or {}
self.locale = locale self.locale = locale
# 获取本地化消息
self.localized_message = self._get_localized_message() self.localized_message = self._get_localized_message()
# 获取解决建议
self.solution = self._get_solution() self.solution = self._get_solution()
def _get_localized_message(self) -> str: def _get_localized_message(self) -> str:
"""获取本地化错误消息"""
if not self.error_key: if not self.error_key:
return str(self) return str(self)
messages = ERROR_MESSAGES.get(self.locale, ERROR_MESSAGES["zh_CN"]) messages = ERROR_MESSAGES.get(self.locale, ERROR_MESSAGES["zh_CN"])
template = messages.get(self.error_key, str(self)) template = messages.get(self.error_key, str(self))
try: try:
# 使用错误详情填充模板
return template.format(**self.error_details) return template.format(**self.error_details)
except (KeyError, ValueError): except (KeyError, ValueError):
# 如果填充失败,返回原始消息
return str(self) return str(self)
def _get_solution(self) -> str: def _get_solution(self) -> str:
"""获取错误解决建议"""
if not self.error_key: if not self.error_key:
return "" return ""
solutions = ERROR_SOLUTIONS.get(self.locale, ERROR_SOLUTIONS["zh_CN"]) solutions = ERROR_SOLUTIONS.get(self.locale, ERROR_SOLUTIONS["zh_CN"])
return solutions.get(self.error_key, "") return solutions.get(self.error_key, "")
@@ -293,19 +270,18 @@ def handle_api_error(error_code: str, error_message: str) -> None:
"RequestQuotaExceeded": (QuotaExceededError, "quota_exceeded"), "RequestQuotaExceeded": (QuotaExceededError, "quota_exceeded"),
"ResourceUnavailable.NetworkError": (NetworkError, "network_error"), "ResourceUnavailable.NetworkError": (NetworkError, "network_error"),
} }
error_info = error_mapping.get(error_code, (ApiError, "api_error")) error_info = error_mapping.get(error_code, (ApiError, "api_error"))
error_class = error_info[0] error_class = error_info[0]
error_key = error_info[1] error_key = error_info[1]
_LOGGER.error("腾讯云API错误: %s - %s", error_code, error_message) _LOGGER.error("腾讯云API错误: %s - %s", error_code, error_message)
# 创建带有本地化消息的错误
error_details = { error_details = {
"code": error_code, "code": error_code,
"message": error_message "message": error_message
} }
raise error_class( raise error_class(
f"{error_code}: {error_message}", f"{error_code}: {error_message}",
error_key=error_key, error_key=error_key,
@@ -316,11 +292,10 @@ def handle_api_error(error_code: str, error_message: str) -> None:
def log_and_raise_error(error_class, message: str, details: Dict[str, Any] = None, error_key: Optional[str] = None) -> None: def log_and_raise_error(error_class, message: str, details: Dict[str, Any] = None, error_key: Optional[str] = None) -> None:
"""记录日志并抛出错误""" """记录日志并抛出错误"""
if details: if details:
_LOGGER.error("%s: %s, 详情: %s", error_class.__name__, message, details) _LOGGER.error("%s: %s", error_class.__name__, message)
else: else:
_LOGGER.error("%s: %s", error_class.__name__, message) _LOGGER.error("%s: %s", error_class.__name__, message)
# 创建带有本地化消息的错误
raise error_class( raise error_class(
message, message,
error_key=error_key, error_key=error_key,
@@ -330,8 +305,8 @@ def log_and_raise_error(error_class, message: str, details: Dict[str, Any] = Non
def validate_config(config: Dict[str, Any]) -> None: def validate_config(config: Dict[str, Any]) -> None:
"""验证配置""" """验证配置"""
required_keys = ["secret_id", "secret_key"] required_keys = [CONF_SECRET_ID, CONF_SECRET_KEY]
for key in required_keys: for key in required_keys:
if not config.get(key): if not config.get(key):
log_and_raise_error( log_and_raise_error(
@@ -340,29 +315,26 @@ def validate_config(config: Dict[str, Any]) -> None:
{"key": key}, {"key": key},
"missing_config" "missing_config"
) )
# 验证Secret ID格式 secret_id = config.get(CONF_SECRET_ID)
secret_id = config.get("secret_id")
if not isinstance(secret_id, str) or len(secret_id.strip()) == 0: if not isinstance(secret_id, str) or len(secret_id.strip()) == 0:
log_and_raise_error( log_and_raise_error(
InvalidConfigError, InvalidConfigError,
"Secret ID必须是非空字符串,且应以 AKID 开头", "Secret ID必须是非空字符串,且应以 AKID 开头",
{"secret_id": str(secret_id)}, {"secret_id": "***"},
"invalid_secret_id" "invalid_secret_id"
) )
# 验证Secret Key格式 secret_key = config.get(CONF_SECRET_KEY)
secret_key = config.get("secret_key")
if not isinstance(secret_key, str) or len(secret_key.strip()) == 0: if not isinstance(secret_key, str) or len(secret_key.strip()) == 0:
log_and_raise_error( log_and_raise_error(
InvalidConfigError, InvalidConfigError,
"Secret Key必须是非空字符串", "Secret Key必须是非空字符串",
{"secret_key": str(secret_key)}, {"secret_key": "***"},
"invalid_secret_key" "invalid_secret_key"
) )
# 验证区域格式 region = config.get(CONF_REGION, "ap-shanghai")
region = config.get("region", "ap-shanghai")
if not isinstance(region, str) or len(region.strip()) == 0: if not isinstance(region, str) or len(region.strip()) == 0:
log_and_raise_error( log_and_raise_error(
InvalidConfigError, InvalidConfigError,
@@ -370,9 +342,8 @@ def validate_config(config: Dict[str, Any]) -> None:
{"region": str(region)}, {"region": str(region)},
"invalid_region" "invalid_region"
) )
# 验证人员库ID格式 person_group_id = config.get(CONF_PERSON_GROUP_ID, "Hass")
person_group_id = config.get("person_group_id", "Hass")
if not isinstance(person_group_id, str) or len(person_group_id.strip()) == 0: if not isinstance(person_group_id, str) or len(person_group_id.strip()) == 0:
log_and_raise_error( log_and_raise_error(
InvalidConfigError, InvalidConfigError,
@@ -380,5 +351,5 @@ def validate_config(config: Dict[str, Any]) -> None:
{"person_group_id": str(person_group_id)}, {"person_group_id": str(person_group_id)},
"invalid_person_group_id" "invalid_person_group_id"
) )
_LOGGER.debug("配置验证通过") _LOGGER.debug("配置验证通过")
+179 -78
View File
@@ -1,7 +1,7 @@
"""人脸识别核心功能""" """人脸识别核心功能"""
import base64
import logging import logging
import json
from typing import Dict, Any, List, Optional from typing import Dict, Any, List, Optional
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
@@ -10,14 +10,19 @@ from homeassistant.exceptions import HomeAssistantError
from .const import ( from .const import (
ATTR_IMAGE_URL, ATTR_IMAGE_URL,
ATTR_IMAGE_PATH, ATTR_IMAGE_PATH,
ATTR_CAMERA_ENTITY_ID,
ATTR_MAX_FACE_NUM, ATTR_MAX_FACE_NUM,
ATTR_MIN_FACE_SIZE, ATTR_MIN_FACE_SIZE,
ATTR_MAX_USER_NUM, ATTR_MAX_USER_NUM,
ATTR_QUALITY_CONTROL, ATTR_QUALITY_CONTROL,
ATTR_NEED_ROTATE_CHECK, ATTR_NEED_ROTATE_CHECK,
ATTR_FACE_MATCH_THRESHOLD, ATTR_FACE_MATCH_THRESHOLD,
ERROR_IMAGE_NOT_FOUND, ATTR_PERSON_ID,
ERROR_FACE_NOT_DETECTED, ATTR_PERSON_NAME,
ATTR_GENDER,
ATTR_FACE_ID,
ATTR_PERSON_TAG,
ATTR_GROUP_ID,
) )
from .tencent_cloud_client import TencentCloudClient from .tencent_cloud_client import TencentCloudClient
@@ -32,11 +37,50 @@ class FaceRecognition:
self.hass = hass self.hass = hass
self.client = client self.client = client
async def _get_image_base64_from_source(
self,
image_url: str = None,
image_path: str = None,
image_file: str = None,
camera_entity_id: str = None,
) -> str:
"""从各种来源获取图片 base64 编码"""
if camera_entity_id:
return await self._get_camera_image_base64(camera_entity_id)
if image_url or image_path or image_file:
return await self.hass.async_add_executor_job(
self.client._get_image_base64, image_url, image_path, image_file
)
raise ValueError("必须提供image_url、image_path、image_file或camera_entity_id")
async def _get_camera_image_base64(self, camera_entity_id: str) -> str:
"""从摄像头实体获取图片 base64"""
try:
from homeassistant.components.camera import async_get_image
image = await async_get_image(self.hass, camera_entity_id)
return base64.b64encode(image.content).decode("utf-8")
except ImportError:
raise HomeAssistantError("摄像头组件不可用,无法获取图片")
except Exception as ex:
raise HomeAssistantError(f"获取摄像头图片失败: {ex}")
def _validate_image_params(
self,
image_url: str = None,
image_path: str = None,
image_file: str = None,
camera_entity_id: str = None,
) -> None:
"""验证图片参数"""
if not any([image_url, image_path, image_file, camera_entity_id]):
raise ValueError("必须提供image_url、image_path、image_file或camera_entity_id")
async def async_search_faces( async def async_search_faces(
self, self,
image_url: str = None, image_url: str = None,
image_path: str = None, image_path: str = None,
image_file: str = None, image_file: str = None,
camera_entity_id: str = None,
group_ids: List[str] = None, group_ids: List[str] = None,
max_face_num: int = 1, max_face_num: int = 1,
min_face_size: int = 34, min_face_size: int = 34,
@@ -47,15 +91,15 @@ class FaceRecognition:
) -> Dict[str, Any]: ) -> Dict[str, Any]:
"""搜索人脸""" """搜索人脸"""
try: try:
# 验证参数 self._validate_image_params(image_url, image_path, image_file, camera_entity_id)
self._validate_image_params(image_url, image_path, image_file)
image_base64 = await self._get_image_base64_from_source(
image_url, image_path, image_file, camera_entity_id
)
# 调用腾讯云API搜索人脸
result = await self.hass.async_add_executor_job( result = await self.hass.async_add_executor_job(
self.client.search_faces, self.client.search_faces,
image_url, None, None, None, image_base64,
image_path,
image_file,
group_ids, group_ids,
max_face_num, max_face_num,
min_face_size, min_face_size,
@@ -65,7 +109,6 @@ class FaceRecognition:
face_match_threshold face_match_threshold
) )
# 处理结果
if result.get("success", False): if result.get("success", False):
processed_faces = self._process_search_results(result.get("faces", [])) processed_faces = self._process_search_results(result.get("faces", []))
return { return {
@@ -76,7 +119,6 @@ class FaceRecognition:
"error_message": None "error_message": None
} }
else: else:
# 返回错误信息
return { return {
"success": False, "success": False,
"faces": [], "faces": [],
@@ -94,6 +136,8 @@ class FaceRecognition:
"error_code": "invalid_parameter", "error_code": "invalid_parameter",
"error_message": str(ex) "error_message": str(ex)
} }
except HomeAssistantError:
raise
except Exception as ex: except Exception as ex:
_LOGGER.error("人脸搜索失败: %s", ex) _LOGGER.error("人脸搜索失败: %s", ex)
return { return {
@@ -104,11 +148,6 @@ class FaceRecognition:
"error_message": str(ex) "error_message": str(ex)
} }
def _validate_image_params(self, image_url: str, image_path: str, image_file: str) -> None:
"""验证图片参数"""
if not image_url and not image_path and not image_file:
raise ValueError("必须提供image_url、image_path或image_file")
def _process_search_results(self, results: List[Dict[str, Any]]) -> List[Dict[str, Any]]: def _process_search_results(self, results: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
"""处理人脸搜索结果""" """处理人脸搜索结果"""
processed_results = [] processed_results = []
@@ -130,74 +169,31 @@ class FaceRecognition:
processed_results.append(processed_result) processed_results.append(processed_result)
return processed_results return processed_results
def _process_detect_results(self, results: List[Dict[str, Any]], include_attributes: bool = False) -> List[Dict[str, Any]]:
"""处理人脸检测结果"""
processed_results = []
for result in results:
face_result = {
"face_id": result.get("face_id", ""),
"x": result.get("x", 0),
"y": result.get("y", 0),
"width": result.get("width", 0),
"height": result.get("height", 0),
"face_rect": result.get("face_rect")
}
# 如果需要包含属性信息
if include_attributes:
face_result.update({
"gender": result.get("gender"),
"age": result.get("age"),
"expression": result.get("expression"),
"beauty": result.get("beauty")
})
processed_results.append(face_result)
return processed_results
def _handle_api_error(self, operation: str, ex: Exception) -> None:
"""处理API错误"""
_LOGGER.error("%s失败: %s", operation, ex)
if isinstance(ex, ValueError):
raise HomeAssistantError(str(ex))
else:
raise HomeAssistantError(f"{operation}失败: {str(ex)}")
def _get_config_entry(self):
"""获取配置项"""
from homeassistant.config_entries import ConfigEntry
from . import DOMAIN
for entry in self.hass.config_entries.async_entries(DOMAIN):
return entry
return None
async def async_get_face_attributes( async def async_get_face_attributes(
self, self,
image_url: str = None, image_url: str = None,
image_path: str = None, image_path: str = None,
image_file: str = None, image_file: str = None,
camera_entity_id: str = None,
max_face_num: int = 1, max_face_num: int = 1,
need_rotate_check: int = 1 need_rotate_check: int = 1
) -> Dict[str, Any]: ) -> Dict[str, Any]:
"""获取人脸属性""" """获取人脸属性"""
try: try:
# 验证参数 self._validate_image_params(image_url, image_path, image_file, camera_entity_id)
self._validate_image_params(image_url, image_path, image_file)
image_base64 = await self._get_image_base64_from_source(
image_url, image_path, image_file, camera_entity_id
)
# 调用腾讯云API获取人脸属性
result = await self.hass.async_add_executor_job( result = await self.hass.async_add_executor_job(
self.client.get_face_attributes, self.client.get_face_attributes,
image_url, None, None, None, image_base64,
image_path,
image_file,
max_face_num, max_face_num,
need_rotate_check need_rotate_check
) )
# 处理结果
if result.get("success", False): if result.get("success", False):
return { return {
"success": True, "success": True,
@@ -207,7 +203,6 @@ class FaceRecognition:
"error_message": None "error_message": None
} }
else: else:
# 返回错误信息
return { return {
"success": False, "success": False,
"faces": [], "faces": [],
@@ -216,6 +211,8 @@ class FaceRecognition:
"error_message": result.get("error_message") "error_message": result.get("error_message")
} }
except HomeAssistantError:
raise
except Exception as ex: except Exception as ex:
_LOGGER.error("获取人脸属性失败: %s", ex) _LOGGER.error("获取人脸属性失败: %s", ex)
return { return {
@@ -226,34 +223,32 @@ class FaceRecognition:
"error_message": str(ex) "error_message": str(ex)
} }
# 移除 _get_face_attributes_sync 方法,因为现在直接使用客户端的 get_face_attributes 方法
async def async_detect_faces( async def async_detect_faces(
self, self,
image_url: str = None, image_url: str = None,
image_path: str = None, image_path: str = None,
image_file: str = None, image_file: str = None,
camera_entity_id: str = None,
max_face_num: int = 1, max_face_num: int = 1,
min_face_size: int = 34, min_face_size: int = 34,
need_rotate_check: int = 1 need_rotate_check: int = 1
) -> Dict[str, Any]: ) -> Dict[str, Any]:
"""检测人脸""" """检测人脸"""
try: try:
# 验证参数 self._validate_image_params(image_url, image_path, image_file, camera_entity_id)
self._validate_image_params(image_url, image_path, image_file)
image_base64 = await self._get_image_base64_from_source(
image_url, image_path, image_file, camera_entity_id
)
# 调用腾讯云API检测人脸
result = await self.hass.async_add_executor_job( result = await self.hass.async_add_executor_job(
self.client.detect_faces, self.client.detect_faces,
image_url, None, None, None, image_base64,
image_path,
image_file,
max_face_num, max_face_num,
min_face_size, min_face_size,
need_rotate_check need_rotate_check
) )
# 处理结果
if result.get("success", False): if result.get("success", False):
return { return {
"success": True, "success": True,
@@ -263,7 +258,6 @@ class FaceRecognition:
"error_message": None "error_message": None
} }
else: else:
# 返回错误信息
return { return {
"success": False, "success": False,
"faces": [], "faces": [],
@@ -272,6 +266,8 @@ class FaceRecognition:
"error_message": result.get("error_message") "error_message": result.get("error_message")
} }
except HomeAssistantError:
raise
except Exception as ex: except Exception as ex:
_LOGGER.error("人脸检测失败: %s", ex) _LOGGER.error("人脸检测失败: %s", ex)
return { return {
@@ -282,4 +278,109 @@ class FaceRecognition:
"error_message": str(ex) "error_message": str(ex)
} }
# 移除 _detect_faces_sync 方法,因为现在直接使用客户端的 detect_faces 方法 async def async_create_person(
self,
person_id: str,
person_name: str,
group_id: str,
image_url: str = None,
image_path: str = None,
image_file: str = None,
camera_entity_id: str = None,
gender: int = None,
person_tag: str = None,
quality_control: int = 1,
need_rotate_check: int = 1,
) -> Dict[str, Any]:
"""创建人员"""
try:
image_base64 = None
if any([image_url, image_path, image_file, camera_entity_id]):
image_base64 = await self._get_image_base64_from_source(
image_url, image_path, image_file, camera_entity_id
)
result = await self.hass.async_add_executor_job(
self.client.create_person,
person_id, person_name, group_id,
None, None, None, image_base64,
gender, person_tag,
quality_control, need_rotate_check,
)
return result
except HomeAssistantError:
raise
except Exception as ex:
_LOGGER.error("创建人员失败: %s", ex)
return {
"success": False,
"error": str(ex),
"error_code": "unknown_error",
"error_message": str(ex)
}
async def async_delete_person(self, person_id: str) -> Dict[str, Any]:
"""删除人员"""
try:
result = await self.hass.async_add_executor_job(
self.client.delete_person, person_id
)
return result
except Exception as ex:
_LOGGER.error("删除人员失败: %s", ex)
return {
"success": False,
"error": str(ex),
"error_code": "unknown_error",
"error_message": str(ex)
}
async def async_create_face(
self,
person_id: str,
image_url: str = None,
image_path: str = None,
image_file: str = None,
camera_entity_id: str = None,
quality_control: int = 1,
need_rotate_check: int = 1,
) -> Dict[str, Any]:
"""为人员注册人脸"""
try:
image_base64 = await self._get_image_base64_from_source(
image_url, image_path, image_file, camera_entity_id
)
result = await self.hass.async_add_executor_job(
self.client.create_face,
person_id,
None, None, None, image_base64,
quality_control, need_rotate_check,
)
return result
except HomeAssistantError:
raise
except Exception as ex:
_LOGGER.error("注册人脸失败: %s", ex)
return {
"success": False,
"error": str(ex),
"error_code": "unknown_error",
"error_message": str(ex)
}
async def async_delete_face(self, person_id: str, face_id: str) -> Dict[str, Any]:
"""删除人脸"""
try:
result = await self.hass.async_add_executor_job(
self.client.delete_face, person_id, face_id
)
return result
except Exception as ex:
_LOGGER.error("删除人脸失败: %s", ex)
return {
"success": False,
"error": str(ex),
"error_code": "unknown_error",
"error_message": str(ex)
}
+4 -4
View File
@@ -5,12 +5,12 @@
"documentation": "https://code.nextrt.com/Hass/tencent_face_recognition", "documentation": "https://code.nextrt.com/Hass/tencent_face_recognition",
"issue_tracker": "https://code.nextrt.com/Hass/tencent_face_recognition/issues", "issue_tracker": "https://code.nextrt.com/Hass/tencent_face_recognition/issues",
"requirements": [ "requirements": [
"tencentcloud-sdk-python-common", "tencentcloud-sdk-python-common>=3.0.0,<4.0.0",
"tencentcloud-sdk-python-iai" "tencentcloud-sdk-python-iai>=3.0.0,<4.0.0"
], ],
"dependencies": [], "dependencies": [],
"codeowners": ["@xyzmos"], "codeowners": ["@xyzmos"],
"version": "1.0.0", "version": "2.0.0",
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"resources": [] "resources": []
} }
+2 -2
View File
@@ -1,2 +1,2 @@
tencentcloud-sdk-python-common>=3.0.0 tencentcloud-sdk-python-common>=3.0.0,<4.0.0
tencentcloud-sdk-python-iai>=3.0.0 tencentcloud-sdk-python-iai>=3.0.0,<4.0.0
+139 -143
View File
@@ -1,8 +1,7 @@
"""传感器实体""" """传感器实体"""
import logging import logging
import base64 from typing import Dict, Any, Optional, List
from typing import Dict, Any, Optional
from datetime import datetime, timedelta from datetime import datetime, timedelta
from homeassistant.components.sensor import SensorEntity from homeassistant.components.sensor import SensorEntity
@@ -11,11 +10,59 @@ from homeassistant.const import CONF_NAME
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity import EntityCategory
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.update_coordinator import (
CoordinatorEntity,
DataUpdateCoordinator,
UpdateFailed,
)
from .const import DOMAIN from .const import (
DOMAIN,
CONF_PERSON_GROUP_ID,
DEFAULT_PERSON_GROUP_ID,
get_entry_value,
)
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
_LOGGER_POLLING = _LOGGER.getChild("polling")
class TencentFaceCoordinator(DataUpdateCoordinator):
"""腾讯云人脸识别数据更新协调器"""
def __init__(self, hass: HomeAssistant, entry: ConfigEntry, client):
super().__init__(
hass,
_LOGGER_POLLING,
name="Tencent Face Recognition",
update_interval=timedelta(minutes=5),
)
self.client = client
self.entry = entry
async def _async_update_data(self):
"""获取最新数据"""
try:
group_id = get_entry_value(
self.entry, CONF_PERSON_GROUP_ID, DEFAULT_PERSON_GROUP_ID
)
group_info = await self.hass.async_add_executor_job(
self.client.get_group_info, group_id
)
persons = await self.hass.async_add_executor_job(
self.client.get_person_list_all, group_id
)
return {
"group_info": group_info,
"persons": persons,
"person_count": len(persons),
}
except Exception as ex:
_LOGGER_POLLING.debug("数据更新失败: %s", ex)
raise UpdateFailed(f"数据更新失败: {ex}") from ex
async def async_setup_entry( async def async_setup_entry(
@@ -23,184 +70,133 @@ async def async_setup_entry(
) -> None: ) -> None:
"""设置传感器实体""" """设置传感器实体"""
_LOGGER.debug("设置腾讯云人脸识别传感器实体") _LOGGER.debug("设置腾讯云人脸识别传感器实体")
# 获取配置项
name = entry.data.get(CONF_NAME, "腾讯云人脸识别")
# 创建传感器实体
async_add_entities(
[
TencentFaceRecognitionStatusSensor(hass, entry, name),
]
)
# 获取人员列表并创建实体
client = hass.data[DOMAIN][entry.entry_id]["client"] client = hass.data[DOMAIN][entry.entry_id]["client"]
person_group_id = entry.data.get("person_group_id", "Hass") name = entry.data.get(CONF_NAME, "腾讯云人脸识别")
try: coordinator = TencentFaceCoordinator(hass, entry, client)
persons = await hass.async_add_executor_job(
client.get_person_list, person_group_id await coordinator.async_config_entry_first_refresh()
hass.data[DOMAIN][entry.entry_id]["coordinator"] = coordinator
entities = [
TencentFaceRecognitionStatusSensor(coordinator, entry, name),
]
for person in coordinator.data.get("persons", []):
entities.append(
TencentFaceRecognitionPersonSensor(coordinator, entry, person)
) )
for person in persons: async_add_entities(entities)
async_add_entities(
[
TencentFaceRecognitionPersonSensor(hass, entry, person),
]
)
except Exception as ex:
_LOGGER.error("获取人员列表失败: %s", ex)
class TencentFaceRecognitionPersonSensor(SensorEntity): class TencentFaceRecognitionPersonSensor(CoordinatorEntity, SensorEntity):
"""腾讯云人脸识别人员传感器""" """腾讯云人脸识别人员传感器"""
_attr_icon = "mdi:account" _attr_icon = "mdi:account"
_attr_has_entity_name = True
def __init__(self, hass: HomeAssistant, entry: ConfigEntry, person: Dict[str, Any]): def __init__(self, coordinator, entry: ConfigEntry, person: Dict[str, Any]):
"""初始化传感器""" """初始化传感器"""
self.hass = hass super().__init__(coordinator)
self._entry = entry self._entry = entry
self._person = person self._person_id = person.get("person_id")
self._attr_name = person.get("person_name", "未知人员") person_name = person.get("person_name", "未知人员")
self._attr_unique_id = f"{entry.entry_id}_{person.get('person_id')}" self._attr_name = person_name
self._state = "未知" self._attr_unique_id = f"{entry.entry_id}_{self._person_id}"
self._extra_state_attributes = { self._attr_translation_key = "person_sensor"
"person_id": person.get("person_id"),
"gender": "" if person.get("gender") == 1 else "",
"face_ids": person.get("face_ids"),
}
@property @property
def state(self) -> str: def state(self) -> str:
"""返回传感器状态""" """返回传感器状态"""
return self._state if self.coordinator.data is None:
return "未知"
for person in self.coordinator.data.get("persons", []):
if person.get("person_id") == self._person_id:
return person.get("person_name", "未知")
return "已删除"
@property @property
def extra_state_attributes(self) -> Dict[str, Any]: def extra_state_attributes(self) -> Dict[str, Any]:
"""返回传感器属性""" """返回传感器属性"""
return self._extra_state_attributes if self.coordinator.data is None:
return {"person_id": self._person_id}
for person in self.coordinator.data.get("persons", []):
if person.get("person_id") == self._person_id:
return {
"person_id": person.get("person_id"),
"gender": "" if person.get("gender") == 1 else "",
"face_ids": person.get("face_ids"),
"face_count": len(person.get("face_ids", [])),
}
return {"person_id": self._person_id, "status": "已删除"}
@property
def available(self) -> bool:
"""传感器是否可用"""
if self.coordinator.data is None:
return False
for person in self.coordinator.data.get("persons", []):
if person.get("person_id") == self._person_id:
return True
return False
class TencentFaceRecognitionStatusSensor(SensorEntity): class TencentFaceRecognitionStatusSensor(CoordinatorEntity, SensorEntity):
"""腾讯云人脸识别状态传感器""" """腾讯云人脸识别状态传感器"""
_attr_icon = "mdi:face-recognition" _attr_icon = "mdi:face-recognition"
_attr_entity_category = EntityCategory.DIAGNOSTIC _attr_entity_category = EntityCategory.DIAGNOSTIC
_attr_has_entity_name = True
_attr_translation_key = "status_sensor"
def __init__(self, hass: HomeAssistant, entry: ConfigEntry, name: str): def __init__(self, coordinator, entry: ConfigEntry, name: str):
"""初始化传感器""" """初始化传感器"""
self.hass = hass super().__init__(coordinator)
self._entry = entry self._entry = entry
self._name = name self._name = name
self._attr_name = f"{name} 状态" self._attr_name = f"{name} 状态"
self._attr_unique_id = f"{entry.entry_id}_status" self._attr_unique_id = f"{entry.entry_id}_status"
self._state = "未连接"
self._extra_state_attributes = {
"last_updated": None,
"api_status": "未知",
"client_status": "未知",
"last_test_time": None,
"error_count": 0,
"success_count": 0,
"last_error": None
}
self._client = None
self._last_successful_test = None
@property @property
def state(self) -> str: def state(self) -> str:
"""返回传感器状态""" """返回传感器状态"""
return self._state if self.coordinator.data is None:
return "未连接"
if self.coordinator.last_update_success:
return "已连接"
return "连接错误"
@property @property
def extra_state_attributes(self) -> Dict[str, Any]: def extra_state_attributes(self) -> Dict[str, Any]:
"""返回传感器属性""" """返回传感器属性"""
return self._extra_state_attributes attrs = {
"last_updated": (
self.coordinator.data.get("last_updated")
if self.coordinator.data
else None
),
"api_status": "正常" if self.coordinator.last_update_success else "错误",
"person_count": (
self.coordinator.data.get("person_count", 0)
if self.coordinator.data
else 0
),
"last_error": None,
}
async def async_added_to_hass(self) -> None: if self.coordinator.data and self.coordinator.data.get("group_info"):
"""当实体添加到Home Assistant时调用""" group_info = self.coordinator.data["group_info"]
# 设置定期更新 attrs["group_name"] = group_info.get("group_name", "")
async_track_time_interval( attrs["face_model_version"] = group_info.get("face_model_version", "")
self.hass,
self._async_update,
timedelta(minutes=5) # 每5分钟更新一次
)
# 立即执行一次更新
await self._async_update(None)
async def _async_update(self, _) -> None: return attrs
"""更新传感器状态"""
await self.async_update()
async def async_update(self) -> None: @property
"""更新传感器状态""" def available(self) -> bool:
try: """状态传感器始终可用"""
# 获取插件实例 return True
entry_data = self.hass.data[DOMAIN].get(self._entry.entry_id)
if not entry_data:
self._state = "未初始化"
self._extra_state_attributes["api_status"] = "插件未正确初始化"
self._extra_state_attributes["client_status"] = "未初始化"
return
# 获取客户端实例
client = entry_data.get("client")
face_recognition = entry_data.get("face_recognition")
if not client or not face_recognition:
self._state = "未初始化"
self._extra_state_attributes["api_status"] = "客户端未正确初始化"
self._extra_state_attributes["client_status"] = "未初始化"
return
# 更新客户端状态
self._client = client
self._extra_state_attributes["client_status"] = "已初始化"
# 使用内置的测试图片进行连接测试
await self._test_api_connection(face_recognition)
except Exception as ex:
_LOGGER.error("更新腾讯云人脸识别状态传感器失败: %s", ex, exc_info=True)
self._state = "连接错误"
self._extra_state_attributes["api_status"] = f"错误: {str(ex)}"
self._extra_state_attributes["last_error"] = str(ex)
self._extra_state_attributes["error_count"] = self._extra_state_attributes.get("error_count", 0) + 1
# 更新最后更新时间
self._extra_state_attributes["last_updated"] = datetime.now().isoformat()
async def _test_api_connection(self, face_recognition) -> None:
"""测试API连接"""
try:
group_id = self._entry.data.get("person_group_id", "Hass")
# 使用 GetGroupInfo 测试连接
group_info = await self.hass.async_add_executor_job(
self._client.get_group_info,
group_id
)
if group_info:
self._state = "已连接"
self._extra_state_attributes["api_status"] = "正常"
self._extra_state_attributes["success_count"] = self._extra_state_attributes.get("success_count", 0) + 1
self._last_successful_test = datetime.now()
self._extra_state_attributes["last_test_time"] = self._last_successful_test.isoformat()
if "last_error" in self._extra_state_attributes:
self._extra_state_attributes["last_error"] = None
else:
self._state = "连接错误"
self._extra_state_attributes["api_status"] = "获取人员库信息失败"
except Exception as ex:
error_msg = str(ex)
self._state = "连接错误"
self._extra_state_attributes["api_status"] = f"连接失败: {error_msg}"
self._extra_state_attributes["last_error"] = error_msg
self._extra_state_attributes["error_count"] = self._extra_state_attributes.get("error_count", 0) + 1
_LOGGER.debug("API连接测试失败: %s", error_msg)
+305 -85
View File
@@ -6,6 +6,7 @@ from typing import Dict, Any, List, Optional
import voluptuous as vol import voluptuous as vol
from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.core import HomeAssistant, ServiceCall
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import config_validation as cv from homeassistant.helpers import config_validation as cv
from .const import ( from .const import (
@@ -13,25 +14,39 @@ from .const import (
SERVICE_FACE_SEARCH, SERVICE_FACE_SEARCH,
SERVICE_DETECT_FACE, SERVICE_DETECT_FACE,
SERVICE_GET_FACE_ATTRIBUTES, SERVICE_GET_FACE_ATTRIBUTES,
SERVICE_CREATE_PERSON,
SERVICE_DELETE_PERSON,
SERVICE_CREATE_FACE,
SERVICE_DELETE_FACE,
ATTR_IMAGE_URL, ATTR_IMAGE_URL,
ATTR_IMAGE_FILE, ATTR_IMAGE_FILE,
ATTR_IMAGE_PATH, ATTR_IMAGE_PATH,
ATTR_CAMERA_ENTITY_ID,
ATTR_CONFIG_ENTRY_ID,
ATTR_GROUP_ID,
ATTR_MAX_FACE_NUM, ATTR_MAX_FACE_NUM,
ATTR_MIN_FACE_SIZE, ATTR_MIN_FACE_SIZE,
ATTR_MAX_USER_NUM, ATTR_MAX_USER_NUM,
ATTR_QUALITY_CONTROL, ATTR_QUALITY_CONTROL,
ATTR_NEED_ROTATE_CHECK, ATTR_NEED_ROTATE_CHECK,
ATTR_FACE_MATCH_THRESHOLD, ATTR_FACE_MATCH_THRESHOLD,
ATTR_PERSON_ID,
ATTR_PERSON_NAME,
ATTR_GENDER,
ATTR_FACE_ID,
ATTR_PERSON_TAG,
EVENT_FACE_DETECTED,
) )
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
# 人脸搜索服务参数
FACE_SEARCH_SCHEMA = vol.Schema({ FACE_SEARCH_SCHEMA = vol.Schema({
vol.Required('group_id'): cv.string, vol.Required(ATTR_GROUP_ID): cv.string,
vol.Optional(ATTR_IMAGE_URL): cv.string, vol.Optional(ATTR_IMAGE_URL): cv.string,
vol.Optional(ATTR_IMAGE_PATH): cv.string, vol.Optional(ATTR_IMAGE_PATH): cv.string,
vol.Optional(ATTR_IMAGE_FILE): cv.string, vol.Optional(ATTR_IMAGE_FILE): cv.string,
vol.Optional(ATTR_CAMERA_ENTITY_ID): cv.string,
vol.Optional(ATTR_CONFIG_ENTRY_ID): cv.string,
vol.Optional(ATTR_MAX_FACE_NUM, default=1): cv.positive_int, vol.Optional(ATTR_MAX_FACE_NUM, default=1): cv.positive_int,
vol.Optional(ATTR_MIN_FACE_SIZE, default=34): cv.positive_int, vol.Optional(ATTR_MIN_FACE_SIZE, default=34): cv.positive_int,
vol.Optional(ATTR_MAX_USER_NUM, default=5): cv.positive_int, vol.Optional(ATTR_MAX_USER_NUM, default=5): cv.positive_int,
@@ -40,31 +55,72 @@ FACE_SEARCH_SCHEMA = vol.Schema({
vol.Optional(ATTR_FACE_MATCH_THRESHOLD, default=60.0): vol.Range(min=0, max=100), vol.Optional(ATTR_FACE_MATCH_THRESHOLD, default=60.0): vol.Range(min=0, max=100),
}) })
# 人脸检测服务参数
DETECT_FACE_SCHEMA = vol.Schema({ DETECT_FACE_SCHEMA = vol.Schema({
vol.Optional(ATTR_IMAGE_URL): cv.string, vol.Optional(ATTR_IMAGE_URL): cv.string,
vol.Optional(ATTR_IMAGE_PATH): cv.string, vol.Optional(ATTR_IMAGE_PATH): cv.string,
vol.Optional(ATTR_IMAGE_FILE): cv.string, vol.Optional(ATTR_IMAGE_FILE): cv.string,
vol.Optional(ATTR_CAMERA_ENTITY_ID): cv.string,
vol.Optional(ATTR_CONFIG_ENTRY_ID): cv.string,
vol.Optional(ATTR_MAX_FACE_NUM, default=1): cv.positive_int, vol.Optional(ATTR_MAX_FACE_NUM, default=1): cv.positive_int,
vol.Optional(ATTR_MIN_FACE_SIZE, default=34): cv.positive_int, vol.Optional(ATTR_MIN_FACE_SIZE, default=34): cv.positive_int,
vol.Optional(ATTR_NEED_ROTATE_CHECK, default=1): vol.In([0, 1]), vol.Optional(ATTR_NEED_ROTATE_CHECK, default=1): vol.In([0, 1]),
}) })
# 获取人脸属性服务参数
GET_FACE_ATTRIBUTES_SCHEMA = vol.Schema({ GET_FACE_ATTRIBUTES_SCHEMA = vol.Schema({
vol.Optional(ATTR_IMAGE_URL): cv.string, vol.Optional(ATTR_IMAGE_URL): cv.string,
vol.Optional(ATTR_IMAGE_PATH): cv.string, vol.Optional(ATTR_IMAGE_PATH): cv.string,
vol.Optional(ATTR_IMAGE_FILE): cv.string, vol.Optional(ATTR_IMAGE_FILE): cv.string,
vol.Optional(ATTR_CAMERA_ENTITY_ID): cv.string,
vol.Optional(ATTR_CONFIG_ENTRY_ID): cv.string,
vol.Optional(ATTR_MAX_FACE_NUM, default=1): cv.positive_int, vol.Optional(ATTR_MAX_FACE_NUM, default=1): cv.positive_int,
vol.Optional(ATTR_NEED_ROTATE_CHECK, default=1): vol.In([0, 1]), vol.Optional(ATTR_NEED_ROTATE_CHECK, default=1): vol.In([0, 1]),
}) })
CREATE_PERSON_SCHEMA = vol.Schema({
vol.Required(ATTR_PERSON_ID): cv.string,
vol.Required(ATTR_PERSON_NAME): cv.string,
vol.Required(ATTR_GROUP_ID): cv.string,
vol.Optional(ATTR_IMAGE_URL): cv.string,
vol.Optional(ATTR_IMAGE_PATH): cv.string,
vol.Optional(ATTR_IMAGE_FILE): cv.string,
vol.Optional(ATTR_CAMERA_ENTITY_ID): cv.string,
vol.Optional(ATTR_CONFIG_ENTRY_ID): cv.string,
vol.Optional(ATTR_GENDER): vol.In([0, 1]),
vol.Optional(ATTR_PERSON_TAG): cv.string,
vol.Optional(ATTR_QUALITY_CONTROL, default=1): vol.In([0, 1]),
vol.Optional(ATTR_NEED_ROTATE_CHECK, default=1): vol.In([0, 1]),
})
DELETE_PERSON_SCHEMA = vol.Schema({
vol.Required(ATTR_PERSON_ID): cv.string,
vol.Optional(ATTR_CONFIG_ENTRY_ID): cv.string,
})
CREATE_FACE_SCHEMA = vol.Schema({
vol.Required(ATTR_PERSON_ID): cv.string,
vol.Optional(ATTR_IMAGE_URL): cv.string,
vol.Optional(ATTR_IMAGE_PATH): cv.string,
vol.Optional(ATTR_IMAGE_FILE): cv.string,
vol.Optional(ATTR_CAMERA_ENTITY_ID): cv.string,
vol.Optional(ATTR_CONFIG_ENTRY_ID): cv.string,
vol.Optional(ATTR_QUALITY_CONTROL, default=1): vol.In([0, 1]),
vol.Optional(ATTR_NEED_ROTATE_CHECK, default=1): vol.In([0, 1]),
})
DELETE_FACE_SCHEMA = vol.Schema({
vol.Required(ATTR_PERSON_ID): cv.string,
vol.Required(ATTR_FACE_ID): cv.string,
vol.Optional(ATTR_CONFIG_ENTRY_ID): cv.string,
})
async def async_setup_services(hass: HomeAssistant) -> None: async def async_setup_services(hass: HomeAssistant) -> None:
"""设置服务""" """设置服务"""
if hass.services.has_service(DOMAIN, SERVICE_FACE_SEARCH):
return
_LOGGER.info("设置腾讯云人脸识别服务") _LOGGER.info("设置腾讯云人脸识别服务")
# 注册人脸搜索服务
hass.services.async_register( hass.services.async_register(
DOMAIN, DOMAIN,
SERVICE_FACE_SEARCH, SERVICE_FACE_SEARCH,
@@ -73,60 +129,108 @@ async def async_setup_services(hass: HomeAssistant) -> None:
supports_response=True supports_response=True
) )
# 注册人脸检测服务
hass.services.async_register( hass.services.async_register(
DOMAIN, DOMAIN,
SERVICE_DETECT_FACE, SERVICE_DETECT_FACE,
async_detect_face_service, async_detect_face_service,
schema=DETECT_FACE_SCHEMA schema=DETECT_FACE_SCHEMA,
supports_response=True
) )
# 注册获取人脸属性服务
hass.services.async_register( hass.services.async_register(
DOMAIN, DOMAIN,
SERVICE_GET_FACE_ATTRIBUTES, SERVICE_GET_FACE_ATTRIBUTES,
async_get_face_attributes_service, async_get_face_attributes_service,
schema=GET_FACE_ATTRIBUTES_SCHEMA schema=GET_FACE_ATTRIBUTES_SCHEMA,
supports_response=True
)
hass.services.async_register(
DOMAIN,
SERVICE_CREATE_PERSON,
async_create_person_service,
schema=CREATE_PERSON_SCHEMA,
supports_response=True
)
hass.services.async_register(
DOMAIN,
SERVICE_DELETE_PERSON,
async_delete_person_service,
schema=DELETE_PERSON_SCHEMA,
supports_response=True
)
hass.services.async_register(
DOMAIN,
SERVICE_CREATE_FACE,
async_create_face_service,
schema=CREATE_FACE_SCHEMA,
supports_response=True
)
hass.services.async_register(
DOMAIN,
SERVICE_DELETE_FACE,
async_delete_face_service,
schema=DELETE_FACE_SCHEMA,
supports_response=True
) )
async def async_unload_services(hass: HomeAssistant) -> None: async def async_unload_services(hass: HomeAssistant) -> None:
"""卸载服务""" """卸载服务"""
if any(hass.config_entries.async_entries(DOMAIN)):
return
_LOGGER.info("卸载腾讯云人脸识别服务") _LOGGER.info("卸载腾讯云人脸识别服务")
# 注销所有服务 for service_name in [
hass.services.async_remove(DOMAIN, SERVICE_FACE_SEARCH) SERVICE_FACE_SEARCH,
hass.services.async_remove(DOMAIN, SERVICE_DETECT_FACE) SERVICE_DETECT_FACE,
hass.services.async_remove(DOMAIN, SERVICE_GET_FACE_ATTRIBUTES) SERVICE_GET_FACE_ATTRIBUTES,
SERVICE_CREATE_PERSON,
SERVICE_DELETE_PERSON,
SERVICE_CREATE_FACE,
SERVICE_DELETE_FACE,
]:
hass.services.async_remove(DOMAIN, service_name)
def _get_entry_data(hass: HomeAssistant, config_entry_id: str = None):
"""获取配置项数据,支持多配置条目"""
domain_data = hass.data.get(DOMAIN, {})
if config_entry_id:
data = domain_data.get(config_entry_id)
if data and isinstance(data, dict) and "face_recognition" in data:
return data
return None
for entry_id, data in domain_data.items():
if isinstance(data, dict) and "face_recognition" in data:
return data
return None
async def async_face_search_service(call: ServiceCall) -> Dict[str, Any]: async def async_face_search_service(call: ServiceCall) -> Dict[str, Any]:
"""人脸搜索服务""" """人脸搜索服务"""
from homeassistant.exceptions import HomeAssistantError
hass = call.hass hass = call.hass
data = call.data data = call.data
# 获取配置项 entry_data = _get_entry_data(hass, data.get(ATTR_CONFIG_ENTRY_ID))
config_entry = _get_config_entry(hass) if not entry_data:
if not config_entry:
raise HomeAssistantError("未找到腾讯云人脸识别配置") raise HomeAssistantError("未找到腾讯云人脸识别配置")
# 获取插件实例
entry_data = _get_entry_data(hass)
if not entry_data:
raise HomeAssistantError("腾讯云人脸识别插件未正确初始化")
face_recognition = entry_data.get("face_recognition") face_recognition = entry_data.get("face_recognition")
if not face_recognition: if not face_recognition:
raise HomeAssistantError("腾讯云人脸识别插件未正确初始化") raise HomeAssistantError("腾讯云人脸识别插件未正确初始化")
# 调用人脸搜索功能
try: try:
group_id = data.get('group_id') group_id = data.get(ATTR_GROUP_ID)
image_url = data.get(ATTR_IMAGE_URL) image_url = data.get(ATTR_IMAGE_URL)
image_path = data.get(ATTR_IMAGE_PATH) image_path = data.get(ATTR_IMAGE_PATH)
image_file = data.get(ATTR_IMAGE_FILE) image_file = data.get(ATTR_IMAGE_FILE)
camera_entity_id = data.get(ATTR_CAMERA_ENTITY_ID)
max_face_num = data.get(ATTR_MAX_FACE_NUM, 1) max_face_num = data.get(ATTR_MAX_FACE_NUM, 1)
min_face_size = data.get(ATTR_MIN_FACE_SIZE, 34) min_face_size = data.get(ATTR_MIN_FACE_SIZE, 34)
max_user_num = data.get(ATTR_MAX_USER_NUM, 5) max_user_num = data.get(ATTR_MAX_USER_NUM, 5)
@@ -138,6 +242,7 @@ async def async_face_search_service(call: ServiceCall) -> Dict[str, Any]:
image_url=image_url, image_url=image_url,
image_path=image_path, image_path=image_path,
image_file=image_file, image_file=image_file,
camera_entity_id=camera_entity_id,
group_ids=[group_id], group_ids=[group_id],
max_face_num=max_face_num, max_face_num=max_face_num,
min_face_size=min_face_size, min_face_size=min_face_size,
@@ -147,47 +252,45 @@ async def async_face_search_service(call: ServiceCall) -> Dict[str, Any]:
face_match_threshold=face_match_threshold face_match_threshold=face_match_threshold
) )
# 直接返回结果,包含状态和错误信息 if result.get("success", False):
for face in result.get("faces", []):
for candidate in face.get("candidates", []):
hass.bus.async_fire(EVENT_FACE_DETECTED, {
ATTR_PERSON_ID: candidate.get("person_id"),
ATTR_PERSON_NAME: candidate.get("person_name"),
"score": candidate.get("score"),
ATTR_PERSON_TAG: candidate.get("person_tag"),
ATTR_GROUP_ID: group_id,
ATTR_CAMERA_ENTITY_ID: camera_entity_id,
})
return result return result
except HomeAssistantError:
raise
except Exception as ex: except Exception as ex:
_LOGGER.error("人脸搜索服务调用失败: %s", ex) _LOGGER.error("人脸搜索服务调用失败: %s", ex)
# 返回错误信息,而不是抛出异常 raise HomeAssistantError(f"人脸搜索服务调用失败: {ex}")
return {
"success": False,
"faces": [],
"error": str(ex),
"error_code": "service_error",
"error_message": str(ex)
}
async def async_detect_face_service(call: ServiceCall) -> Dict[str, Any]: async def async_detect_face_service(call: ServiceCall) -> Dict[str, Any]:
"""人脸检测服务""" """人脸检测服务"""
from homeassistant.exceptions import HomeAssistantError
hass = call.hass hass = call.hass
data = call.data data = call.data
# 获取配置项 entry_data = _get_entry_data(hass, data.get(ATTR_CONFIG_ENTRY_ID))
config_entry = _get_config_entry(hass) if not entry_data:
if not config_entry:
raise HomeAssistantError("未找到腾讯云人脸识别配置") raise HomeAssistantError("未找到腾讯云人脸识别配置")
# 获取插件实例
entry_data = _get_entry_data(hass)
if not entry_data:
raise HomeAssistantError("腾讯云人脸识别插件未正确初始化")
face_recognition = entry_data.get("face_recognition") face_recognition = entry_data.get("face_recognition")
if not face_recognition: if not face_recognition:
raise HomeAssistantError("腾讯云人脸识别插件未正确初始化") raise HomeAssistantError("腾讯云人脸识别插件未正确初始化")
# 调用人脸检测功能
try: try:
image_url = data.get(ATTR_IMAGE_URL) image_url = data.get(ATTR_IMAGE_URL)
image_path = data.get(ATTR_IMAGE_PATH) image_path = data.get(ATTR_IMAGE_PATH)
image_file = data.get(ATTR_IMAGE_FILE) image_file = data.get(ATTR_IMAGE_FILE)
camera_entity_id = data.get(ATTR_CAMERA_ENTITY_ID)
max_face_num = data.get(ATTR_MAX_FACE_NUM, 1) max_face_num = data.get(ATTR_MAX_FACE_NUM, 1)
min_face_size = data.get(ATTR_MIN_FACE_SIZE, 34) min_face_size = data.get(ATTR_MIN_FACE_SIZE, 34)
need_rotate_check = data.get(ATTR_NEED_ROTATE_CHECK, 1) need_rotate_check = data.get(ATTR_NEED_ROTATE_CHECK, 1)
@@ -196,52 +299,43 @@ async def async_detect_face_service(call: ServiceCall) -> Dict[str, Any]:
image_url=image_url, image_url=image_url,
image_path=image_path, image_path=image_path,
image_file=image_file, image_file=image_file,
camera_entity_id=camera_entity_id,
max_face_num=max_face_num, max_face_num=max_face_num,
min_face_size=min_face_size, min_face_size=min_face_size,
need_rotate_check=need_rotate_check need_rotate_check=need_rotate_check
) )
# 直接返回结果,包含状态和错误信息 if not result.get("success", False):
error_msg = result.get("error_message", result.get("error", "未知错误"))
raise HomeAssistantError(f"人脸检测失败: {error_msg}")
return result return result
except HomeAssistantError:
raise
except Exception as ex: except Exception as ex:
_LOGGER.error("人脸检测服务调用失败: %s", ex) _LOGGER.error("人脸检测服务调用失败: %s", ex)
# 返回错误信息,而不是抛出异常 raise HomeAssistantError(f"人脸检测服务调用失败: {ex}")
return {
"success": False,
"faces": [],
"error": str(ex),
"error_code": "service_error",
"error_message": str(ex)
}
async def async_get_face_attributes_service(call: ServiceCall) -> Dict[str, Any]: async def async_get_face_attributes_service(call: ServiceCall) -> Dict[str, Any]:
"""获取人脸属性服务""" """获取人脸属性服务"""
from homeassistant.exceptions import HomeAssistantError
hass = call.hass hass = call.hass
data = call.data data = call.data
# 获取配置项 entry_data = _get_entry_data(hass, data.get(ATTR_CONFIG_ENTRY_ID))
config_entry = _get_config_entry(hass) if not entry_data:
if not config_entry:
raise HomeAssistantError("未找到腾讯云人脸识别配置") raise HomeAssistantError("未找到腾讯云人脸识别配置")
# 获取插件实例
entry_data = _get_entry_data(hass)
if not entry_data:
raise HomeAssistantError("腾讯云人脸识别插件未正确初始化")
face_recognition = entry_data.get("face_recognition") face_recognition = entry_data.get("face_recognition")
if not face_recognition: if not face_recognition:
raise HomeAssistantError("腾讯云人脸识别插件未正确初始化") raise HomeAssistantError("腾讯云人脸识别插件未正确初始化")
# 调用获取人脸属性功能
try: try:
image_url = data.get(ATTR_IMAGE_URL) image_url = data.get(ATTR_IMAGE_URL)
image_path = data.get(ATTR_IMAGE_PATH) image_path = data.get(ATTR_IMAGE_PATH)
image_file = data.get(ATTR_IMAGE_FILE) image_file = data.get(ATTR_IMAGE_FILE)
camera_entity_id = data.get(ATTR_CAMERA_ENTITY_ID)
max_face_num = data.get(ATTR_MAX_FACE_NUM, 1) max_face_num = data.get(ATTR_MAX_FACE_NUM, 1)
need_rotate_check = data.get(ATTR_NEED_ROTATE_CHECK, 1) need_rotate_check = data.get(ATTR_NEED_ROTATE_CHECK, 1)
@@ -249,34 +343,160 @@ async def async_get_face_attributes_service(call: ServiceCall) -> Dict[str, Any]
image_url=image_url, image_url=image_url,
image_path=image_path, image_path=image_path,
image_file=image_file, image_file=image_file,
camera_entity_id=camera_entity_id,
max_face_num=max_face_num, max_face_num=max_face_num,
need_rotate_check=need_rotate_check need_rotate_check=need_rotate_check
) )
# 直接返回结果,包含状态和错误信息 if not result.get("success", False):
error_msg = result.get("error_message", result.get("error", "未知错误"))
raise HomeAssistantError(f"获取人脸属性失败: {error_msg}")
return result return result
except HomeAssistantError:
raise
except Exception as ex: except Exception as ex:
_LOGGER.error("获取人脸属性服务调用失败: %s", ex) _LOGGER.error("获取人脸属性服务调用失败: %s", ex)
# 返回错误信息,而不是抛出异常 raise HomeAssistantError(f"获取人脸属性服务调用失败: {ex}")
return {
"success": False,
"faces": [],
"error": str(ex),
"error_code": "service_error",
"error_message": str(ex)
}
def _get_config_entry(hass: HomeAssistant): async def async_create_person_service(call: ServiceCall) -> Dict[str, Any]:
"""获取配置项""" """创建人员服务"""
for entry in hass.config_entries.async_entries(DOMAIN): hass = call.hass
return entry data = call.data
return None
def _get_entry_data(hass: HomeAssistant): entry_data = _get_entry_data(hass, data.get(ATTR_CONFIG_ENTRY_ID))
"""获取配置项数据""" if not entry_data:
for entry_id, data in hass.data.get(DOMAIN, {}).items(): raise HomeAssistantError("未找到腾讯云人脸识别配置")
if isinstance(data, dict) and "face_recognition" in data:
return data face_recognition = entry_data.get("face_recognition")
return None if not face_recognition:
raise HomeAssistantError("腾讯云人脸识别插件未正确初始化")
try:
result = await face_recognition.async_create_person(
person_id=data.get(ATTR_PERSON_ID),
person_name=data.get(ATTR_PERSON_NAME),
group_id=data.get(ATTR_GROUP_ID),
image_url=data.get(ATTR_IMAGE_URL),
image_path=data.get(ATTR_IMAGE_PATH),
image_file=data.get(ATTR_IMAGE_FILE),
camera_entity_id=data.get(ATTR_CAMERA_ENTITY_ID),
gender=data.get(ATTR_GENDER),
person_tag=data.get(ATTR_PERSON_TAG),
quality_control=data.get(ATTR_QUALITY_CONTROL, 1),
need_rotate_check=data.get(ATTR_NEED_ROTATE_CHECK, 1),
)
if not result.get("success", False):
error_msg = result.get("error_message", result.get("error", "未知错误"))
raise HomeAssistantError(f"创建人员失败: {error_msg}")
return result
except HomeAssistantError:
raise
except Exception as ex:
_LOGGER.error("创建人员服务调用失败: %s", ex)
raise HomeAssistantError(f"创建人员服务调用失败: {ex}")
async def async_delete_person_service(call: ServiceCall) -> Dict[str, Any]:
"""删除人员服务"""
hass = call.hass
data = call.data
entry_data = _get_entry_data(hass, data.get(ATTR_CONFIG_ENTRY_ID))
if not entry_data:
raise HomeAssistantError("未找到腾讯云人脸识别配置")
face_recognition = entry_data.get("face_recognition")
if not face_recognition:
raise HomeAssistantError("腾讯云人脸识别插件未正确初始化")
try:
result = await face_recognition.async_delete_person(
person_id=data.get(ATTR_PERSON_ID),
)
if not result.get("success", False):
error_msg = result.get("error_message", result.get("error", "未知错误"))
raise HomeAssistantError(f"删除人员失败: {error_msg}")
return result
except HomeAssistantError:
raise
except Exception as ex:
_LOGGER.error("删除人员服务调用失败: %s", ex)
raise HomeAssistantError(f"删除人员服务调用失败: {ex}")
async def async_create_face_service(call: ServiceCall) -> Dict[str, Any]:
"""注册人脸服务"""
hass = call.hass
data = call.data
entry_data = _get_entry_data(hass, data.get(ATTR_CONFIG_ENTRY_ID))
if not entry_data:
raise HomeAssistantError("未找到腾讯云人脸识别配置")
face_recognition = entry_data.get("face_recognition")
if not face_recognition:
raise HomeAssistantError("腾讯云人脸识别插件未正确初始化")
try:
result = await face_recognition.async_create_face(
person_id=data.get(ATTR_PERSON_ID),
image_url=data.get(ATTR_IMAGE_URL),
image_path=data.get(ATTR_IMAGE_PATH),
image_file=data.get(ATTR_IMAGE_FILE),
camera_entity_id=data.get(ATTR_CAMERA_ENTITY_ID),
quality_control=data.get(ATTR_QUALITY_CONTROL, 1),
need_rotate_check=data.get(ATTR_NEED_ROTATE_CHECK, 1),
)
if not result.get("success", False):
error_msg = result.get("error_message", result.get("error", "未知错误"))
raise HomeAssistantError(f"注册人脸失败: {error_msg}")
return result
except HomeAssistantError:
raise
except Exception as ex:
_LOGGER.error("注册人脸服务调用失败: %s", ex)
raise HomeAssistantError(f"注册人脸服务调用失败: {ex}")
async def async_delete_face_service(call: ServiceCall) -> Dict[str, Any]:
"""删除人脸服务"""
hass = call.hass
data = call.data
entry_data = _get_entry_data(hass, data.get(ATTR_CONFIG_ENTRY_ID))
if not entry_data:
raise HomeAssistantError("未找到腾讯云人脸识别配置")
face_recognition = entry_data.get("face_recognition")
if not face_recognition:
raise HomeAssistantError("腾讯云人脸识别插件未正确初始化")
try:
result = await face_recognition.async_delete_face(
person_id=data.get(ATTR_PERSON_ID),
face_id=data.get(ATTR_FACE_ID),
)
if not result.get("success", False):
error_msg = result.get("error_message", result.get("error", "未知错误"))
raise HomeAssistantError(f"删除人脸失败: {error_msg}")
return result
except HomeAssistantError:
raise
except Exception as ex:
_LOGGER.error("删除人脸服务调用失败: %s", ex)
raise HomeAssistantError(f"删除人脸服务调用失败: {ex}")
+238 -88
View File
@@ -1,4 +1,3 @@
# 腾讯云人脸识别服务定义
face_search: face_search:
name: 人脸搜索 name: 人脸搜索
description: 在指定的人员库中搜索人脸。 description: 在指定的人员库中搜索人脸。
@@ -24,7 +23,19 @@ face_search:
text: text:
image_file: image_file:
name: 图片文件 name: 图片文件
description: 通过文件上传的人脸图片。 description: 通过文件上传的人脸图片Base64编码)
selector:
text:
camera_entity_id:
name: 摄像头实体
description: 用于获取图片的摄像头实体ID。
example: "camera.front_door"
selector:
entity:
domain: camera
config_entry_id:
name: 配置项ID
description: 使用的配置项ID(多配置时指定)。
selector: selector:
text: text:
max_face_num: max_face_num:
@@ -62,90 +73,16 @@ face_search:
mode: box mode: box
quality_control: quality_control:
name: 质量控制 name: 质量控制
description: 是否进行质量控制。 description: 是否进行质量控制0=否,1=是)
default: true
example: 1
selector:
number:
min: 0
max: 1
mode: box
need_rotate_check:
name: 旋转检查
description: 是否进行旋转检查。
default: true
example: true
selector:
boolean:
face_match_threshold:
name: 人脸匹配阈值
description: 人脸匹配阈值(0-100)。
default: 60
example: 60
selector:
number:
min: 0
max: 100
step: 1
mode: slider
response:
description: "人脸搜索结果"
fields:
results:
name: "结果"
description: "包含人脸ID和候选人列表的数组"
example: '[{"face_id": "3440118425101275818", "candidates": [{"person_id": "person_1", "person_name": "张三", "score": 99.9, "person_tag": "员工"}]}]'
detect_face:
name: 人脸检测
description: 检测图片中的人脸。
fields:
image_url:
name: 图片URL
description: 要检测的图片URL。
example: "https://example.com/image.jpg"
selector:
text:
image_path:
name: 图片路径
description: 要检测的本地图片路径。
example: "/config/www/image.jpg"
selector:
text:
image_file:
name: 图片文件
description: 要检测的图片文件。
selector:
text:
max_face_num:
name: 最大人脸数量
description: 最多检测的人脸数量。
default: 1 default: 1
example: 1 example: 1
selector: selector:
number: select:
min: 1 options:
max: 10 - label: "否"
step: 1 value: "0"
mode: box - label: "是"
min_face_size: value: "1"
name: 最小人脸尺寸
description: 最小人脸尺寸(像素)。
default: 34
example: 34
selector:
number:
min: 1
max: 200
step: 1
mode: box
need_rotate_check:
name: 旋转检查
description: 是否进行旋转检查。
default: true
example: true
selector:
boolean:
get_face_attributes: get_face_attributes:
name: 获取人脸属性 name: 获取人脸属性
@@ -165,7 +102,19 @@ get_face_attributes:
text: text:
image_file: image_file:
name: 图片文件 name: 图片文件
description: 要分析的图片文件。 description: 要分析的图片文件Base64编码)
selector:
text:
camera_entity_id:
name: 摄像头实体
description: 用于获取图片的摄像头实体ID。
example: "camera.front_door"
selector:
entity:
domain: camera
config_entry_id:
name: 配置项ID
description: 使用的配置项ID(多配置时指定)。
selector: selector:
text: text:
max_face_num: max_face_num:
@@ -181,8 +130,209 @@ get_face_attributes:
mode: box mode: box
need_rotate_check: need_rotate_check:
name: 旋转检查 name: 旋转检查
description: 是否进行旋转检查。 description: 是否进行旋转检查0=否,1=是)
default: true default: 1
example: true example: 1
selector: selector:
boolean: select:
options:
- label: "否"
value: "0"
- label: "是"
value: "1"
create_person:
name: 创建人员
description: 在人员库中创建新人员。
fields:
person_id:
name: 人员ID
description: 人员的唯一标识符。
required: true
example: "person_001"
selector:
text:
person_name:
name: 人员名称
description: 人员名称。
required: true
example: "张三"
selector:
text:
group_id:
name: 人员库ID
description: 要添加到的人员库ID。
required: true
example: "Hass"
selector:
text:
image_url:
name: 图片URL
description: 人员人脸图片URL。
example: "https://example.com/face.jpg"
selector:
text:
image_path:
name: 图片路径
description: 人员人脸本地图片路径。
example: "/config/www/face.jpg"
selector:
text:
image_file:
name: 图片文件
description: 人员人脸图片文件(Base64编码)。
selector:
text:
camera_entity_id:
name: 摄像头实体
description: 用于获取人脸图片的摄像头实体ID。
example: "camera.front_door"
selector:
entity:
domain: camera
config_entry_id:
name: 配置项ID
description: 使用的配置项ID(多配置时指定)。
selector:
text:
gender:
name: 性别
description: 性别(0=女,1=男)。
selector:
select:
options:
- label: "女"
value: "0"
- label: "男"
value: "1"
person_tag:
name: 人员标签
description: 人员备注标签。
selector:
text:
quality_control:
name: 质量控制
description: 是否进行质量控制(0=否,1=是)。
default: 1
selector:
select:
options:
- label: "否"
value: "0"
- label: "是"
value: "1"
need_rotate_check:
name: 旋转检查
description: 是否进行旋转检查(0=否,1=是)。
default: 1
selector:
select:
options:
- label: "否"
value: "0"
- label: "是"
value: "1"
delete_person:
name: 删除人员
description: 从人员库中删除人员。
fields:
person_id:
name: 人员ID
description: 要删除的人员ID。
required: true
example: "person_001"
selector:
text:
config_entry_id:
name: 配置项ID
description: 使用的配置项ID(多配置时指定)。
selector:
text:
create_face:
name: 注册人脸
description: 为已有人员添加新的人脸照片。
fields:
person_id:
name: 人员ID
description: 要添加人脸的人员ID。
required: true
example: "person_001"
selector:
text:
image_url:
name: 图片URL
description: 人脸图片URL。
example: "https://example.com/face.jpg"
selector:
text:
image_path:
name: 图片路径
description: 人脸本地图片路径。
example: "/config/www/face.jpg"
selector:
text:
image_file:
name: 图片文件
description: 人脸图片文件(Base64编码)。
selector:
text:
camera_entity_id:
name: 摄像头实体
description: 用于获取人脸图片的摄像头实体ID。
example: "camera.front_door"
selector:
entity:
domain: camera
config_entry_id:
name: 配置项ID
description: 使用的配置项ID(多配置时指定)。
selector:
text:
quality_control:
name: 质量控制
description: 是否进行质量控制(0=否,1=是)。
default: 1
selector:
select:
options:
- label: "否"
value: "0"
- label: "是"
value: "1"
need_rotate_check:
name: 旋转检查
description: 是否进行旋转检查(0=否,1=是)。
default: 1
selector:
select:
options:
- label: "否"
value: "0"
- label: "是"
value: "1"
delete_face:
name: 删除人脸
description: 删除指定人员的人脸。
fields:
person_id:
name: 人员ID
description: 人员ID。
required: true
example: "person_001"
selector:
text:
face_id:
name: 人脸ID
description: 要删除的人脸ID。
required: true
example: "3440118425101275818"
selector:
text:
config_entry_id:
name: 配置项ID
description: 使用的配置项ID(多配置时指定)。
selector:
text:
+206 -11
View File
@@ -12,7 +12,7 @@
"name": "名称" "name": "名称"
}, },
"data_description": { "data_description": {
"secret_id": "腾讯云API的Secret ID", "secret_id": "腾讯云API的Secret ID,以AKID开头",
"secret_key": "腾讯云API的Secret Key", "secret_key": "腾讯云API的Secret Key",
"region": "腾讯云服务区域,例如:ap-shanghai", "region": "腾讯云服务区域,例如:ap-shanghai",
"person_group_id": "人脸库人员组ID,用于存储和搜索人脸", "person_group_id": "人脸库人员组ID,用于存储和搜索人脸",
@@ -21,7 +21,10 @@
} }
}, },
"error": { "error": {
"invalid_config": "无效的配置" "invalid_config": "无效的配置",
"auth_failure": "认证失败,请检查Secret ID和Secret Key",
"network_error": "网络错误,请检查网络连接和区域设置",
"rate_limit_exceeded": "API调用频率过高,请稍后再试"
}, },
"abort": { "abort": {
"already_configured": "已经配置了腾讯云人脸识别服务" "already_configured": "已经配置了腾讯云人脸识别服务"
@@ -31,21 +34,65 @@
"step": { "step": {
"init": { "init": {
"title": "腾讯云人脸识别选项", "title": "腾讯云人脸识别选项",
"description": "配置腾讯云人脸识别服务选项", "description": "请选择要管理的项目",
"data": { "data": {
"region": "区域" "action": "操作"
}
},
"config": {
"title": "配置设置",
"description": "修改腾讯云人脸识别的配置设置",
"data": {
"region": "区域",
"person_group_id": "人员组ID"
}, },
"data_description": { "data_description": {
"region": "腾讯云服务区域,例如:ap-shanghai" "region": "腾讯云服务区域",
"person_group_id": "人脸库人员组ID"
} }
},
"test_connection": {
"title": "测试连接",
"description": "测试腾讯云API连接状态\n连接状态: {connection_status}"
},
"reauth": {
"title": "重新认证",
"description": "更新腾讯云API凭据\n当前Secret ID: {current_secret_id}\n当前区域: {current_region}",
"data": {
"secret_id": "Secret ID",
"secret_key": "Secret Key"
},
"data_description": {
"secret_id": "新的腾讯云API Secret ID",
"secret_key": "新的腾讯云API Secret Key"
}
}
},
"error": {
"auth_failure": "认证失败,请检查Secret ID和Secret Key",
"network_error": "网络错误,请检查网络连接和区域设置",
"invalid_config": "配置验证失败"
}
},
"entity": {
"sensor": {
"status_sensor": {
"name": "状态"
},
"person_sensor": {
"name": "人员"
} }
} }
}, },
"services": { "services": {
"face_search": { "face_search": {
"name": "人脸搜索", "name": "人脸搜索",
"description": "搜索人脸", "description": "在指定的人员库中搜索人脸",
"fields": { "fields": {
"group_id": {
"name": "人员库ID",
"description": "要搜索的人员库ID"
},
"image_url": { "image_url": {
"name": "图片URL", "name": "图片URL",
"description": "要搜索的图片URL" "description": "要搜索的图片URL"
@@ -58,6 +105,14 @@
"name": "图片文件", "name": "图片文件",
"description": "要搜索的图片文件(Base64编码)" "description": "要搜索的图片文件(Base64编码)"
}, },
"camera_entity_id": {
"name": "摄像头实体",
"description": "用于获取图片的摄像头实体ID"
},
"config_entry_id": {
"name": "配置项ID",
"description": "使用的配置项ID(多配置时指定)"
},
"max_face_num": { "max_face_num": {
"name": "最大人脸数量", "name": "最大人脸数量",
"description": "最多处理的人脸数量" "description": "最多处理的人脸数量"
@@ -72,11 +127,11 @@
}, },
"quality_control": { "quality_control": {
"name": "质量控制", "name": "质量控制",
"description": "是否进行质量控制" "description": "是否进行质量控制0=否,1=是)"
}, },
"need_rotate_check": { "need_rotate_check": {
"name": "旋转检查", "name": "旋转检查",
"description": "是否进行旋转检查" "description": "是否进行旋转检查0=否,1=是)"
}, },
"face_match_threshold": { "face_match_threshold": {
"name": "人脸匹配阈值", "name": "人脸匹配阈值",
@@ -100,6 +155,14 @@
"name": "图片文件", "name": "图片文件",
"description": "要检测的图片文件(Base64编码)" "description": "要检测的图片文件(Base64编码)"
}, },
"camera_entity_id": {
"name": "摄像头实体",
"description": "用于获取图片的摄像头实体ID"
},
"config_entry_id": {
"name": "配置项ID",
"description": "使用的配置项ID(多配置时指定)"
},
"max_face_num": { "max_face_num": {
"name": "最大人脸数量", "name": "最大人脸数量",
"description": "最多检测的人脸数量" "description": "最多检测的人脸数量"
@@ -110,7 +173,7 @@
}, },
"need_rotate_check": { "need_rotate_check": {
"name": "旋转检查", "name": "旋转检查",
"description": "是否进行旋转检查" "description": "是否进行旋转检查0=否,1=是)"
} }
} }
}, },
@@ -130,15 +193,147 @@
"name": "图片文件", "name": "图片文件",
"description": "要分析的图片文件(Base64编码)" "description": "要分析的图片文件(Base64编码)"
}, },
"camera_entity_id": {
"name": "摄像头实体",
"description": "用于获取图片的摄像头实体ID"
},
"config_entry_id": {
"name": "配置项ID",
"description": "使用的配置项ID(多配置时指定)"
},
"max_face_num": { "max_face_num": {
"name": "最大人脸数量", "name": "最大人脸数量",
"description": "最多分析的人脸数量" "description": "最多分析的人脸数量"
}, },
"need_rotate_check": { "need_rotate_check": {
"name": "旋转检查", "name": "旋转检查",
"description": "是否进行旋转检查" "description": "是否进行旋转检查0=否,1=是)"
}
}
},
"create_person": {
"name": "创建人员",
"description": "在人员库中创建新人员",
"fields": {
"person_id": {
"name": "人员ID",
"description": "人员的唯一标识符"
},
"person_name": {
"name": "人员名称",
"description": "人员名称"
},
"group_id": {
"name": "人员库ID",
"description": "要添加到的人员库ID"
},
"image_url": {
"name": "图片URL",
"description": "人员人脸图片URL"
},
"image_path": {
"name": "图片路径",
"description": "人员人脸本地图片路径"
},
"image_file": {
"name": "图片文件",
"description": "人员人脸图片文件(Base64编码)"
},
"camera_entity_id": {
"name": "摄像头实体",
"description": "用于获取人脸图片的摄像头实体ID"
},
"config_entry_id": {
"name": "配置项ID",
"description": "使用的配置项ID(多配置时指定)"
},
"gender": {
"name": "性别",
"description": "性别(0=女,1=男)"
},
"person_tag": {
"name": "人员标签",
"description": "人员备注标签"
},
"quality_control": {
"name": "质量控制",
"description": "是否进行质量控制(0=否,1=是)"
},
"need_rotate_check": {
"name": "旋转检查",
"description": "是否进行旋转检查(0=否,1=是)"
}
}
},
"delete_person": {
"name": "删除人员",
"description": "从人员库中删除人员",
"fields": {
"person_id": {
"name": "人员ID",
"description": "要删除的人员ID"
},
"config_entry_id": {
"name": "配置项ID",
"description": "使用的配置项ID(多配置时指定)"
}
}
},
"create_face": {
"name": "注册人脸",
"description": "为已有人员添加新的人脸照片",
"fields": {
"person_id": {
"name": "人员ID",
"description": "要添加人脸的人员ID"
},
"image_url": {
"name": "图片URL",
"description": "人脸图片URL"
},
"image_path": {
"name": "图片路径",
"description": "人脸本地图片路径"
},
"image_file": {
"name": "图片文件",
"description": "人脸图片文件(Base64编码)"
},
"camera_entity_id": {
"name": "摄像头实体",
"description": "用于获取人脸图片的摄像头实体ID"
},
"config_entry_id": {
"name": "配置项ID",
"description": "使用的配置项ID(多配置时指定)"
},
"quality_control": {
"name": "质量控制",
"description": "是否进行质量控制(0=否,1=是)"
},
"need_rotate_check": {
"name": "旋转检查",
"description": "是否进行旋转检查(0=否,1=是)"
}
}
},
"delete_face": {
"name": "删除人脸",
"description": "删除指定人员的人脸",
"fields": {
"person_id": {
"name": "人员ID",
"description": "人员ID"
},
"face_id": {
"name": "人脸ID",
"description": "要删除的人脸ID"
},
"config_entry_id": {
"name": "配置项ID",
"description": "使用的配置项ID(多配置时指定)"
} }
} }
} }
} }
} }
+443 -331
View File
File diff suppressed because it is too large Load Diff