refactor: 重构服务层代码结构并优化性能
- 提取公共服务方法 _get_face_recognition、_check_result、_call_service - 添加 HTTP Session 池化与 LRU 图片缓存 - 完善 create_person 错误处理,返回统一格式 - 卸载时正确关闭 client 连接释放资源 - 传感器适配 API 返回结构变化 - 全面更新 README 文档,补充服务说明与示例
This commit is contained in:
@@ -1,100 +1,271 @@
|
||||
# 腾讯云人脸识别Home Assistant插件
|
||||
# 腾讯云人脸识别 Home Assistant 插件
|
||||
|
||||
这是一个基于腾讯云人脸识别API的Home Assistant插件,提供了人脸搜索功能。
|
||||
基于腾讯云人脸识别 API 的 Home Assistant 自定义集成,提供完整的人脸管理能力。
|
||||
|
||||
## 功能特性
|
||||
|
||||
- **人脸搜索**:支持通过图片URL或本地文件路径搜索人脸,返回匹配的人员信息
|
||||
- **人脸搜索**:通过图片 URL、本地文件、Base64 编码或摄像头实体搜索人脸,返回匹配的人员信息
|
||||
- **人脸检测**:检测图片中的人脸位置和尺寸
|
||||
- **人脸属性分析**:获取人脸的性别、年龄、表情等属性
|
||||
- **人员管理**:创建/删除人员,支持带图或无图创建
|
||||
- **人脸管理**:为人员注册新人脸或删除已有人脸
|
||||
- **传感器**:自动同步人员库状态,提供人员数量和连接状态传感器
|
||||
- **多配置支持**:支持多个腾讯云账号同时接入
|
||||
- **自动重试**:内置指数退避重试机制,应对网络波动和限流
|
||||
- **连接复用**:HTTP Session 池化,减少重复连接开销
|
||||
- **图片缓存**:URL/路径图片 LRU 缓存,避免重复下载
|
||||
|
||||
## 安装
|
||||
|
||||
1. 将`custom_components/tencent_face_recognition`目录复制到Home Assistant的`custom_components`目录下
|
||||
2. 重启Home Assistant
|
||||
3. 在"配置" -> "集成"中点击"+"添加集成
|
||||
4. 搜索"腾讯云人脸识别"并点击
|
||||
5. 输入您的腾讯云凭据和配置信息
|
||||
### 方式一:手动安装
|
||||
|
||||
## 腾讯云相关页面入口
|
||||
1. 将 `tencent_face_recognition` 目录复制到 Home Assistant 的 `custom_components` 目录下
|
||||
2. 重启 Home Assistant
|
||||
|
||||
### 方式二:HACS 安装
|
||||
|
||||
在 HACS 中搜索"腾讯云人脸识别"并安装。
|
||||
|
||||
### 配置集成
|
||||
|
||||
1. 在"设置" → "设备与服务"中点击"添加集成"
|
||||
2. 搜索"腾讯云人脸识别"
|
||||
3. 输入腾讯云 Secret ID、Secret Key 及可选的区域和人员库 ID
|
||||
|
||||
## 腾讯云相关
|
||||
|
||||
- [腾讯云人脸识别](https://curl.qcloud.com/oqiFPa7h)
|
||||
- [人脸管理控制台](https://curl.qcloud.com/RMATbuiO)
|
||||
- [获取API密钥](https://curl.qcloud.com/cT0HlJRW)
|
||||
- [获取 API 密钥](https://curl.qcloud.com/cT0HlJRW)
|
||||
|
||||
## 配置
|
||||
## 配置项
|
||||
|
||||
### 必需配置
|
||||
|
||||
- **Secret ID**:腾讯云API的Secret ID
|
||||
- **Secret Key**:腾讯云API的Secret Key
|
||||
|
||||
### 可选配置
|
||||
|
||||
- **人员库ID**:默认使用的人员库ID(默认值:Hass)
|
||||
- **区域**:腾讯云服务区域(默认值:ap-shanghai)
|
||||
- **名称**:集成名称(默认值:腾讯云人脸识别)
|
||||
| 参数 | 必需 | 默认值 | 说明 |
|
||||
|------|------|--------|------|
|
||||
| Secret ID | 是 | - | 腾讯云 API 的 Secret ID |
|
||||
| Secret Key | 是 | - | 腾讯云 API 的 Secret Key |
|
||||
| 区域 | 否 | `ap-shanghai` | 腾讯云服务区域 |
|
||||
| 人员库 ID | 否 | `Hass` | 默认使用的人员库 ID |
|
||||
|
||||
## 服务
|
||||
|
||||
插件提供以下服务,可以通过开发者工具中的"服务"选项卡调用:
|
||||
所有服务均支持 `response_variable` 获取返回结果,统一返回格式:
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"error": null,
|
||||
"error_code": null,
|
||||
"error_message": null
|
||||
}
|
||||
```
|
||||
|
||||
失败时:
|
||||
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"error": "错误描述",
|
||||
"error_code": "error_code",
|
||||
"error_message": "详细错误信息"
|
||||
}
|
||||
```
|
||||
|
||||
通用图片参数(按优先级取第一个有效的):
|
||||
|
||||
| 参数 | 说明 |
|
||||
|------|------|
|
||||
| `camera_entity_id` | 摄像头实体 ID(优先) |
|
||||
| `image_url` | 图片 URL |
|
||||
| `image_path` | 本地文件路径 |
|
||||
| `image_file` | Base64 编码图片 |
|
||||
| `config_entry_id` | 多配置时指定配置项 ID |
|
||||
|
||||
---
|
||||
|
||||
### 人脸搜索
|
||||
|
||||
**服务名称**:`tencent_face_recognition.face_search`
|
||||
`tencent_face_recognition.face_search`
|
||||
|
||||
**描述**:在人员库中搜索人脸
|
||||
在人员库中搜索匹配的人脸。搜索成功时会触发 `face_detected` 事件。
|
||||
|
||||
**参数**:
|
||||
**独有参数**:
|
||||
|
||||
- `image_url`(可选):要搜索的图片URL
|
||||
- `image_path`(可选):要搜索的本地图片路径
|
||||
- `person_group_id`(可选):要搜索的人员库ID,留空使用默认人员库
|
||||
- `max_face_num`(可选,默认1):最多处理的人脸数量
|
||||
- `min_face_size`(可选,默认34):最小人脸尺寸(像素)
|
||||
- `max_user_num`(可选,默认5):最多返回的匹配人员数量
|
||||
- `quality_control`(可选,默认1):是否进行质量控制(0:不控制,1:低质量控制,2:高质量控制)
|
||||
- `need_rotate_check`(可选,默认1):是否进行旋转检查(0:不检查,1:检查)
|
||||
- `face_match_threshold`(可选,默认60.0):人脸匹配阈值(0-100)
|
||||
| 参数 | 默认值 | 说明 |
|
||||
|------|--------|------|
|
||||
| `group_id` (必填) | - | 要搜索的人员库 ID |
|
||||
| `max_face_num` | 1 | 最多处理的人脸数量 |
|
||||
| `min_face_size` | 34 | 最小人脸尺寸(像素) |
|
||||
| `max_user_num` | 5 | 最多返回的匹配人员数量 |
|
||||
| `quality_control` | 1 | 质量控制(0=关闭,1=开启) |
|
||||
| `need_rotate_check` | 1 | 旋转检查(0=关闭,1=开启) |
|
||||
| `face_match_threshold` | 60.0 | 匹配阈值(0-100) |
|
||||
|
||||
**示例**:
|
||||
|
||||
``` yaml
|
||||
```yaml
|
||||
action: tencent_face_recognition.face_search
|
||||
response_variable: face_recognition_result_raw
|
||||
response_variable: search_result
|
||||
data:
|
||||
group_id: Hass
|
||||
face_match_threshold: 60
|
||||
min_face_size: 34
|
||||
max_face_num: 10
|
||||
max_user_num: 10
|
||||
image_path: /config/www/camera/face.jpg
|
||||
group_id: "Hass"
|
||||
image_path: "/config/www/camera/face.jpg"
|
||||
face_match_threshold: 70
|
||||
max_face_num: 5
|
||||
```
|
||||
|
||||
|
||||
**触发事件** `face_detected`:
|
||||
|
||||
```yaml
|
||||
service: tencent_face_recognition.face_search
|
||||
data:
|
||||
image_url: "https://example.com/face.jpg"
|
||||
group_id: "Hass"
|
||||
- trigger:
|
||||
- platform: event
|
||||
event_type: face_detected
|
||||
action:
|
||||
- service: persistent_notification.create
|
||||
data:
|
||||
message: "识别到 {{ trigger.event.data.person_name }},置信度 {{ trigger.event.data.score }}"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 人脸检测
|
||||
|
||||
`tencent_face_recognition.detect_face`
|
||||
|
||||
检测图片中的人脸位置和尺寸。
|
||||
|
||||
| 参数 | 默认值 | 说明 |
|
||||
|------|--------|------|
|
||||
| `max_face_num` | 1 | 最多检测的人脸数量 |
|
||||
| `min_face_size` | 34 | 最小人脸尺寸(像素) |
|
||||
| `need_rotate_check` | 1 | 旋转检查 |
|
||||
|
||||
**示例**:
|
||||
|
||||
```yaml
|
||||
action: tencent_face_recognition.detect_face
|
||||
response_variable: detect_result
|
||||
data:
|
||||
camera_entity_id: "camera.front_door"
|
||||
max_face_num: 10
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 获取人脸属性
|
||||
|
||||
`tencent_face_recognition.get_face_attributes`
|
||||
|
||||
获取图片中人脸的性别、年龄、表情等属性信息。
|
||||
|
||||
| 参数 | 默认值 | 说明 |
|
||||
|------|--------|------|
|
||||
| `max_face_num` | 1 | 最多分析的人脸数量 |
|
||||
| `need_rotate_check` | 1 | 旋转检查 |
|
||||
|
||||
**示例**:
|
||||
|
||||
```yaml
|
||||
action: tencent_face_recognition.get_face_attributes
|
||||
response_variable: attr_result
|
||||
data:
|
||||
image_url: "https://example.com/photo.jpg"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 创建人员
|
||||
|
||||
`tencent_face_recognition.create_person`
|
||||
|
||||
在人员库中创建新人员,图片为可选(支持先建人后传图)。
|
||||
|
||||
| 参数 | 默认值 | 说明 |
|
||||
|------|--------|------|
|
||||
| `person_id` (必填) | - | 人员唯一标识符 |
|
||||
| `person_name` (必填) | - | 人员名称 |
|
||||
| `group_id` (必填) | - | 所属人员库 ID |
|
||||
| `gender` | - | 性别(0=女,1=男) |
|
||||
| `person_tag` | - | 备注标签 |
|
||||
| `quality_control` | 1 | 质量控制 |
|
||||
| `need_rotate_check` | 1 | 旋转检查 |
|
||||
|
||||
**示例**:
|
||||
|
||||
```yaml
|
||||
action: tencent_face_recognition.create_person
|
||||
response_variable: create_result
|
||||
data:
|
||||
person_id: "person_001"
|
||||
person_name: "张三"
|
||||
group_id: "Hass"
|
||||
image_url: "https://example.com/face.jpg"
|
||||
gender: 1
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 删除人员
|
||||
|
||||
`tencent_face_recognition.delete_person`
|
||||
|
||||
从人员库中删除人员。
|
||||
|
||||
| 参数 | 说明 |
|
||||
|------|------|
|
||||
| `person_id` (必填) | 要删除的人员 ID |
|
||||
|
||||
---
|
||||
|
||||
### 注册人脸
|
||||
|
||||
`tencent_face_recognition.create_face`
|
||||
|
||||
为已有人员添加新的人脸照片。
|
||||
|
||||
| 参数 | 默认值 | 说明 |
|
||||
|------|--------|------|
|
||||
| `person_id` (必填) | - | 人员 ID |
|
||||
| `quality_control` | 1 | 质量控制 |
|
||||
| `need_rotate_check` | 1 | 旋转检查 |
|
||||
|
||||
---
|
||||
|
||||
### 删除人脸
|
||||
|
||||
`tencent_face_recognition.delete_face`
|
||||
|
||||
删除指定人员的人脸。
|
||||
|
||||
| 参数 | 说明 |
|
||||
|------|------|
|
||||
| `person_id` (必填) | 人员 ID |
|
||||
| `face_id` (必填) | 要删除的人脸 ID |
|
||||
|
||||
---
|
||||
|
||||
## 传感器
|
||||
|
||||
集成会自动创建以下传感器:
|
||||
|
||||
| 传感器 | 说明 |
|
||||
|--------|------|
|
||||
| 状态传感器 | 显示连接状态(已连接/未连接/连接错误) |
|
||||
| 人员传感器 | 每个注册人员一个传感器,显示名称和属性 |
|
||||
|
||||
传感器每 5 分钟自动刷新。
|
||||
|
||||
## 故障排除
|
||||
|
||||
### 常见问题
|
||||
| 问题 | 解决方案 |
|
||||
|------|----------|
|
||||
| 配置失败 | 检查 Secret ID/Key 是否正确,确认以 AKID 开头 |
|
||||
| 图片处理失败 | 确保 URL 可访问或本地路径正确,图片格式为 JPG/PNG/BMP/GIF,大小不超过 10MB |
|
||||
| 人脸检测失败 | 确保图片中包含清晰的人脸,人脸尺寸不小于 34 像素 |
|
||||
| API 调用失败 | 检查账户余额和 API 调用配额 |
|
||||
| 限流错误 | 降低调用频率,插件已内置重试机制 |
|
||||
|
||||
1. **配置失败**:请检查腾讯云凭据是否正确,以及是否有足够的权限
|
||||
2. **图片处理失败**:请确保图片URL可访问或本地文件路径正确
|
||||
3. **人脸检测失败**:请确保图片中包含清晰的人脸,且人脸尺寸足够大
|
||||
4. **API调用失败**:请检查腾讯云账户余额是否充足,以及API调用配额是否足够
|
||||
|
||||
### 日志查看
|
||||
|
||||
在Home Assistant的"开发者工具" -> "日志"中查看插件日志,可以获取更多错误信息。
|
||||
|
||||
## 贡献
|
||||
|
||||
欢迎提交Issue和Pull Request来改进这个插件。
|
||||
在"开发者工具" → "日志"中开启调试日志可获取更详细的错误信息。
|
||||
|
||||
## 许可证
|
||||
|
||||
MIT许可证
|
||||
MIT 许可证
|
||||
|
||||
+4
-1
@@ -67,7 +67,10 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
if unload_ok:
|
||||
await async_unload_services(hass)
|
||||
|
||||
hass.data[DOMAIN].pop(entry.entry_id)
|
||||
entry_data = hass.data[DOMAIN].pop(entry.entry_id, None)
|
||||
if entry_data and "client" in entry_data:
|
||||
client = entry_data["client"]
|
||||
await hass.async_add_executor_job(client.close)
|
||||
|
||||
return unload_ok
|
||||
|
||||
|
||||
+162
-245
@@ -33,28 +33,28 @@ class FaceRecognition:
|
||||
"""人脸识别核心功能"""
|
||||
|
||||
def __init__(self, hass: HomeAssistant, client: TencentCloudClient):
|
||||
"""初始化人脸识别功能"""
|
||||
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,
|
||||
image_url: Optional[str] = None,
|
||||
image_path: Optional[str] = None,
|
||||
image_file: Optional[str] = None,
|
||||
camera_entity_id: Optional[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
|
||||
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)
|
||||
@@ -66,90 +66,91 @@ class FaceRecognition:
|
||||
|
||||
def _validate_image_params(
|
||||
self,
|
||||
image_url: str = None,
|
||||
image_path: str = None,
|
||||
image_file: str = None,
|
||||
camera_entity_id: str = None,
|
||||
image_url: Optional[str] = None,
|
||||
image_path: Optional[str] = None,
|
||||
image_file: Optional[str] = None,
|
||||
camera_entity_id: Optional[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 _safe_api_call(
|
||||
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,
|
||||
max_user_num: int = 5,
|
||||
quality_control: int = 1,
|
||||
need_rotate_check: int = 1,
|
||||
face_match_threshold: float = 60.0
|
||||
operation_name: str,
|
||||
client_method,
|
||||
**kwargs,
|
||||
) -> Dict[str, Any]:
|
||||
"""搜索人脸"""
|
||||
try:
|
||||
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
|
||||
)
|
||||
|
||||
result = await self.hass.async_add_executor_job(
|
||||
self.client.search_faces,
|
||||
None, None, None, image_base64,
|
||||
group_ids,
|
||||
max_face_num,
|
||||
min_face_size,
|
||||
max_user_num,
|
||||
quality_control,
|
||||
need_rotate_check,
|
||||
face_match_threshold
|
||||
lambda: client_method(**kwargs)
|
||||
)
|
||||
|
||||
if result.get("success", False):
|
||||
processed_faces = self._process_search_results(result.get("faces", []))
|
||||
return {
|
||||
"success": True,
|
||||
"faces": processed_faces,
|
||||
"error": None,
|
||||
"error_code": None,
|
||||
"error_message": None
|
||||
}
|
||||
else:
|
||||
return {
|
||||
"success": False,
|
||||
"faces": [],
|
||||
"error": result.get("error"),
|
||||
"error_code": result.get("error_code"),
|
||||
"error_message": result.get("error_message")
|
||||
}
|
||||
|
||||
return result
|
||||
except HomeAssistantError:
|
||||
raise
|
||||
except ValueError as ex:
|
||||
_LOGGER.error("人脸搜索参数错误: %s", ex)
|
||||
_LOGGER.error("%s参数错误: %s", operation_name, ex)
|
||||
return {
|
||||
"success": False,
|
||||
"faces": [],
|
||||
"error": str(ex),
|
||||
"error_code": "invalid_parameter",
|
||||
"error_message": str(ex)
|
||||
"error_message": str(ex),
|
||||
}
|
||||
except HomeAssistantError:
|
||||
raise
|
||||
except Exception as ex:
|
||||
_LOGGER.error("人脸搜索失败: %s", ex)
|
||||
_LOGGER.error("%s失败: %s", operation_name, ex)
|
||||
return {
|
||||
"success": False,
|
||||
"faces": [],
|
||||
"error": str(ex),
|
||||
"error_code": "unknown_error",
|
||||
"error_message": str(ex)
|
||||
"error_message": str(ex),
|
||||
}
|
||||
|
||||
async def async_search_faces(
|
||||
self,
|
||||
image_url: Optional[str] = None,
|
||||
image_path: Optional[str] = None,
|
||||
image_file: Optional[str] = None,
|
||||
camera_entity_id: Optional[str] = None,
|
||||
group_ids: Optional[List[str]] = None,
|
||||
max_face_num: int = 1,
|
||||
min_face_size: int = 34,
|
||||
max_user_num: int = 5,
|
||||
quality_control: int = 1,
|
||||
need_rotate_check: int = 1,
|
||||
face_match_threshold: float = 60.0,
|
||||
) -> Dict[str, Any]:
|
||||
self._validate_image_params(image_url, image_path, image_file, camera_entity_id)
|
||||
|
||||
if not group_ids:
|
||||
raise ValueError("必须提供至少一个人员库ID (group_ids)")
|
||||
|
||||
image_base64 = await self._get_image_base64_from_source(
|
||||
image_url=image_url,
|
||||
image_path=image_path,
|
||||
image_file=image_file,
|
||||
camera_entity_id=camera_entity_id,
|
||||
)
|
||||
|
||||
result = await self._safe_api_call(
|
||||
"人脸搜索",
|
||||
self.client.search_faces,
|
||||
image_base64=image_base64,
|
||||
group_ids=group_ids,
|
||||
max_face_num=max_face_num,
|
||||
min_face_size=min_face_size,
|
||||
max_user_num=max_user_num,
|
||||
quality_control=quality_control,
|
||||
need_rotate_check=need_rotate_check,
|
||||
face_match_threshold=face_match_threshold,
|
||||
)
|
||||
|
||||
if result.get("success", False):
|
||||
result["faces"] = self._process_search_results(result.get("faces", []))
|
||||
|
||||
return result
|
||||
|
||||
def _process_search_results(self, results: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
||||
"""处理人脸搜索结果"""
|
||||
processed_results = []
|
||||
for result in results:
|
||||
processed_result = {
|
||||
@@ -172,215 +173,131 @@ class FaceRecognition:
|
||||
|
||||
async def async_get_face_attributes(
|
||||
self,
|
||||
image_url: str = None,
|
||||
image_path: str = None,
|
||||
image_file: str = None,
|
||||
camera_entity_id: str = None,
|
||||
image_url: Optional[str] = None,
|
||||
image_path: Optional[str] = None,
|
||||
image_file: Optional[str] = None,
|
||||
camera_entity_id: Optional[str] = None,
|
||||
max_face_num: int = 1,
|
||||
need_rotate_check: int = 1
|
||||
need_rotate_check: int = 1,
|
||||
) -> Dict[str, Any]:
|
||||
"""获取人脸属性"""
|
||||
try:
|
||||
self._validate_image_params(image_url, image_path, image_file, camera_entity_id)
|
||||
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
|
||||
)
|
||||
image_base64 = await self._get_image_base64_from_source(
|
||||
image_url=image_url,
|
||||
image_path=image_path,
|
||||
image_file=image_file,
|
||||
camera_entity_id=camera_entity_id,
|
||||
)
|
||||
|
||||
result = await self.hass.async_add_executor_job(
|
||||
self.client.get_face_attributes,
|
||||
None, None, None, image_base64,
|
||||
max_face_num,
|
||||
need_rotate_check
|
||||
)
|
||||
|
||||
if result.get("success", False):
|
||||
return {
|
||||
"success": True,
|
||||
"faces": result.get("faces", []),
|
||||
"error": None,
|
||||
"error_code": None,
|
||||
"error_message": None
|
||||
}
|
||||
else:
|
||||
return {
|
||||
"success": False,
|
||||
"faces": [],
|
||||
"error": result.get("error"),
|
||||
"error_code": result.get("error_code"),
|
||||
"error_message": result.get("error_message")
|
||||
}
|
||||
|
||||
except HomeAssistantError:
|
||||
raise
|
||||
except Exception as ex:
|
||||
_LOGGER.error("获取人脸属性失败: %s", ex)
|
||||
return {
|
||||
"success": False,
|
||||
"faces": [],
|
||||
"error": str(ex),
|
||||
"error_code": "unknown_error",
|
||||
"error_message": str(ex)
|
||||
}
|
||||
return await self._safe_api_call(
|
||||
"获取人脸属性",
|
||||
self.client.get_face_attributes,
|
||||
image_base64=image_base64,
|
||||
max_face_num=max_face_num,
|
||||
need_rotate_check=need_rotate_check,
|
||||
)
|
||||
|
||||
async def async_detect_faces(
|
||||
self,
|
||||
image_url: str = None,
|
||||
image_path: str = None,
|
||||
image_file: str = None,
|
||||
camera_entity_id: str = None,
|
||||
image_url: Optional[str] = None,
|
||||
image_path: Optional[str] = None,
|
||||
image_file: Optional[str] = None,
|
||||
camera_entity_id: Optional[str] = None,
|
||||
max_face_num: int = 1,
|
||||
min_face_size: int = 34,
|
||||
need_rotate_check: int = 1
|
||||
need_rotate_check: int = 1,
|
||||
) -> Dict[str, Any]:
|
||||
"""检测人脸"""
|
||||
try:
|
||||
self._validate_image_params(image_url, image_path, image_file, camera_entity_id)
|
||||
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
|
||||
)
|
||||
image_base64 = await self._get_image_base64_from_source(
|
||||
image_url=image_url,
|
||||
image_path=image_path,
|
||||
image_file=image_file,
|
||||
camera_entity_id=camera_entity_id,
|
||||
)
|
||||
|
||||
result = await self.hass.async_add_executor_job(
|
||||
self.client.detect_faces,
|
||||
None, None, None, image_base64,
|
||||
max_face_num,
|
||||
min_face_size,
|
||||
need_rotate_check
|
||||
)
|
||||
|
||||
if result.get("success", False):
|
||||
return {
|
||||
"success": True,
|
||||
"faces": result.get("faces", []),
|
||||
"error": None,
|
||||
"error_code": None,
|
||||
"error_message": None
|
||||
}
|
||||
else:
|
||||
return {
|
||||
"success": False,
|
||||
"faces": [],
|
||||
"error": result.get("error"),
|
||||
"error_code": result.get("error_code"),
|
||||
"error_message": result.get("error_message")
|
||||
}
|
||||
|
||||
except HomeAssistantError:
|
||||
raise
|
||||
except Exception as ex:
|
||||
_LOGGER.error("人脸检测失败: %s", ex)
|
||||
return {
|
||||
"success": False,
|
||||
"faces": [],
|
||||
"error": str(ex),
|
||||
"error_code": "unknown_error",
|
||||
"error_message": str(ex)
|
||||
}
|
||||
return await self._safe_api_call(
|
||||
"人脸检测",
|
||||
self.client.detect_faces,
|
||||
image_base64=image_base64,
|
||||
max_face_num=max_face_num,
|
||||
min_face_size=min_face_size,
|
||||
need_rotate_check=need_rotate_check,
|
||||
)
|
||||
|
||||
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,
|
||||
image_url: Optional[str] = None,
|
||||
image_path: Optional[str] = None,
|
||||
image_file: Optional[str] = None,
|
||||
camera_entity_id: Optional[str] = None,
|
||||
gender: Optional[int] = None,
|
||||
person_tag: Optional[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,
|
||||
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_url,
|
||||
image_path=image_path,
|
||||
image_file=image_file,
|
||||
camera_entity_id=camera_entity_id,
|
||||
)
|
||||
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)
|
||||
}
|
||||
|
||||
return await self._safe_api_call(
|
||||
"创建人员",
|
||||
self.client.create_person,
|
||||
person_id=person_id,
|
||||
person_name=person_name,
|
||||
group_id=group_id,
|
||||
image_base64=image_base64,
|
||||
gender=gender,
|
||||
person_tag=person_tag,
|
||||
quality_control=quality_control,
|
||||
need_rotate_check=need_rotate_check,
|
||||
)
|
||||
|
||||
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)
|
||||
}
|
||||
return await self._safe_api_call(
|
||||
"删除人员",
|
||||
self.client.delete_person,
|
||||
person_id=person_id,
|
||||
)
|
||||
|
||||
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,
|
||||
image_url: Optional[str] = None,
|
||||
image_path: Optional[str] = None,
|
||||
image_file: Optional[str] = None,
|
||||
camera_entity_id: Optional[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
|
||||
)
|
||||
image_base64 = await self._get_image_base64_from_source(
|
||||
image_url=image_url,
|
||||
image_path=image_path,
|
||||
image_file=image_file,
|
||||
camera_entity_id=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)
|
||||
}
|
||||
return await self._safe_api_call(
|
||||
"注册人脸",
|
||||
self.client.create_face,
|
||||
person_id=person_id,
|
||||
image_base64=image_base64,
|
||||
quality_control=quality_control,
|
||||
need_rotate_check=need_rotate_check,
|
||||
)
|
||||
|
||||
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)
|
||||
}
|
||||
return await self._safe_api_call(
|
||||
"删除人脸",
|
||||
self.client.delete_face,
|
||||
person_id=person_id,
|
||||
face_id=face_id,
|
||||
)
|
||||
|
||||
@@ -47,13 +47,15 @@ class TencentFaceCoordinator(DataUpdateCoordinator):
|
||||
self.entry, CONF_PERSON_GROUP_ID, DEFAULT_PERSON_GROUP_ID
|
||||
)
|
||||
|
||||
group_info = await self.hass.async_add_executor_job(
|
||||
group_result = await self.hass.async_add_executor_job(
|
||||
self.client.get_group_info, group_id
|
||||
)
|
||||
group_info = group_result if group_result.get("success", False) else {}
|
||||
|
||||
persons = await self.hass.async_add_executor_job(
|
||||
persons_result = await self.hass.async_add_executor_job(
|
||||
self.client.get_person_list_all, group_id
|
||||
)
|
||||
persons = persons_result.get("persons", []) if persons_result.get("success", False) else []
|
||||
|
||||
return {
|
||||
"group_info": group_info,
|
||||
|
||||
+111
-237
@@ -1,7 +1,7 @@
|
||||
"""服务定义"""
|
||||
|
||||
import logging
|
||||
from typing import Dict, Any, List, Optional
|
||||
from typing import Dict, Any
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
@@ -115,7 +115,6 @@ DELETE_FACE_SCHEMA = vol.Schema({
|
||||
|
||||
|
||||
async def async_setup_services(hass: HomeAssistant) -> None:
|
||||
"""设置服务"""
|
||||
if hass.services.has_service(DOMAIN, SERVICE_FACE_SEARCH):
|
||||
return
|
||||
|
||||
@@ -179,7 +178,6 @@ async def async_setup_services(hass: HomeAssistant) -> None:
|
||||
|
||||
|
||||
async def async_unload_services(hass: HomeAssistant) -> None:
|
||||
"""卸载服务"""
|
||||
if any(hass.config_entries.async_entries(DOMAIN)):
|
||||
return
|
||||
|
||||
@@ -198,7 +196,6 @@ async def async_unload_services(hass: HomeAssistant) -> None:
|
||||
|
||||
|
||||
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)
|
||||
@@ -212,44 +209,62 @@ def _get_entry_data(hass: HomeAssistant, config_entry_id: str = None):
|
||||
return None
|
||||
|
||||
|
||||
async def async_face_search_service(call: ServiceCall) -> Dict[str, Any]:
|
||||
"""人脸搜索服务"""
|
||||
hass = call.hass
|
||||
data = call.data
|
||||
|
||||
entry_data = _get_entry_data(hass, data.get(ATTR_CONFIG_ENTRY_ID))
|
||||
def _get_face_recognition(hass: HomeAssistant, config_entry_id: str = None):
|
||||
entry_data = _get_entry_data(hass, config_entry_id)
|
||||
if not entry_data:
|
||||
raise HomeAssistantError("未找到腾讯云人脸识别配置")
|
||||
|
||||
face_recognition = entry_data.get("face_recognition")
|
||||
if not face_recognition:
|
||||
raise HomeAssistantError("腾讯云人脸识别插件未正确初始化")
|
||||
return face_recognition
|
||||
|
||||
|
||||
def _check_result(result: Dict[str, Any], operation_name: str) -> Dict[str, Any]:
|
||||
if not result.get("success", False):
|
||||
error_msg = result.get("error_message", result.get("error", "未知错误"))
|
||||
raise HomeAssistantError(f"{operation_name}失败: {error_msg}")
|
||||
return result
|
||||
|
||||
|
||||
async def _call_service(
|
||||
call: ServiceCall,
|
||||
operation_name: str,
|
||||
method_name: str,
|
||||
**kwargs,
|
||||
) -> Dict[str, Any]:
|
||||
face_recognition = _get_face_recognition(call.hass, call.data.get(ATTR_CONFIG_ENTRY_ID))
|
||||
try:
|
||||
method = getattr(face_recognition, method_name)
|
||||
result = await method(**kwargs)
|
||||
return _check_result(result, operation_name)
|
||||
except HomeAssistantError:
|
||||
raise
|
||||
except Exception as ex:
|
||||
_LOGGER.error("%s服务调用失败: %s", operation_name, ex)
|
||||
raise HomeAssistantError(f"{operation_name}服务调用失败: {ex}")
|
||||
|
||||
|
||||
async def async_face_search_service(call: ServiceCall) -> Dict[str, Any]:
|
||||
data = call.data
|
||||
hass = call.hass
|
||||
|
||||
face_recognition = _get_face_recognition(hass, data.get(ATTR_CONFIG_ENTRY_ID))
|
||||
group_id = data.get(ATTR_GROUP_ID)
|
||||
camera_entity_id = data.get(ATTR_CAMERA_ENTITY_ID)
|
||||
|
||||
try:
|
||||
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)
|
||||
max_face_num = data.get(ATTR_MAX_FACE_NUM, 1)
|
||||
min_face_size = data.get(ATTR_MIN_FACE_SIZE, 34)
|
||||
max_user_num = data.get(ATTR_MAX_USER_NUM, 5)
|
||||
quality_control = data.get(ATTR_QUALITY_CONTROL, 1)
|
||||
need_rotate_check = data.get(ATTR_NEED_ROTATE_CHECK, 1)
|
||||
face_match_threshold = data.get(ATTR_FACE_MATCH_THRESHOLD, 60.0)
|
||||
|
||||
result = await face_recognition.async_search_faces(
|
||||
image_url=image_url,
|
||||
image_path=image_path,
|
||||
image_file=image_file,
|
||||
image_url=data.get(ATTR_IMAGE_URL),
|
||||
image_path=data.get(ATTR_IMAGE_PATH),
|
||||
image_file=data.get(ATTR_IMAGE_FILE),
|
||||
camera_entity_id=camera_entity_id,
|
||||
group_ids=[group_id],
|
||||
max_face_num=max_face_num,
|
||||
min_face_size=min_face_size,
|
||||
max_user_num=max_user_num,
|
||||
quality_control=quality_control,
|
||||
need_rotate_check=need_rotate_check,
|
||||
face_match_threshold=face_match_threshold
|
||||
max_face_num=data.get(ATTR_MAX_FACE_NUM, 1),
|
||||
min_face_size=data.get(ATTR_MIN_FACE_SIZE, 34),
|
||||
max_user_num=data.get(ATTR_MAX_USER_NUM, 5),
|
||||
quality_control=data.get(ATTR_QUALITY_CONTROL, 1),
|
||||
need_rotate_check=data.get(ATTR_NEED_ROTATE_CHECK, 1),
|
||||
face_match_threshold=data.get(ATTR_FACE_MATCH_THRESHOLD, 60.0),
|
||||
)
|
||||
|
||||
if result.get("success", False):
|
||||
@@ -274,229 +289,88 @@ async def async_face_search_service(call: ServiceCall) -> Dict[str, Any]:
|
||||
|
||||
|
||||
async def async_detect_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:
|
||||
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)
|
||||
max_face_num = data.get(ATTR_MAX_FACE_NUM, 1)
|
||||
min_face_size = data.get(ATTR_MIN_FACE_SIZE, 34)
|
||||
need_rotate_check = data.get(ATTR_NEED_ROTATE_CHECK, 1)
|
||||
|
||||
result = await face_recognition.async_detect_faces(
|
||||
image_url=image_url,
|
||||
image_path=image_path,
|
||||
image_file=image_file,
|
||||
camera_entity_id=camera_entity_id,
|
||||
max_face_num=max_face_num,
|
||||
min_face_size=min_face_size,
|
||||
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
|
||||
|
||||
except HomeAssistantError:
|
||||
raise
|
||||
except Exception as ex:
|
||||
_LOGGER.error("人脸检测服务调用失败: %s", ex)
|
||||
raise HomeAssistantError(f"人脸检测服务调用失败: {ex}")
|
||||
return await _call_service(
|
||||
call,
|
||||
"人脸检测",
|
||||
"async_detect_faces",
|
||||
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),
|
||||
max_face_num=data.get(ATTR_MAX_FACE_NUM, 1),
|
||||
min_face_size=data.get(ATTR_MIN_FACE_SIZE, 34),
|
||||
need_rotate_check=data.get(ATTR_NEED_ROTATE_CHECK, 1),
|
||||
)
|
||||
|
||||
|
||||
async def async_get_face_attributes_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:
|
||||
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)
|
||||
max_face_num = data.get(ATTR_MAX_FACE_NUM, 1)
|
||||
need_rotate_check = data.get(ATTR_NEED_ROTATE_CHECK, 1)
|
||||
|
||||
result = await face_recognition.async_get_face_attributes(
|
||||
image_url=image_url,
|
||||
image_path=image_path,
|
||||
image_file=image_file,
|
||||
camera_entity_id=camera_entity_id,
|
||||
max_face_num=max_face_num,
|
||||
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
|
||||
|
||||
except HomeAssistantError:
|
||||
raise
|
||||
except Exception as ex:
|
||||
_LOGGER.error("获取人脸属性服务调用失败: %s", ex)
|
||||
raise HomeAssistantError(f"获取人脸属性服务调用失败: {ex}")
|
||||
return await _call_service(
|
||||
call,
|
||||
"获取人脸属性",
|
||||
"async_get_face_attributes",
|
||||
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),
|
||||
max_face_num=data.get(ATTR_MAX_FACE_NUM, 1),
|
||||
need_rotate_check=data.get(ATTR_NEED_ROTATE_CHECK, 1),
|
||||
)
|
||||
|
||||
|
||||
async def async_create_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_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}")
|
||||
return await _call_service(
|
||||
call,
|
||||
"创建人员",
|
||||
"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),
|
||||
)
|
||||
|
||||
|
||||
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}")
|
||||
return await _call_service(
|
||||
call,
|
||||
"删除人员",
|
||||
"async_delete_person",
|
||||
person_id=data.get(ATTR_PERSON_ID),
|
||||
)
|
||||
|
||||
|
||||
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}")
|
||||
return await _call_service(
|
||||
call,
|
||||
"注册人脸",
|
||||
"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),
|
||||
)
|
||||
|
||||
|
||||
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}")
|
||||
return await _call_service(
|
||||
call,
|
||||
"删除人脸",
|
||||
"async_delete_face",
|
||||
person_id=data.get(ATTR_PERSON_ID),
|
||||
face_id=data.get(ATTR_FACE_ID),
|
||||
)
|
||||
|
||||
+141
-18
@@ -10,6 +10,7 @@ import re
|
||||
import requests
|
||||
import time
|
||||
import uuid
|
||||
from functools import lru_cache
|
||||
from typing import Dict, Any, List, Optional, Union, Callable
|
||||
|
||||
from tencentcloud.common import credential
|
||||
@@ -115,6 +116,14 @@ class TencentCloudClient:
|
||||
self.region = region
|
||||
self.retry_config = retry_config or RetryConfig()
|
||||
self.client = self._create_client()
|
||||
self._session = requests.Session()
|
||||
self._session.mount('http://', requests.adapters.HTTPAdapter(max_retries=2))
|
||||
self._session.mount('https://', requests.adapters.HTTPAdapter(max_retries=2))
|
||||
|
||||
def close(self) -> None:
|
||||
"""释放资源"""
|
||||
if self._session:
|
||||
self._session.close()
|
||||
|
||||
def _create_client(self) -> iai_client.IaiClient:
|
||||
"""创建腾讯云人脸识别客户端"""
|
||||
@@ -161,6 +170,14 @@ class TencentCloudClient:
|
||||
error_msg = error_msg.replace(self.secret_key, "***")
|
||||
return error_msg
|
||||
|
||||
@lru_cache(maxsize=32)
|
||||
def _cached_download_image_as_base64(self, image_url: str) -> str:
|
||||
return self._download_image_as_base64_uncached(image_url)
|
||||
|
||||
@lru_cache(maxsize=32)
|
||||
def _cached_read_local_image_as_base64(self, image_path: str) -> str:
|
||||
return self._read_local_image_as_base64_uncached(image_path)
|
||||
|
||||
def _get_image_base64(self, image_url: str = None, image_path: str = None, image_file: str = None) -> str:
|
||||
"""获取图片的base64编码"""
|
||||
_LOGGER.debug(
|
||||
@@ -177,10 +194,10 @@ class TencentCloudClient:
|
||||
try:
|
||||
if image_url:
|
||||
_LOGGER.debug("使用图片URL: %s", image_url)
|
||||
return self._download_image_as_base64(image_url)
|
||||
return self._cached_download_image_as_base64(image_url)
|
||||
elif image_path:
|
||||
_LOGGER.debug("使用本地图片路径: %s", image_path)
|
||||
return self._read_local_image_as_base64(image_path)
|
||||
return self._cached_read_local_image_as_base64(image_path)
|
||||
elif image_file:
|
||||
_LOGGER.debug("使用上传的图片文件")
|
||||
return self._process_uploaded_image_as_base64(image_file)
|
||||
@@ -192,7 +209,7 @@ class TencentCloudClient:
|
||||
{"image_url": image_url, "image_path": image_path, "error_type": type(ex).__name__}
|
||||
)
|
||||
|
||||
def _download_image_as_base64(self, image_url: str) -> str:
|
||||
def _download_image_as_base64_uncached(self, image_url: str) -> str:
|
||||
"""下载图片并转换为base64编码"""
|
||||
try:
|
||||
_LOGGER.debug("开始下载图片: %s", image_url)
|
||||
@@ -204,9 +221,7 @@ class TencentCloudClient:
|
||||
{"url": image_url}
|
||||
)
|
||||
|
||||
session = requests.Session()
|
||||
session.mount('http://', requests.adapters.HTTPAdapter(max_retries=2))
|
||||
session.mount('https://', requests.adapters.HTTPAdapter(max_retries=2))
|
||||
session = self._session
|
||||
|
||||
headers = {
|
||||
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
|
||||
@@ -251,7 +266,7 @@ class TencentCloudClient:
|
||||
{"url": image_url, "error": str(ex)}
|
||||
)
|
||||
|
||||
def _read_local_image_as_base64(self, image_path: str) -> str:
|
||||
def _read_local_image_as_base64_uncached(self, image_path: str) -> str:
|
||||
"""读取本地图片并转换为base64编码"""
|
||||
try:
|
||||
if not os.path.exists(image_path):
|
||||
@@ -717,7 +732,23 @@ class TencentCloudClient:
|
||||
except Exception as ex:
|
||||
duration = time.time() - start_time
|
||||
self._log_error("创建人员", request_id, ex, duration)
|
||||
raise
|
||||
|
||||
error_code = None
|
||||
error_message = str(ex)
|
||||
|
||||
if isinstance(ex, TencentCloudSDKException):
|
||||
error_code = getattr(ex, "code", None)
|
||||
error_message = getattr(ex, "message", str(ex))
|
||||
|
||||
return {
|
||||
"success": False,
|
||||
"person_id": person_id,
|
||||
"face_id": "",
|
||||
"face_rect": "",
|
||||
"error": str(ex),
|
||||
"error_code": error_code,
|
||||
"error_message": error_message
|
||||
}
|
||||
|
||||
return self._execute_with_retry(api_call, "创建人员")
|
||||
|
||||
@@ -744,7 +775,21 @@ class TencentCloudClient:
|
||||
except Exception as ex:
|
||||
duration = time.time() - start_time
|
||||
self._log_error("删除人员", request_id, ex, duration)
|
||||
raise
|
||||
|
||||
error_code = None
|
||||
error_message = str(ex)
|
||||
|
||||
if isinstance(ex, TencentCloudSDKException):
|
||||
error_code = getattr(ex, "code", None)
|
||||
error_message = getattr(ex, "message", str(ex))
|
||||
|
||||
return {
|
||||
"success": False,
|
||||
"person_id": person_id,
|
||||
"error": str(ex),
|
||||
"error_code": error_code,
|
||||
"error_message": error_message
|
||||
}
|
||||
|
||||
return self._execute_with_retry(api_call, "删除人员")
|
||||
|
||||
@@ -793,7 +838,23 @@ class TencentCloudClient:
|
||||
except Exception as ex:
|
||||
duration = time.time() - start_time
|
||||
self._log_error("注册人脸", request_id, ex, duration)
|
||||
raise
|
||||
|
||||
error_code = None
|
||||
error_message = str(ex)
|
||||
|
||||
if isinstance(ex, TencentCloudSDKException):
|
||||
error_code = getattr(ex, "code", None)
|
||||
error_message = getattr(ex, "message", str(ex))
|
||||
|
||||
return {
|
||||
"success": False,
|
||||
"person_id": person_id,
|
||||
"face_ids": [],
|
||||
"face_rects": [],
|
||||
"error": str(ex),
|
||||
"error_code": error_code,
|
||||
"error_message": error_message
|
||||
}
|
||||
|
||||
return self._execute_with_retry(api_call, "注册人脸")
|
||||
|
||||
@@ -823,7 +884,22 @@ class TencentCloudClient:
|
||||
except Exception as ex:
|
||||
duration = time.time() - start_time
|
||||
self._log_error("删除人脸", request_id, ex, duration)
|
||||
raise
|
||||
|
||||
error_code = None
|
||||
error_message = str(ex)
|
||||
|
||||
if isinstance(ex, TencentCloudSDKException):
|
||||
error_code = getattr(ex, "code", None)
|
||||
error_message = getattr(ex, "message", str(ex))
|
||||
|
||||
return {
|
||||
"success": False,
|
||||
"person_id": person_id,
|
||||
"face_id": face_id,
|
||||
"error": str(ex),
|
||||
"error_code": error_code,
|
||||
"error_message": error_message
|
||||
}
|
||||
|
||||
return self._execute_with_retry(api_call, "删除人脸")
|
||||
|
||||
@@ -847,6 +923,7 @@ class TencentCloudClient:
|
||||
self._log_response("获取人员库信息", request_id, resp, duration)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"group_name": getattr(resp, "GroupName", ""),
|
||||
"group_tag": getattr(resp, "GroupTag", ""),
|
||||
"face_model_version": getattr(resp, "FaceModelVersion", ""),
|
||||
@@ -855,7 +932,24 @@ class TencentCloudClient:
|
||||
except Exception as ex:
|
||||
duration = time.time() - start_time
|
||||
self._log_error("获取人员库信息", request_id, ex, duration)
|
||||
raise
|
||||
|
||||
error_code = None
|
||||
error_message = str(ex)
|
||||
|
||||
if isinstance(ex, TencentCloudSDKException):
|
||||
error_code = getattr(ex, "code", None)
|
||||
error_message = getattr(ex, "message", str(ex))
|
||||
|
||||
return {
|
||||
"success": False,
|
||||
"group_name": "",
|
||||
"group_tag": "",
|
||||
"face_model_version": "",
|
||||
"creation_timestamp": 0,
|
||||
"error": str(ex),
|
||||
"error_code": error_code,
|
||||
"error_message": error_message
|
||||
}
|
||||
|
||||
return self._execute_with_retry(api_call, "获取人员库信息")
|
||||
|
||||
@@ -884,7 +978,7 @@ class TencentCloudClient:
|
||||
_LOGGER.error("API连接测试失败: %s", self._sanitize_error(str(ex)))
|
||||
return False
|
||||
|
||||
def get_person_list(self, group_id: str, limit: int = 100, offset: int = 0) -> List[Dict[str, Any]]:
|
||||
def get_person_list(self, group_id: str, limit: int = 100, offset: int = 0) -> Dict[str, Any]:
|
||||
"""获取人员列表"""
|
||||
_LOGGER.info("开始获取人员列表: group_id=%s, limit=%d, offset=%d", group_id, limit, offset)
|
||||
|
||||
@@ -916,26 +1010,55 @@ class TencentCloudClient:
|
||||
"gender": getattr(person_info, "Gender", 0),
|
||||
"face_ids": getattr(person_info, "FaceIds", []),
|
||||
})
|
||||
return persons
|
||||
return {
|
||||
"success": True,
|
||||
"persons": persons,
|
||||
"error": None,
|
||||
"error_code": None,
|
||||
"error_message": None
|
||||
}
|
||||
except Exception as ex:
|
||||
duration = time.time() - start_time
|
||||
self._log_error("获取人员列表", request_id, ex, duration)
|
||||
raise
|
||||
|
||||
error_code = None
|
||||
error_message = str(ex)
|
||||
|
||||
if isinstance(ex, TencentCloudSDKException):
|
||||
error_code = getattr(ex, "code", None)
|
||||
error_message = getattr(ex, "message", str(ex))
|
||||
|
||||
return {
|
||||
"success": False,
|
||||
"persons": [],
|
||||
"error": str(ex),
|
||||
"error_code": error_code,
|
||||
"error_message": error_message
|
||||
}
|
||||
|
||||
return self._execute_with_retry(api_call, "获取人员列表")
|
||||
|
||||
def get_person_list_all(self, group_id: str) -> List[Dict[str, Any]]:
|
||||
def get_person_list_all(self, group_id: str) -> Dict[str, Any]:
|
||||
"""获取人员列表(自动分页获取全部)"""
|
||||
all_persons = []
|
||||
offset = 0
|
||||
limit = 100
|
||||
while True:
|
||||
persons = self.get_person_list(group_id, limit=limit, offset=offset)
|
||||
result = self.get_person_list(group_id, limit=limit, offset=offset)
|
||||
if not result.get("success", False):
|
||||
return result
|
||||
persons = result.get("persons", [])
|
||||
all_persons.extend(persons)
|
||||
if len(persons) < limit:
|
||||
break
|
||||
offset += limit
|
||||
return all_persons
|
||||
return {
|
||||
"success": True,
|
||||
"persons": all_persons,
|
||||
"error": None,
|
||||
"error_code": None,
|
||||
"error_message": None
|
||||
}
|
||||
|
||||
def _execute_with_retry(self, api_call: Callable, operation_name: str) -> Any:
|
||||
"""执行带重试的API调用,max_attempts 为总尝试次数(1次初始 + N次重试)"""
|
||||
|
||||
Reference in New Issue
Block a user