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
+179 -78
View File
@@ -1,7 +1,7 @@
"""人脸识别核心功能"""
import base64
import logging
import json
from typing import Dict, Any, List, Optional
from homeassistant.core import HomeAssistant
@@ -10,14 +10,19 @@ from homeassistant.exceptions import HomeAssistantError
from .const import (
ATTR_IMAGE_URL,
ATTR_IMAGE_PATH,
ATTR_CAMERA_ENTITY_ID,
ATTR_MAX_FACE_NUM,
ATTR_MIN_FACE_SIZE,
ATTR_MAX_USER_NUM,
ATTR_QUALITY_CONTROL,
ATTR_NEED_ROTATE_CHECK,
ATTR_FACE_MATCH_THRESHOLD,
ERROR_IMAGE_NOT_FOUND,
ERROR_FACE_NOT_DETECTED,
ATTR_PERSON_ID,
ATTR_PERSON_NAME,
ATTR_GENDER,
ATTR_FACE_ID,
ATTR_PERSON_TAG,
ATTR_GROUP_ID,
)
from .tencent_cloud_client import TencentCloudClient
@@ -32,11 +37,50 @@ class FaceRecognition:
self.hass = hass
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(
self,
image_url: str = None,
image_path: str = None,
image_file: str = None,
camera_entity_id: str = None,
group_ids: List[str] = None,
max_face_num: int = 1,
min_face_size: int = 34,
@@ -47,15 +91,15 @@ class FaceRecognition:
) -> Dict[str, Any]:
"""搜索人脸"""
try:
# 验证参数
self._validate_image_params(image_url, image_path, image_file)
self._validate_image_params(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
)
# 调用腾讯云API搜索人脸
result = await self.hass.async_add_executor_job(
self.client.search_faces,
image_url,
image_path,
image_file,
None, None, None, image_base64,
group_ids,
max_face_num,
min_face_size,
@@ -65,7 +109,6 @@ class FaceRecognition:
face_match_threshold
)
# 处理结果
if result.get("success", False):
processed_faces = self._process_search_results(result.get("faces", []))
return {
@@ -76,7 +119,6 @@ class FaceRecognition:
"error_message": None
}
else:
# 返回错误信息
return {
"success": False,
"faces": [],
@@ -94,6 +136,8 @@ class FaceRecognition:
"error_code": "invalid_parameter",
"error_message": str(ex)
}
except HomeAssistantError:
raise
except Exception as ex:
_LOGGER.error("人脸搜索失败: %s", ex)
return {
@@ -104,11 +148,6 @@ class FaceRecognition:
"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]]:
"""处理人脸搜索结果"""
processed_results = []
@@ -130,74 +169,31 @@ class FaceRecognition:
processed_results.append(processed_result)
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(
self,
image_url: str = None,
image_path: str = None,
image_file: str = None,
camera_entity_id: str = None,
max_face_num: int = 1,
need_rotate_check: int = 1
) -> Dict[str, Any]:
"""获取人脸属性"""
try:
# 验证参数
self._validate_image_params(image_url, image_path, image_file)
self._validate_image_params(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
)
# 调用腾讯云API获取人脸属性
result = await self.hass.async_add_executor_job(
self.client.get_face_attributes,
image_url,
image_path,
image_file,
None, None, None, image_base64,
max_face_num,
need_rotate_check
)
# 处理结果
if result.get("success", False):
return {
"success": True,
@@ -207,7 +203,6 @@ class FaceRecognition:
"error_message": None
}
else:
# 返回错误信息
return {
"success": False,
"faces": [],
@@ -216,6 +211,8 @@ class FaceRecognition:
"error_message": result.get("error_message")
}
except HomeAssistantError:
raise
except Exception as ex:
_LOGGER.error("获取人脸属性失败: %s", ex)
return {
@@ -226,34 +223,32 @@ class FaceRecognition:
"error_message": str(ex)
}
# 移除 _get_face_attributes_sync 方法,因为现在直接使用客户端的 get_face_attributes 方法
async def async_detect_faces(
self,
image_url: str = None,
image_path: str = None,
image_file: str = None,
camera_entity_id: str = None,
max_face_num: int = 1,
min_face_size: int = 34,
need_rotate_check: int = 1
) -> Dict[str, Any]:
"""检测人脸"""
try:
# 验证参数
self._validate_image_params(image_url, image_path, image_file)
self._validate_image_params(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
)
# 调用腾讯云API检测人脸
result = await self.hass.async_add_executor_job(
self.client.detect_faces,
image_url,
image_path,
image_file,
None, None, None, image_base64,
max_face_num,
min_face_size,
need_rotate_check
)
# 处理结果
if result.get("success", False):
return {
"success": True,
@@ -263,7 +258,6 @@ class FaceRecognition:
"error_message": None
}
else:
# 返回错误信息
return {
"success": False,
"faces": [],
@@ -272,6 +266,8 @@ class FaceRecognition:
"error_message": result.get("error_message")
}
except HomeAssistantError:
raise
except Exception as ex:
_LOGGER.error("人脸检测失败: %s", ex)
return {
@@ -282,4 +278,109 @@ class FaceRecognition:
"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)
}