feat: 升级至v2.0.0,新增人员/人脸管理服务与摄像头支持
- 新增 create_person、delete_person、create_face、delete_face 服务 - 支持通过摄像头实体获取图片 (camera_entity_id 参数) - 添加配置选项支持,可通过 Options Flow 修改配置 - 传感器采用 CoordinatorEntity 模式,优化数据更新机制 - 改进重试逻辑与错误处理 - 重构代码结构,清理冗余代码
This commit is contained in:
+20
-46
@@ -4,12 +4,9 @@
|
||||
import logging
|
||||
from typing import Dict, Any
|
||||
|
||||
from homeassistant.components.http import StaticPathConfig
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant, ServiceCall
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from .const import (
|
||||
DOMAIN,
|
||||
@@ -17,7 +14,8 @@ from .const import (
|
||||
CONF_SECRET_ID,
|
||||
CONF_SECRET_KEY,
|
||||
CONF_REGION,
|
||||
CONF_PERSON_GROUP_ID
|
||||
CONF_PERSON_GROUP_ID,
|
||||
get_entry_value,
|
||||
)
|
||||
from .tencent_cloud_client import TencentCloudClient
|
||||
from .face_recognition import FaceRecognition
|
||||
@@ -25,30 +23,24 @@ from .services import async_setup_services, async_unload_services
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
# 添加传感器平台
|
||||
PLATFORMS.append("sensor")
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""设置配置项"""
|
||||
_LOGGER.info("设置腾讯云人脸识别插件")
|
||||
|
||||
# 获取配置
|
||||
|
||||
secret_id = entry.data.get(CONF_SECRET_ID)
|
||||
secret_key = entry.data.get(CONF_SECRET_KEY)
|
||||
region = entry.data.get(CONF_REGION, "ap-shanghai")
|
||||
person_group_id = entry.data.get(CONF_PERSON_GROUP_ID, "Hass")
|
||||
|
||||
# 创建腾讯云客户端
|
||||
region = get_entry_value(entry, CONF_REGION, "ap-shanghai")
|
||||
person_group_id = get_entry_value(entry, CONF_PERSON_GROUP_ID, "Hass")
|
||||
|
||||
try:
|
||||
client = TencentCloudClient(secret_id, secret_key, region)
|
||||
except Exception as ex:
|
||||
_LOGGER.error("创建腾讯云客户端失败: %s", ex)
|
||||
_LOGGER.error("创建腾讯云客户端失败")
|
||||
raise ConfigEntryNotReady(f"创建腾讯云客户端失败: {ex}")
|
||||
|
||||
# 创建人脸识别实例
|
||||
|
||||
face_recognition = FaceRecognition(hass, client)
|
||||
|
||||
# 存储配置和实例
|
||||
|
||||
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = {
|
||||
"entry": entry,
|
||||
"client": client,
|
||||
@@ -58,46 +50,28 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"region": region,
|
||||
"person_group_id": person_group_id
|
||||
}
|
||||
|
||||
# 设置服务
|
||||
|
||||
await async_setup_services(hass)
|
||||
|
||||
# 设置平台
|
||||
# 转发配置项到平台
|
||||
for platform in PLATFORMS:
|
||||
hass.async_create_task(
|
||||
hass.config_entries.async_forward_entry_setups(entry, [platform])
|
||||
)
|
||||
|
||||
# 不再注册前端面板,改为使用配置流管理
|
||||
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""卸载配置项"""
|
||||
_LOGGER.info("卸载腾讯云人脸识别插件")
|
||||
|
||||
# 注销自定义面板
|
||||
# 注意:在新版本的 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
|
||||
|
||||
|
||||
unload_ok = await hass.config_entries.async_forward_entry_unload(entry, "sensor")
|
||||
|
||||
if unload_ok:
|
||||
# 卸载服务
|
||||
await async_unload_services(hass)
|
||||
|
||||
# 移除数据
|
||||
|
||||
hass.data[DOMAIN].pop(entry.entry_id)
|
||||
|
||||
|
||||
return unload_ok
|
||||
|
||||
|
||||
async def async_remove_entry(hass: HomeAssistant, entry: ConfigEntry) -> None:
|
||||
"""移除配置项"""
|
||||
_LOGGER.info("移除腾讯云人脸识别插件配置")
|
||||
_LOGGER.info("移除腾讯云人脸识别插件配置")
|
||||
|
||||
+99
-114
@@ -18,8 +18,6 @@ from .const import (
|
||||
CONF_REGION,
|
||||
CONF_PERSON_GROUP_ID,
|
||||
DEFAULT_REGION,
|
||||
ERROR_INVALID_CONFIG,
|
||||
ERROR_API_ERROR,
|
||||
)
|
||||
from .errors import (
|
||||
validate_config,
|
||||
@@ -33,20 +31,6 @@ from .tencent_cloud_client import TencentCloudClient
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
# 腾讯云区域列表
|
||||
AVAILABLE_REGIONS = [
|
||||
"ap-beijing", "ap-shanghai", "ap-guangzhou", "ap-chengdu", "ap-chongqing", "ap-nanjing", "ap-hangzhou"
|
||||
]
|
||||
|
||||
# Secret ID 格式验证
|
||||
SECRET_ID_REGEX = re.compile(r'^AKID[a-zA-Z0-9\-]{36,}$')
|
||||
|
||||
# Secret Key 格式验证
|
||||
SECRET_KEY_REGEX = re.compile(r'^[a-zA-Z0-9+/]{20,}$')
|
||||
|
||||
# 人员库ID格式验证
|
||||
PERSON_GROUP_ID_REGEX = re.compile(r'^[a-zA-Z0-9\-_]{1,64}$')
|
||||
|
||||
|
||||
class TencentFaceRecognitionConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
"""腾讯云人脸识别配置流程"""
|
||||
@@ -54,6 +38,15 @@ class TencentFaceRecognitionConfigFlow(config_entries.ConfigFlow, domain=DOMAIN)
|
||||
VERSION = 1
|
||||
CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL
|
||||
|
||||
AVAILABLE_REGIONS = [
|
||||
"ap-beijing", "ap-shanghai", "ap-guangzhou", "ap-chengdu",
|
||||
"ap-chongqing", "ap-nanjing", "ap-hangzhou"
|
||||
]
|
||||
|
||||
SECRET_ID_REGEX = re.compile(r'^AKID[a-zA-Z0-9\-]{36,}$')
|
||||
SECRET_KEY_REGEX = re.compile(r'^[a-zA-Z0-9+/]{20,}$')
|
||||
PERSON_GROUP_ID_REGEX = re.compile(r'^[a-zA-Z0-9\-_]{1,64}$')
|
||||
|
||||
def __init__(self):
|
||||
"""初始化配置流程"""
|
||||
self._client = None
|
||||
@@ -64,38 +57,32 @@ class TencentFaceRecognitionConfigFlow(config_entries.ConfigFlow, domain=DOMAIN)
|
||||
errors = {}
|
||||
|
||||
if user_input is not None:
|
||||
# 验证配置
|
||||
try:
|
||||
# 验证输入格式
|
||||
self._validate_input_format(user_input, errors)
|
||||
|
||||
|
||||
if errors:
|
||||
return self._show_user_form(errors)
|
||||
|
||||
# 基本配置验证
|
||||
|
||||
validate_config(user_input)
|
||||
|
||||
# 尝试创建腾讯云客户端以验证凭据
|
||||
|
||||
validation_result = await self._validate_tencent_cloud_credentials(user_input)
|
||||
|
||||
|
||||
if not validation_result["success"]:
|
||||
errors = validation_result["errors"]
|
||||
return self._show_user_form(errors)
|
||||
|
||||
# 保存配置并创建配置项
|
||||
|
||||
return self.async_create_entry(
|
||||
title=user_input.get(CONF_NAME, "腾讯云人脸识别"),
|
||||
data=user_input
|
||||
)
|
||||
|
||||
|
||||
except InvalidConfigError as ex:
|
||||
_LOGGER.error("配置验证失败: %s", ex)
|
||||
errors["base"] = str(ex)
|
||||
except Exception as ex:
|
||||
_LOGGER.error("配置验证失败: %s", ex, exc_info=True)
|
||||
_LOGGER.error("配置验证失败: %s", ex)
|
||||
errors["base"] = f"配置验证失败: {str(ex)}"
|
||||
|
||||
# 显示配置表单
|
||||
return self._show_user_form(errors)
|
||||
|
||||
def _show_user_form(self, errors: Dict[str, str]) -> dict:
|
||||
@@ -107,7 +94,7 @@ class TencentFaceRecognitionConfigFlow(config_entries.ConfigFlow, domain=DOMAIN)
|
||||
vol.Required(CONF_SECRET_KEY): vol.All(str, vol.Length(min=1)),
|
||||
vol.Optional(CONF_REGION, default=DEFAULT_REGION): selector({
|
||||
"select": {
|
||||
"options": [{"label": region, "value": region} for region in AVAILABLE_REGIONS],
|
||||
"options": [{"label": region, "value": region} for region in self.AVAILABLE_REGIONS],
|
||||
"mode": "dropdown"
|
||||
}
|
||||
}),
|
||||
@@ -128,47 +115,42 @@ class TencentFaceRecognitionConfigFlow(config_entries.ConfigFlow, domain=DOMAIN)
|
||||
|
||||
def _validate_input_format(self, user_input: Dict[str, Any], errors: Dict[str, str]) -> None:
|
||||
"""验证输入格式"""
|
||||
# 验证Secret ID格式
|
||||
secret_id = user_input.get(CONF_SECRET_ID, "")
|
||||
if not SECRET_ID_REGEX.match(secret_id):
|
||||
if not self.SECRET_ID_REGEX.match(secret_id):
|
||||
errors[CONF_SECRET_ID] = "Secret ID格式不正确,应以AKID开头,后跟36个以上字符"
|
||||
|
||||
# 验证Secret Key格式
|
||||
|
||||
secret_key = user_input.get(CONF_SECRET_KEY, "")
|
||||
if not SECRET_KEY_REGEX.match(secret_key):
|
||||
if not self.SECRET_KEY_REGEX.match(secret_key):
|
||||
errors[CONF_SECRET_KEY] = "Secret Key格式不正确,应为20个以上字符"
|
||||
|
||||
# 验证区域
|
||||
|
||||
region = user_input.get(CONF_REGION, DEFAULT_REGION)
|
||||
if region not in AVAILABLE_REGIONS:
|
||||
errors[CONF_REGION] = f"无效的区域,请从列表中选择有效的区域"
|
||||
|
||||
# 验证人员库ID格式
|
||||
if region not in self.AVAILABLE_REGIONS:
|
||||
errors[CONF_REGION] = "无效的区域,请从列表中选择有效的区域"
|
||||
|
||||
person_group_id = user_input.get(CONF_PERSON_GROUP_ID, "Hass")
|
||||
if not PERSON_GROUP_ID_REGEX.match(person_group_id):
|
||||
if not self.PERSON_GROUP_ID_REGEX.match(person_group_id):
|
||||
errors[CONF_PERSON_GROUP_ID] = "人员库ID只能包含字母、数字、连字符和下划线,长度1-64个字符"
|
||||
|
||||
# 验证名称
|
||||
|
||||
name = user_input.get(CONF_NAME, "腾讯云人脸识别")
|
||||
if len(name) > 50:
|
||||
errors[CONF_NAME] = "名称长度不能超过50个字符"
|
||||
|
||||
async def _validate_tencent_cloud_credentials(self, user_input: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""验证腾讯云凭据"""
|
||||
"""验证腾讯云凭据 - 通过真实API调用验证"""
|
||||
try:
|
||||
_LOGGER.info("开始验证腾讯云凭据")
|
||||
|
||||
# 创建腾讯云客户端
|
||||
self._client = TencentCloudClient(
|
||||
|
||||
client = TencentCloudClient(
|
||||
user_input[CONF_SECRET_ID],
|
||||
user_input[CONF_SECRET_KEY],
|
||||
user_input.get(CONF_REGION, DEFAULT_REGION)
|
||||
)
|
||||
|
||||
_LOGGER.info("腾讯云客户端创建成功,凭据验证通过")
|
||||
|
||||
|
||||
await self.hass.async_add_executor_job(client.verify_credentials)
|
||||
|
||||
_LOGGER.info("腾讯云凭据验证通过")
|
||||
return {"success": True}
|
||||
|
||||
|
||||
except AuthenticationError as ex:
|
||||
_LOGGER.error("腾讯云认证失败: %s", ex)
|
||||
return {
|
||||
@@ -197,7 +179,7 @@ class TencentFaceRecognitionConfigFlow(config_entries.ConfigFlow, domain=DOMAIN)
|
||||
}
|
||||
}
|
||||
except Exception as ex:
|
||||
_LOGGER.error("腾讯云凭据验证失败: %s", ex, exc_info=True)
|
||||
_LOGGER.error("腾讯云凭据验证失败: %s", ex)
|
||||
return {
|
||||
"success": False,
|
||||
"errors": {
|
||||
@@ -215,9 +197,10 @@ class TencentFaceRecognitionConfigFlow(config_entries.ConfigFlow, domain=DOMAIN)
|
||||
class TencentFaceRecognitionOptionsFlow(config_entries.OptionsFlow):
|
||||
"""腾讯云人脸识别选项流程"""
|
||||
|
||||
AVAILABLE_REGIONS = TencentFaceRecognitionConfigFlow.AVAILABLE_REGIONS
|
||||
|
||||
def __init__(self, config_entry):
|
||||
"""初始化选项流程"""
|
||||
# 存储需要的数据而不是整个 config_entry 实例
|
||||
self._entry_id = config_entry.entry_id
|
||||
self._title = config_entry.title
|
||||
self._data = dict(config_entry.data)
|
||||
@@ -227,17 +210,16 @@ class TencentFaceRecognitionOptionsFlow(config_entries.OptionsFlow):
|
||||
"""管理选项流程"""
|
||||
if user_input is not None:
|
||||
action = user_input.get("action")
|
||||
|
||||
|
||||
if action == "config":
|
||||
return await self.async_step_config()
|
||||
elif action == "test_connection":
|
||||
return await self.async_step_test_connection()
|
||||
elif action == "reauth":
|
||||
return await self.async_step_reauth()
|
||||
|
||||
|
||||
return self.async_create_entry(title="", data=user_input)
|
||||
|
||||
# 显示管理菜单
|
||||
return self.async_show_form(
|
||||
step_id="init",
|
||||
data_schema=vol.Schema({
|
||||
@@ -259,28 +241,41 @@ class TencentFaceRecognitionOptionsFlow(config_entries.OptionsFlow):
|
||||
async def async_step_config(self, user_input=None):
|
||||
"""配置设置步骤"""
|
||||
errors = {}
|
||||
|
||||
|
||||
if user_input is not None:
|
||||
# 验证区域
|
||||
region = user_input.get(CONF_REGION)
|
||||
if region not in AVAILABLE_REGIONS:
|
||||
errors[CONF_REGION] = f"无效的区域,请从列表中选择有效的区域"
|
||||
|
||||
if region not in self.AVAILABLE_REGIONS:
|
||||
errors[CONF_REGION] = "无效的区域,请从列表中选择有效的区域"
|
||||
|
||||
person_group_id = user_input.get(CONF_PERSON_GROUP_ID, "")
|
||||
if person_group_id and not re.match(r'^[a-zA-Z0-9\-_]{1,64}$', person_group_id):
|
||||
errors[CONF_PERSON_GROUP_ID] = "人员库ID只能包含字母、数字、连字符和下划线,长度1-64个字符"
|
||||
|
||||
if not errors:
|
||||
# 验证新配置是否有效
|
||||
try:
|
||||
# 创建临时客户端进行验证
|
||||
temp_client = TencentCloudClient(
|
||||
client = TencentCloudClient(
|
||||
self._data.get(CONF_SECRET_ID),
|
||||
self._data.get(CONF_SECRET_KEY),
|
||||
region
|
||||
)
|
||||
|
||||
|
||||
await self.hass.async_add_executor_job(client.verify_credentials)
|
||||
|
||||
_LOGGER.info("配置验证成功")
|
||||
|
||||
# 保存配置
|
||||
return self.async_create_entry(title="", data=user_input)
|
||||
|
||||
|
||||
new_data = dict(self._data)
|
||||
new_data[CONF_REGION] = region
|
||||
if person_group_id:
|
||||
new_data[CONF_PERSON_GROUP_ID] = person_group_id
|
||||
|
||||
entry = self.hass.config_entries.async_get_entry(self._entry_id)
|
||||
self.hass.config_entries.async_update_entry(entry, data=new_data)
|
||||
|
||||
return self.async_create_entry(title="", data={
|
||||
CONF_REGION: region,
|
||||
CONF_PERSON_GROUP_ID: person_group_id,
|
||||
})
|
||||
|
||||
except AuthenticationError as ex:
|
||||
_LOGGER.error("腾讯云认证失败: %s", ex)
|
||||
errors[CONF_REGION] = "认证失败,请检查区域设置"
|
||||
@@ -290,7 +285,7 @@ class TencentFaceRecognitionOptionsFlow(config_entries.OptionsFlow):
|
||||
errors[CONF_REGION] = "网络错误,请检查区域设置"
|
||||
errors["base"] = "连接腾讯云失败,请检查网络连接和区域设置"
|
||||
except Exception as ex:
|
||||
_LOGGER.error("配置验证失败: %s", ex, exc_info=True)
|
||||
_LOGGER.error("配置验证失败: %s", ex)
|
||||
errors["base"] = f"配置验证失败: {str(ex)}"
|
||||
|
||||
options = {
|
||||
@@ -299,7 +294,7 @@ class TencentFaceRecognitionOptionsFlow(config_entries.OptionsFlow):
|
||||
default=self._options.get(CONF_REGION, self._data.get(CONF_REGION, DEFAULT_REGION))
|
||||
): selector({
|
||||
"select": {
|
||||
"options": [{"label": region, "value": region} for region in AVAILABLE_REGIONS],
|
||||
"options": [{"label": region, "value": region} for region in self.AVAILABLE_REGIONS],
|
||||
"mode": "dropdown"
|
||||
}
|
||||
}),
|
||||
@@ -314,36 +309,31 @@ class TencentFaceRecognitionOptionsFlow(config_entries.OptionsFlow):
|
||||
data_schema=vol.Schema(options),
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
|
||||
async def async_step_test_connection(self, user_input=None):
|
||||
"""测试连接步骤"""
|
||||
errors = {}
|
||||
connection_status = "未知"
|
||||
|
||||
|
||||
if user_input is None:
|
||||
# 尝试测试连接
|
||||
try:
|
||||
# 使用当前配置创建客户端
|
||||
client = TencentCloudClient(
|
||||
self._data.get(CONF_SECRET_ID),
|
||||
self._data.get(CONF_SECRET_KEY),
|
||||
self._data.get(CONF_REGION, DEFAULT_REGION)
|
||||
)
|
||||
|
||||
# 使用 GetGroupInfo 测试连接
|
||||
group_id = self._data.get(CONF_PERSON_GROUP_ID, "Hass")
|
||||
|
||||
|
||||
success = await self.hass.async_add_executor_job(
|
||||
client._test_api_connection,
|
||||
group_id
|
||||
self._data.get(CONF_PERSON_GROUP_ID, "Hass")
|
||||
)
|
||||
|
||||
|
||||
if not success:
|
||||
raise Exception("连接测试失败,请检查人员库ID是否正确")
|
||||
|
||||
|
||||
connection_status = "连接成功"
|
||||
_LOGGER.info("连接测试成功")
|
||||
|
||||
|
||||
except AuthenticationError as ex:
|
||||
connection_status = f"认证失败: {str(ex)}"
|
||||
errors["base"] = "认证失败,请检查Secret ID和Secret Key"
|
||||
@@ -355,8 +345,8 @@ class TencentFaceRecognitionOptionsFlow(config_entries.OptionsFlow):
|
||||
except Exception as ex:
|
||||
connection_status = f"测试失败: {str(ex)}"
|
||||
errors["base"] = f"连接测试失败: {str(ex)}"
|
||||
_LOGGER.error("连接测试失败: %s", ex, exc_info=True)
|
||||
|
||||
_LOGGER.error("连接测试失败: %s", ex)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="test_connection",
|
||||
data_schema=vol.Schema({}),
|
||||
@@ -365,18 +355,16 @@ class TencentFaceRecognitionOptionsFlow(config_entries.OptionsFlow):
|
||||
"connection_status": connection_status
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
async def async_step_reauth(self, user_input=None):
|
||||
"""重新认证步骤"""
|
||||
errors = {}
|
||||
|
||||
|
||||
if user_input is not None:
|
||||
# 验证新凭据
|
||||
try:
|
||||
# 验证输入格式
|
||||
temp_errors = {}
|
||||
self._validate_input_format(user_input, temp_errors)
|
||||
|
||||
|
||||
if temp_errors:
|
||||
errors = temp_errors
|
||||
return self.async_show_form(
|
||||
@@ -387,28 +375,27 @@ class TencentFaceRecognitionOptionsFlow(config_entries.OptionsFlow):
|
||||
}),
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
# 创建腾讯云客户端验证新凭据
|
||||
|
||||
new_client = TencentCloudClient(
|
||||
user_input[CONF_SECRET_ID],
|
||||
user_input[CONF_SECRET_KEY],
|
||||
self._data.get(CONF_REGION, DEFAULT_REGION)
|
||||
)
|
||||
|
||||
|
||||
await self.hass.async_add_executor_job(new_client.verify_credentials)
|
||||
|
||||
_LOGGER.info("重新认证成功")
|
||||
|
||||
# 更新配置
|
||||
|
||||
new_data = dict(self._data)
|
||||
new_data.update(user_input)
|
||||
|
||||
# 重新创建配置项
|
||||
self.hass.config_entries.async_update_entry(
|
||||
self.hass.config_entries.async_get_entry(self._entry_id),
|
||||
data=new_data
|
||||
)
|
||||
|
||||
|
||||
entry = self.hass.config_entries.async_get_entry(self._entry_id)
|
||||
self.hass.config_entries.async_update_entry(entry, data=new_data)
|
||||
|
||||
await self.hass.config_entries.async_reload(entry.entry_id)
|
||||
|
||||
return self.async_create_entry(title="", data={})
|
||||
|
||||
|
||||
except AuthenticationError as ex:
|
||||
_LOGGER.error("重新认证失败: %s", ex)
|
||||
errors["base"] = "认证失败,请检查Secret ID和Secret Key"
|
||||
@@ -416,9 +403,9 @@ class TencentFaceRecognitionOptionsFlow(config_entries.OptionsFlow):
|
||||
_LOGGER.error("重新认证网络错误: %s", ex)
|
||||
errors["base"] = "网络错误,请检查网络连接和区域设置"
|
||||
except Exception as ex:
|
||||
_LOGGER.error("重新认证失败: %s", ex, exc_info=True)
|
||||
_LOGGER.error("重新认证失败: %s", ex)
|
||||
errors["base"] = f"重新认证失败: {str(ex)}"
|
||||
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="reauth",
|
||||
data_schema=vol.Schema({
|
||||
@@ -427,19 +414,17 @@ class TencentFaceRecognitionOptionsFlow(config_entries.OptionsFlow):
|
||||
}),
|
||||
errors=errors,
|
||||
description_placeholders={
|
||||
"current_secret_id": self._data.get(CONF_SECRET_ID, ""),
|
||||
"current_secret_id": self._data.get(CONF_SECRET_ID, "")[:8] + "***" if self._data.get(CONF_SECRET_ID) else "",
|
||||
"current_region": self._data.get(CONF_REGION, DEFAULT_REGION)
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def _validate_input_format(self, user_input: Dict[str, Any], errors: Dict[str, str]) -> None:
|
||||
"""验证输入格式"""
|
||||
# 验证Secret ID格式
|
||||
secret_id = user_input.get(CONF_SECRET_ID, "")
|
||||
if not SECRET_ID_REGEX.match(secret_id):
|
||||
if not TencentFaceRecognitionConfigFlow.SECRET_ID_REGEX.match(secret_id):
|
||||
errors[CONF_SECRET_ID] = "Secret ID格式不正确,应以AKID开头,后跟36个以上字符"
|
||||
|
||||
# 验证Secret Key格式
|
||||
|
||||
secret_key = user_input.get(CONF_SECRET_KEY, "")
|
||||
if not SECRET_KEY_REGEX.match(secret_key):
|
||||
errors[CONF_SECRET_KEY] = "Secret Key格式不正确,应为20个以上字符"
|
||||
if not TencentFaceRecognitionConfigFlow.SECRET_KEY_REGEX.match(secret_key):
|
||||
errors[CONF_SECRET_KEY] = "Secret Key格式不正确,应为20个以上字符"
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
"""腾讯云人脸识别插件常量定义"""
|
||||
|
||||
DOMAIN = "tencent_face_recognition"
|
||||
PLATFORMS = []
|
||||
PLATFORMS = ["sensor"]
|
||||
|
||||
# 配置项键
|
||||
CONF_SECRET_ID = "secret_id"
|
||||
CONF_SECRET_KEY = "secret_key"
|
||||
CONF_REGION = "region"
|
||||
CONF_PERSON_GROUP_ID = "person_group_id"
|
||||
|
||||
# 默认值
|
||||
DEFAULT_REGION = "ap-shanghai"
|
||||
DEFAULT_PERSON_GROUP_ID = "Hass"
|
||||
DEFAULT_MAX_FACE_NUM = 1
|
||||
@@ -19,37 +17,46 @@ DEFAULT_QUALITY_CONTROL = 1
|
||||
DEFAULT_NEED_ROTATE_CHECK = 1
|
||||
DEFAULT_FACE_MATCH_THRESHOLD = 60.0
|
||||
|
||||
# 服务
|
||||
SERVICE_FACE_SEARCH = "face_search"
|
||||
SERVICE_DETECT_FACE = "detect_face"
|
||||
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_PATH = "image_path"
|
||||
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_MIN_FACE_SIZE = "min_face_size"
|
||||
ATTR_MAX_USER_NUM = "max_user_num"
|
||||
ATTR_QUALITY_CONTROL = "quality_control"
|
||||
ATTR_NEED_ROTATE_CHECK = "need_rotate_check"
|
||||
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_API_ERROR = "API调用错误"
|
||||
ERROR_IMAGE_NOT_FOUND = "图片未找到"
|
||||
ERROR_FACE_NOT_DETECTED = "未检测到人脸"
|
||||
|
||||
# 状态
|
||||
STATE_CONNECTED = "已连接"
|
||||
STATE_DISCONNECTED = "未连接"
|
||||
STATE_CONNECTING = "连接中"
|
||||
STATE_ERROR = "连接错误"
|
||||
STATE_PARTIALLY_CONNECTED = "部分连接"
|
||||
|
||||
# 日志级别
|
||||
LOG_LEVEL_DEBUG = "DEBUG"
|
||||
LOG_LEVEL_INFO = "INFO"
|
||||
LOG_LEVEL_WARNING = "WARNING"
|
||||
LOG_LEVEL_ERROR = "ERROR"
|
||||
|
||||
def get_entry_value(entry, key, default=None):
|
||||
"""Get a config value from entry.options with fallback to entry.data."""
|
||||
return entry.options.get(key, entry.data.get(key, default))
|
||||
|
||||
@@ -5,19 +5,18 @@ from typing import Dict, Any, Optional
|
||||
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
|
||||
from .const import CONF_SECRET_ID, CONF_SECRET_KEY, CONF_REGION, CONF_PERSON_GROUP_ID
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
# 错误消息本地化
|
||||
ERROR_MESSAGES = {
|
||||
"zh_CN": {
|
||||
# 配置错误
|
||||
"missing_config": "缺少必需的配置项: {key}",
|
||||
"invalid_secret_id": "Secret ID 必须是非空字符串,且应以 AKID 开头",
|
||||
"invalid_secret_key": "Secret Key 必须是非空字符串",
|
||||
"invalid_region": "区域必须是非空字符串,且为有效的腾讯云区域",
|
||||
"invalid_person_group_id": "人员库ID必须是非空字符串,只能包含字母、数字、连字符和下划线",
|
||||
|
||||
# 图片错误
|
||||
|
||||
"image_not_found": "图片未找到: {path}",
|
||||
"image_too_large": "图片文件过大: {path}, 大小: {size} 字节,最大支持: {max_size}",
|
||||
"image_format_not_supported": "不支持的图片格式: {format}, 支持的格式: {supported_formats}",
|
||||
@@ -26,43 +25,35 @@ ERROR_MESSAGES = {
|
||||
"image_download_error": "下载图片失败: {url}, 错误: {error}",
|
||||
"invalid_image_url": "无效的图片URL: {url}",
|
||||
"permission_denied": "没有权限读取图片文件: {path}",
|
||||
|
||||
# 人脸检测错误
|
||||
|
||||
"face_not_detected": "未检测到人脸,请确保图片中包含清晰的人脸",
|
||||
"face_too_small": "人脸太小,请使用包含更大人脸的图片",
|
||||
"face_quality_poor": "人脸质量较差,请使用更清晰的图片",
|
||||
|
||||
# API错误
|
||||
|
||||
"api_error": "API调用失败: {code} - {message}",
|
||||
"person_not_found": "人员未找到: {person_id}",
|
||||
"group_not_found": "人员库未找到: {group_id}",
|
||||
"face_not_found": "人脸未找到: {face_id}",
|
||||
|
||||
# 网络错误
|
||||
|
||||
"network_error": "网络连接错误,请检查网络连接",
|
||||
"timeout_error": "请求超时,请稍后重试",
|
||||
|
||||
# 认证错误
|
||||
|
||||
"auth_failure": "认证失败,请检查Secret ID和Secret Key是否正确",
|
||||
"auth_expired": "认证已过期,请更新凭证",
|
||||
|
||||
# 限制错误
|
||||
|
||||
"rate_limit_exceeded": "API调用频率超限,请稍后重试",
|
||||
"quota_exceeded": "API调用配额已用完,请等待配额重置或升级套餐",
|
||||
|
||||
# 通用错误
|
||||
|
||||
"unknown_error": "未知错误: {message}",
|
||||
"invalid_parameter": "无效参数: {parameter}",
|
||||
},
|
||||
"en_US": {
|
||||
# Configuration errors
|
||||
"missing_config": "Missing required configuration: {key}",
|
||||
"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_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",
|
||||
|
||||
# Image errors
|
||||
|
||||
"image_not_found": "Image not found: {path}",
|
||||
"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}",
|
||||
@@ -71,37 +62,30 @@ ERROR_MESSAGES = {
|
||||
"image_download_error": "Image download failed: {url}, error: {error}",
|
||||
"invalid_image_url": "Invalid image URL: {url}",
|
||||
"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_too_small": "Face too small, please use an image with larger faces",
|
||||
"face_quality_poor": "Poor face quality, please use a clearer image",
|
||||
|
||||
# API errors
|
||||
|
||||
"api_error": "API call failed: {code} - {message}",
|
||||
"person_not_found": "Person not found: {person_id}",
|
||||
"group_not_found": "Person group not found: {group_id}",
|
||||
"face_not_found": "Face not found: {face_id}",
|
||||
|
||||
# Network errors
|
||||
|
||||
"network_error": "Network connection error, please check your network connection",
|
||||
"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_expired": "Authentication expired, please update credentials",
|
||||
|
||||
# Limit errors
|
||||
|
||||
"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",
|
||||
|
||||
# General errors
|
||||
|
||||
"unknown_error": "Unknown error: {message}",
|
||||
"invalid_parameter": "Invalid parameter: {parameter}",
|
||||
}
|
||||
}
|
||||
|
||||
# 错误解决建议
|
||||
ERROR_SOLUTIONS = {
|
||||
"zh_CN": {
|
||||
"missing_config": "请在配置中添加缺少的配置项",
|
||||
@@ -109,7 +93,7 @@ ERROR_SOLUTIONS = {
|
||||
"invalid_secret_key": "请检查Secret Key格式,确保输入正确",
|
||||
"invalid_region": "请选择有效的腾讯云区域,如ap-shanghai",
|
||||
"invalid_person_group_id": "请使用字母、数字、连字符和下划线组合",
|
||||
|
||||
|
||||
"image_not_found": "请检查图片路径是否正确",
|
||||
"image_too_large": "请使用小于10MB的图片,或对图片进行压缩",
|
||||
"image_format_not_supported": "请将图片转换为JPG、PNG、BMP或GIF格式",
|
||||
@@ -118,25 +102,25 @@ ERROR_SOLUTIONS = {
|
||||
"image_download_error": "请检查图片URL是否有效,或稍后重试",
|
||||
"invalid_image_url": "请提供有效的图片URL",
|
||||
"permission_denied": "请检查文件权限,或使用其他图片",
|
||||
|
||||
|
||||
"face_not_detected": "请使用包含清晰人脸的图片",
|
||||
"face_too_small": "请使用人脸更大的图片,或调整拍摄距离",
|
||||
"face_quality_poor": "请使用光线充足、对焦清晰的图片",
|
||||
|
||||
|
||||
"api_error": "请稍后重试,如果问题持续,请联系技术支持",
|
||||
"person_not_found": "请检查人员ID是否正确,或确保人员已添加到人员库",
|
||||
"group_not_found": "请检查人员库ID是否正确,或创建新的人员库",
|
||||
"face_not_found": "请检查人脸ID是否正确",
|
||||
|
||||
|
||||
"network_error": "请检查网络连接,确保网络通畅",
|
||||
"timeout_error": "请稍后重试,或在网络状况良好时使用",
|
||||
|
||||
|
||||
"auth_failure": "请检查Secret ID和Secret Key是否正确",
|
||||
"auth_expired": "请更新腾讯云凭证",
|
||||
|
||||
|
||||
"rate_limit_exceeded": "请降低调用频率,稍后重试",
|
||||
"quota_exceeded": "请等待配额重置,或升级腾讯云套餐",
|
||||
|
||||
|
||||
"unknown_error": "请记录错误信息并联系技术支持",
|
||||
"invalid_parameter": "请检查API调用参数是否正确",
|
||||
},
|
||||
@@ -146,7 +130,7 @@ ERROR_SOLUTIONS = {
|
||||
"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_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_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",
|
||||
@@ -155,25 +139,25 @@ ERROR_SOLUTIONS = {
|
||||
"image_download_error": "Please check if the image URL is valid, or try again later",
|
||||
"invalid_image_url": "Please provide a valid image URL",
|
||||
"permission_denied": "Please check file permissions, or use a different image",
|
||||
|
||||
|
||||
"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_quality_poor": "Please use a well-lit and focused image",
|
||||
|
||||
|
||||
"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",
|
||||
"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",
|
||||
|
||||
|
||||
"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",
|
||||
|
||||
|
||||
"auth_failure": "Please check if Secret ID and Secret Key are correct",
|
||||
"auth_expired": "Please update Tencent Cloud credentials",
|
||||
|
||||
|
||||
"rate_limit_exceeded": "Please reduce call frequency and try again later",
|
||||
"quota_exceeded": "Please wait for quota reset, or upgrade Tencent Cloud plan",
|
||||
|
||||
|
||||
"unknown_error": "Please record the error information and contact technical support",
|
||||
"invalid_parameter": "Please check if the API call parameters are correct",
|
||||
}
|
||||
@@ -182,7 +166,7 @@ ERROR_SOLUTIONS = {
|
||||
|
||||
class TencentFaceRecognitionError(HomeAssistantError):
|
||||
"""腾讯云人脸识别基础错误类"""
|
||||
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
message: str,
|
||||
@@ -190,37 +174,30 @@ class TencentFaceRecognitionError(HomeAssistantError):
|
||||
error_details: Optional[Dict[str, Any]] = None,
|
||||
locale: str = "zh_CN"
|
||||
):
|
||||
"""初始化错误"""
|
||||
super().__init__(message)
|
||||
self.error_key = error_key
|
||||
self.error_details = error_details or {}
|
||||
self.locale = locale
|
||||
|
||||
# 获取本地化消息
|
||||
|
||||
self.localized_message = self._get_localized_message()
|
||||
# 获取解决建议
|
||||
self.solution = self._get_solution()
|
||||
|
||||
|
||||
def _get_localized_message(self) -> str:
|
||||
"""获取本地化错误消息"""
|
||||
if not self.error_key:
|
||||
return str(self)
|
||||
|
||||
|
||||
messages = ERROR_MESSAGES.get(self.locale, ERROR_MESSAGES["zh_CN"])
|
||||
template = messages.get(self.error_key, str(self))
|
||||
|
||||
|
||||
try:
|
||||
# 使用错误详情填充模板
|
||||
return template.format(**self.error_details)
|
||||
except (KeyError, ValueError):
|
||||
# 如果填充失败,返回原始消息
|
||||
return str(self)
|
||||
|
||||
|
||||
def _get_solution(self) -> str:
|
||||
"""获取错误解决建议"""
|
||||
if not self.error_key:
|
||||
return ""
|
||||
|
||||
|
||||
solutions = ERROR_SOLUTIONS.get(self.locale, ERROR_SOLUTIONS["zh_CN"])
|
||||
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"),
|
||||
"ResourceUnavailable.NetworkError": (NetworkError, "network_error"),
|
||||
}
|
||||
|
||||
|
||||
error_info = error_mapping.get(error_code, (ApiError, "api_error"))
|
||||
error_class = error_info[0]
|
||||
error_key = error_info[1]
|
||||
|
||||
|
||||
_LOGGER.error("腾讯云API错误: %s - %s", error_code, error_message)
|
||||
|
||||
# 创建带有本地化消息的错误
|
||||
|
||||
error_details = {
|
||||
"code": error_code,
|
||||
"message": error_message
|
||||
}
|
||||
|
||||
|
||||
raise error_class(
|
||||
f"{error_code}: {error_message}",
|
||||
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:
|
||||
"""记录日志并抛出错误"""
|
||||
if details:
|
||||
_LOGGER.error("%s: %s, 详情: %s", error_class.__name__, message, details)
|
||||
_LOGGER.error("%s: %s", error_class.__name__, message)
|
||||
else:
|
||||
_LOGGER.error("%s: %s", error_class.__name__, message)
|
||||
|
||||
# 创建带有本地化消息的错误
|
||||
|
||||
raise error_class(
|
||||
message,
|
||||
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:
|
||||
"""验证配置"""
|
||||
required_keys = ["secret_id", "secret_key"]
|
||||
|
||||
required_keys = [CONF_SECRET_ID, CONF_SECRET_KEY]
|
||||
|
||||
for key in required_keys:
|
||||
if not config.get(key):
|
||||
log_and_raise_error(
|
||||
@@ -340,29 +315,26 @@ def validate_config(config: Dict[str, Any]) -> None:
|
||||
{"key": key},
|
||||
"missing_config"
|
||||
)
|
||||
|
||||
# 验证Secret ID格式
|
||||
secret_id = config.get("secret_id")
|
||||
|
||||
secret_id = config.get(CONF_SECRET_ID)
|
||||
if not isinstance(secret_id, str) or len(secret_id.strip()) == 0:
|
||||
log_and_raise_error(
|
||||
InvalidConfigError,
|
||||
"Secret ID必须是非空字符串,且应以 AKID 开头",
|
||||
{"secret_id": str(secret_id)},
|
||||
{"secret_id": "***"},
|
||||
"invalid_secret_id"
|
||||
)
|
||||
|
||||
# 验证Secret Key格式
|
||||
secret_key = config.get("secret_key")
|
||||
|
||||
secret_key = config.get(CONF_SECRET_KEY)
|
||||
if not isinstance(secret_key, str) or len(secret_key.strip()) == 0:
|
||||
log_and_raise_error(
|
||||
InvalidConfigError,
|
||||
"Secret Key必须是非空字符串",
|
||||
{"secret_key": str(secret_key)},
|
||||
{"secret_key": "***"},
|
||||
"invalid_secret_key"
|
||||
)
|
||||
|
||||
# 验证区域格式
|
||||
region = config.get("region", "ap-shanghai")
|
||||
|
||||
region = config.get(CONF_REGION, "ap-shanghai")
|
||||
if not isinstance(region, str) or len(region.strip()) == 0:
|
||||
log_and_raise_error(
|
||||
InvalidConfigError,
|
||||
@@ -370,9 +342,8 @@ def validate_config(config: Dict[str, Any]) -> None:
|
||||
{"region": str(region)},
|
||||
"invalid_region"
|
||||
)
|
||||
|
||||
# 验证人员库ID格式
|
||||
person_group_id = config.get("person_group_id", "Hass")
|
||||
|
||||
person_group_id = config.get(CONF_PERSON_GROUP_ID, "Hass")
|
||||
if not isinstance(person_group_id, str) or len(person_group_id.strip()) == 0:
|
||||
log_and_raise_error(
|
||||
InvalidConfigError,
|
||||
@@ -380,5 +351,5 @@ def validate_config(config: Dict[str, Any]) -> None:
|
||||
{"person_group_id": str(person_group_id)},
|
||||
"invalid_person_group_id"
|
||||
)
|
||||
|
||||
_LOGGER.debug("配置验证通过")
|
||||
|
||||
_LOGGER.debug("配置验证通过")
|
||||
|
||||
+179
-78
@@ -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)
|
||||
}
|
||||
|
||||
+4
-4
@@ -5,12 +5,12 @@
|
||||
"documentation": "https://code.nextrt.com/Hass/tencent_face_recognition",
|
||||
"issue_tracker": "https://code.nextrt.com/Hass/tencent_face_recognition/issues",
|
||||
"requirements": [
|
||||
"tencentcloud-sdk-python-common",
|
||||
"tencentcloud-sdk-python-iai"
|
||||
"tencentcloud-sdk-python-common>=3.0.0,<4.0.0",
|
||||
"tencentcloud-sdk-python-iai>=3.0.0,<4.0.0"
|
||||
],
|
||||
"dependencies": [],
|
||||
"codeowners": ["@xyzmos"],
|
||||
"version": "1.0.0",
|
||||
"version": "2.0.0",
|
||||
"iot_class": "cloud_polling",
|
||||
"resources": []
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -1,2 +1,2 @@
|
||||
tencentcloud-sdk-python-common>=3.0.0
|
||||
tencentcloud-sdk-python-iai>=3.0.0
|
||||
tencentcloud-sdk-python-common>=3.0.0,<4.0.0
|
||||
tencentcloud-sdk-python-iai>=3.0.0,<4.0.0
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
"""传感器实体"""
|
||||
|
||||
import logging
|
||||
import base64
|
||||
from typing import Dict, Any, Optional
|
||||
from typing import Dict, Any, Optional, List
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from homeassistant.components.sensor import SensorEntity
|
||||
@@ -11,11 +10,59 @@ from homeassistant.const import CONF_NAME
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.entity import EntityCategory
|
||||
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_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(
|
||||
@@ -23,184 +70,133 @@ async def async_setup_entry(
|
||||
) -> None:
|
||||
"""设置传感器实体"""
|
||||
_LOGGER.debug("设置腾讯云人脸识别传感器实体")
|
||||
|
||||
# 获取配置项
|
||||
name = entry.data.get(CONF_NAME, "腾讯云人脸识别")
|
||||
|
||||
# 创建传感器实体
|
||||
async_add_entities(
|
||||
[
|
||||
TencentFaceRecognitionStatusSensor(hass, entry, name),
|
||||
]
|
||||
)
|
||||
|
||||
# 获取人员列表并创建实体
|
||||
client = hass.data[DOMAIN][entry.entry_id]["client"]
|
||||
person_group_id = entry.data.get("person_group_id", "Hass")
|
||||
|
||||
try:
|
||||
persons = await hass.async_add_executor_job(
|
||||
client.get_person_list, person_group_id
|
||||
name = entry.data.get(CONF_NAME, "腾讯云人脸识别")
|
||||
|
||||
coordinator = TencentFaceCoordinator(hass, entry, client)
|
||||
|
||||
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(
|
||||
[
|
||||
TencentFaceRecognitionPersonSensor(hass, entry, person),
|
||||
]
|
||||
)
|
||||
except Exception as ex:
|
||||
_LOGGER.error("获取人员列表失败: %s", ex)
|
||||
|
||||
async_add_entities(entities)
|
||||
|
||||
|
||||
class TencentFaceRecognitionPersonSensor(SensorEntity):
|
||||
class TencentFaceRecognitionPersonSensor(CoordinatorEntity, SensorEntity):
|
||||
"""腾讯云人脸识别人员传感器"""
|
||||
|
||||
_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._person = person
|
||||
self._attr_name = person.get("person_name", "未知人员")
|
||||
self._attr_unique_id = f"{entry.entry_id}_{person.get('person_id')}"
|
||||
self._state = "未知"
|
||||
self._extra_state_attributes = {
|
||||
"person_id": person.get("person_id"),
|
||||
"gender": "男" if person.get("gender") == 1 else "女",
|
||||
"face_ids": person.get("face_ids"),
|
||||
}
|
||||
self._person_id = person.get("person_id")
|
||||
person_name = person.get("person_name", "未知人员")
|
||||
self._attr_name = person_name
|
||||
self._attr_unique_id = f"{entry.entry_id}_{self._person_id}"
|
||||
self._attr_translation_key = "person_sensor"
|
||||
|
||||
@property
|
||||
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
|
||||
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_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._name = name
|
||||
self._attr_name = f"{name} 状态"
|
||||
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
|
||||
def state(self) -> str:
|
||||
"""返回传感器状态"""
|
||||
return self._state
|
||||
if self.coordinator.data is None:
|
||||
return "未连接"
|
||||
if self.coordinator.last_update_success:
|
||||
return "已连接"
|
||||
return "连接错误"
|
||||
|
||||
@property
|
||||
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:
|
||||
"""当实体添加到Home Assistant时调用"""
|
||||
# 设置定期更新
|
||||
async_track_time_interval(
|
||||
self.hass,
|
||||
self._async_update,
|
||||
timedelta(minutes=5) # 每5分钟更新一次
|
||||
)
|
||||
|
||||
# 立即执行一次更新
|
||||
await self._async_update(None)
|
||||
if self.coordinator.data and self.coordinator.data.get("group_info"):
|
||||
group_info = self.coordinator.data["group_info"]
|
||||
attrs["group_name"] = group_info.get("group_name", "")
|
||||
attrs["face_model_version"] = group_info.get("face_model_version", "")
|
||||
|
||||
async def _async_update(self, _) -> None:
|
||||
"""更新传感器状态"""
|
||||
await self.async_update()
|
||||
return attrs
|
||||
|
||||
async def async_update(self) -> None:
|
||||
"""更新传感器状态"""
|
||||
try:
|
||||
# 获取插件实例
|
||||
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)
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""状态传感器始终可用"""
|
||||
return True
|
||||
|
||||
+305
-85
@@ -6,6 +6,7 @@ from typing import Dict, Any, List, Optional
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.core import HomeAssistant, ServiceCall
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
|
||||
from .const import (
|
||||
@@ -13,25 +14,39 @@ from .const import (
|
||||
SERVICE_FACE_SEARCH,
|
||||
SERVICE_DETECT_FACE,
|
||||
SERVICE_GET_FACE_ATTRIBUTES,
|
||||
SERVICE_CREATE_PERSON,
|
||||
SERVICE_DELETE_PERSON,
|
||||
SERVICE_CREATE_FACE,
|
||||
SERVICE_DELETE_FACE,
|
||||
ATTR_IMAGE_URL,
|
||||
ATTR_IMAGE_FILE,
|
||||
ATTR_IMAGE_PATH,
|
||||
ATTR_CAMERA_ENTITY_ID,
|
||||
ATTR_CONFIG_ENTRY_ID,
|
||||
ATTR_GROUP_ID,
|
||||
ATTR_MAX_FACE_NUM,
|
||||
ATTR_MIN_FACE_SIZE,
|
||||
ATTR_MAX_USER_NUM,
|
||||
ATTR_QUALITY_CONTROL,
|
||||
ATTR_NEED_ROTATE_CHECK,
|
||||
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__)
|
||||
|
||||
# 人脸搜索服务参数
|
||||
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_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_MAX_FACE_NUM, default=1): 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,
|
||||
@@ -40,31 +55,72 @@ FACE_SEARCH_SCHEMA = vol.Schema({
|
||||
vol.Optional(ATTR_FACE_MATCH_THRESHOLD, default=60.0): vol.Range(min=0, max=100),
|
||||
})
|
||||
|
||||
# 人脸检测服务参数
|
||||
DETECT_FACE_SCHEMA = vol.Schema({
|
||||
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_MAX_FACE_NUM, default=1): 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]),
|
||||
})
|
||||
|
||||
# 获取人脸属性服务参数
|
||||
GET_FACE_ATTRIBUTES_SCHEMA = vol.Schema({
|
||||
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_MAX_FACE_NUM, default=1): cv.positive_int,
|
||||
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:
|
||||
"""设置服务"""
|
||||
if hass.services.has_service(DOMAIN, SERVICE_FACE_SEARCH):
|
||||
return
|
||||
|
||||
_LOGGER.info("设置腾讯云人脸识别服务")
|
||||
|
||||
# 注册人脸搜索服务
|
||||
hass.services.async_register(
|
||||
DOMAIN,
|
||||
SERVICE_FACE_SEARCH,
|
||||
@@ -73,60 +129,108 @@ async def async_setup_services(hass: HomeAssistant) -> None:
|
||||
supports_response=True
|
||||
)
|
||||
|
||||
# 注册人脸检测服务
|
||||
hass.services.async_register(
|
||||
DOMAIN,
|
||||
SERVICE_DETECT_FACE,
|
||||
async_detect_face_service,
|
||||
schema=DETECT_FACE_SCHEMA
|
||||
schema=DETECT_FACE_SCHEMA,
|
||||
supports_response=True
|
||||
)
|
||||
|
||||
# 注册获取人脸属性服务
|
||||
hass.services.async_register(
|
||||
DOMAIN,
|
||||
SERVICE_GET_FACE_ATTRIBUTES,
|
||||
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:
|
||||
"""卸载服务"""
|
||||
if any(hass.config_entries.async_entries(DOMAIN)):
|
||||
return
|
||||
|
||||
_LOGGER.info("卸载腾讯云人脸识别服务")
|
||||
|
||||
# 注销所有服务
|
||||
hass.services.async_remove(DOMAIN, SERVICE_FACE_SEARCH)
|
||||
hass.services.async_remove(DOMAIN, SERVICE_DETECT_FACE)
|
||||
hass.services.async_remove(DOMAIN, SERVICE_GET_FACE_ATTRIBUTES)
|
||||
for service_name in [
|
||||
SERVICE_FACE_SEARCH,
|
||||
SERVICE_DETECT_FACE,
|
||||
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]:
|
||||
"""人脸搜索服务"""
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
|
||||
hass = call.hass
|
||||
data = call.data
|
||||
|
||||
# 获取配置项
|
||||
config_entry = _get_config_entry(hass)
|
||||
if not config_entry:
|
||||
entry_data = _get_entry_data(hass, data.get(ATTR_CONFIG_ENTRY_ID))
|
||||
if not entry_data:
|
||||
raise HomeAssistantError("未找到腾讯云人脸识别配置")
|
||||
|
||||
# 获取插件实例
|
||||
entry_data = _get_entry_data(hass)
|
||||
if not entry_data:
|
||||
raise HomeAssistantError("腾讯云人脸识别插件未正确初始化")
|
||||
|
||||
face_recognition = entry_data.get("face_recognition")
|
||||
if not face_recognition:
|
||||
raise HomeAssistantError("腾讯云人脸识别插件未正确初始化")
|
||||
|
||||
# 调用人脸搜索功能
|
||||
try:
|
||||
group_id = data.get('group_id')
|
||||
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)
|
||||
@@ -138,6 +242,7 @@ async def async_face_search_service(call: ServiceCall) -> Dict[str, Any]:
|
||||
image_url=image_url,
|
||||
image_path=image_path,
|
||||
image_file=image_file,
|
||||
camera_entity_id=camera_entity_id,
|
||||
group_ids=[group_id],
|
||||
max_face_num=max_face_num,
|
||||
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
|
||||
)
|
||||
|
||||
# 直接返回结果,包含状态和错误信息
|
||||
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
|
||||
|
||||
except HomeAssistantError:
|
||||
raise
|
||||
except Exception as ex:
|
||||
_LOGGER.error("人脸搜索服务调用失败: %s", ex)
|
||||
# 返回错误信息,而不是抛出异常
|
||||
return {
|
||||
"success": False,
|
||||
"faces": [],
|
||||
"error": str(ex),
|
||||
"error_code": "service_error",
|
||||
"error_message": str(ex)
|
||||
}
|
||||
raise HomeAssistantError(f"人脸搜索服务调用失败: {ex}")
|
||||
|
||||
|
||||
async def async_detect_face_service(call: ServiceCall) -> Dict[str, Any]:
|
||||
"""人脸检测服务"""
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
|
||||
hass = call.hass
|
||||
data = call.data
|
||||
|
||||
# 获取配置项
|
||||
config_entry = _get_config_entry(hass)
|
||||
if not config_entry:
|
||||
entry_data = _get_entry_data(hass, data.get(ATTR_CONFIG_ENTRY_ID))
|
||||
if not entry_data:
|
||||
raise HomeAssistantError("未找到腾讯云人脸识别配置")
|
||||
|
||||
# 获取插件实例
|
||||
entry_data = _get_entry_data(hass)
|
||||
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)
|
||||
@@ -196,52 +299,43 @@ async def async_detect_face_service(call: ServiceCall) -> Dict[str, Any]:
|
||||
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)
|
||||
# 返回错误信息,而不是抛出异常
|
||||
return {
|
||||
"success": False,
|
||||
"faces": [],
|
||||
"error": str(ex),
|
||||
"error_code": "service_error",
|
||||
"error_message": str(ex)
|
||||
}
|
||||
raise HomeAssistantError(f"人脸检测服务调用失败: {ex}")
|
||||
|
||||
|
||||
async def async_get_face_attributes_service(call: ServiceCall) -> Dict[str, Any]:
|
||||
"""获取人脸属性服务"""
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
|
||||
hass = call.hass
|
||||
data = call.data
|
||||
|
||||
# 获取配置项
|
||||
config_entry = _get_config_entry(hass)
|
||||
if not config_entry:
|
||||
entry_data = _get_entry_data(hass, data.get(ATTR_CONFIG_ENTRY_ID))
|
||||
if not entry_data:
|
||||
raise HomeAssistantError("未找到腾讯云人脸识别配置")
|
||||
|
||||
# 获取插件实例
|
||||
entry_data = _get_entry_data(hass)
|
||||
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)
|
||||
|
||||
@@ -249,34 +343,160 @@ async def async_get_face_attributes_service(call: ServiceCall) -> Dict[str, Any]
|
||||
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)
|
||||
# 返回错误信息,而不是抛出异常
|
||||
return {
|
||||
"success": False,
|
||||
"faces": [],
|
||||
"error": str(ex),
|
||||
"error_code": "service_error",
|
||||
"error_message": str(ex)
|
||||
}
|
||||
raise HomeAssistantError(f"获取人脸属性服务调用失败: {ex}")
|
||||
|
||||
|
||||
def _get_config_entry(hass: HomeAssistant):
|
||||
"""获取配置项"""
|
||||
for entry in hass.config_entries.async_entries(DOMAIN):
|
||||
return entry
|
||||
return None
|
||||
async def async_create_person_service(call: ServiceCall) -> Dict[str, Any]:
|
||||
"""创建人员服务"""
|
||||
hass = call.hass
|
||||
data = call.data
|
||||
|
||||
def _get_entry_data(hass: HomeAssistant):
|
||||
"""获取配置项数据"""
|
||||
for entry_id, data in hass.data.get(DOMAIN, {}).items():
|
||||
if isinstance(data, dict) and "face_recognition" in data:
|
||||
return data
|
||||
return None
|
||||
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}")
|
||||
|
||||
|
||||
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
@@ -1,4 +1,3 @@
|
||||
# 腾讯云人脸识别服务定义
|
||||
face_search:
|
||||
name: 人脸搜索
|
||||
description: 在指定的人员库中搜索人脸。
|
||||
@@ -24,7 +23,19 @@ face_search:
|
||||
text:
|
||||
image_file:
|
||||
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:
|
||||
text:
|
||||
max_face_num:
|
||||
@@ -62,90 +73,16 @@ face_search:
|
||||
mode: box
|
||||
quality_control:
|
||||
name: 质量控制
|
||||
description: 是否进行质量控制。
|
||||
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: 最多检测的人脸数量。
|
||||
description: 是否进行质量控制(0=否,1=是)。
|
||||
default: 1
|
||||
example: 1
|
||||
selector:
|
||||
number:
|
||||
min: 1
|
||||
max: 10
|
||||
step: 1
|
||||
mode: box
|
||||
min_face_size:
|
||||
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:
|
||||
select:
|
||||
options:
|
||||
- label: "否"
|
||||
value: "0"
|
||||
- label: "是"
|
||||
value: "1"
|
||||
|
||||
get_face_attributes:
|
||||
name: 获取人脸属性
|
||||
@@ -165,7 +102,19 @@ get_face_attributes:
|
||||
text:
|
||||
image_file:
|
||||
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:
|
||||
text:
|
||||
max_face_num:
|
||||
@@ -181,8 +130,209 @@ get_face_attributes:
|
||||
mode: box
|
||||
need_rotate_check:
|
||||
name: 旋转检查
|
||||
description: 是否进行旋转检查。
|
||||
default: true
|
||||
example: true
|
||||
description: 是否进行旋转检查(0=否,1=是)。
|
||||
default: 1
|
||||
example: 1
|
||||
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
@@ -12,7 +12,7 @@
|
||||
"name": "名称"
|
||||
},
|
||||
"data_description": {
|
||||
"secret_id": "腾讯云API的Secret ID",
|
||||
"secret_id": "腾讯云API的Secret ID,以AKID开头",
|
||||
"secret_key": "腾讯云API的Secret Key",
|
||||
"region": "腾讯云服务区域,例如:ap-shanghai",
|
||||
"person_group_id": "人脸库人员组ID,用于存储和搜索人脸",
|
||||
@@ -21,7 +21,10 @@
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"invalid_config": "无效的配置"
|
||||
"invalid_config": "无效的配置",
|
||||
"auth_failure": "认证失败,请检查Secret ID和Secret Key",
|
||||
"network_error": "网络错误,请检查网络连接和区域设置",
|
||||
"rate_limit_exceeded": "API调用频率过高,请稍后再试"
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "已经配置了腾讯云人脸识别服务"
|
||||
@@ -31,21 +34,65 @@
|
||||
"step": {
|
||||
"init": {
|
||||
"title": "腾讯云人脸识别选项",
|
||||
"description": "配置腾讯云人脸识别服务选项",
|
||||
"description": "请选择要管理的项目",
|
||||
"data": {
|
||||
"region": "区域"
|
||||
"action": "操作"
|
||||
}
|
||||
},
|
||||
"config": {
|
||||
"title": "配置设置",
|
||||
"description": "修改腾讯云人脸识别的配置设置",
|
||||
"data": {
|
||||
"region": "区域",
|
||||
"person_group_id": "人员组ID"
|
||||
},
|
||||
"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": {
|
||||
"face_search": {
|
||||
"name": "人脸搜索",
|
||||
"description": "搜索人脸",
|
||||
"description": "在指定的人员库中搜索人脸",
|
||||
"fields": {
|
||||
"group_id": {
|
||||
"name": "人员库ID",
|
||||
"description": "要搜索的人员库ID"
|
||||
},
|
||||
"image_url": {
|
||||
"name": "图片URL",
|
||||
"description": "要搜索的图片URL"
|
||||
@@ -58,6 +105,14 @@
|
||||
"name": "图片文件",
|
||||
"description": "要搜索的图片文件(Base64编码)"
|
||||
},
|
||||
"camera_entity_id": {
|
||||
"name": "摄像头实体",
|
||||
"description": "用于获取图片的摄像头实体ID"
|
||||
},
|
||||
"config_entry_id": {
|
||||
"name": "配置项ID",
|
||||
"description": "使用的配置项ID(多配置时指定)"
|
||||
},
|
||||
"max_face_num": {
|
||||
"name": "最大人脸数量",
|
||||
"description": "最多处理的人脸数量"
|
||||
@@ -72,11 +127,11 @@
|
||||
},
|
||||
"quality_control": {
|
||||
"name": "质量控制",
|
||||
"description": "是否进行质量控制"
|
||||
"description": "是否进行质量控制(0=否,1=是)"
|
||||
},
|
||||
"need_rotate_check": {
|
||||
"name": "旋转检查",
|
||||
"description": "是否进行旋转检查"
|
||||
"description": "是否进行旋转检查(0=否,1=是)"
|
||||
},
|
||||
"face_match_threshold": {
|
||||
"name": "人脸匹配阈值",
|
||||
@@ -100,6 +155,14 @@
|
||||
"name": "图片文件",
|
||||
"description": "要检测的图片文件(Base64编码)"
|
||||
},
|
||||
"camera_entity_id": {
|
||||
"name": "摄像头实体",
|
||||
"description": "用于获取图片的摄像头实体ID"
|
||||
},
|
||||
"config_entry_id": {
|
||||
"name": "配置项ID",
|
||||
"description": "使用的配置项ID(多配置时指定)"
|
||||
},
|
||||
"max_face_num": {
|
||||
"name": "最大人脸数量",
|
||||
"description": "最多检测的人脸数量"
|
||||
@@ -110,7 +173,7 @@
|
||||
},
|
||||
"need_rotate_check": {
|
||||
"name": "旋转检查",
|
||||
"description": "是否进行旋转检查"
|
||||
"description": "是否进行旋转检查(0=否,1=是)"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -130,15 +193,147 @@
|
||||
"name": "图片文件",
|
||||
"description": "要分析的图片文件(Base64编码)"
|
||||
},
|
||||
"camera_entity_id": {
|
||||
"name": "摄像头实体",
|
||||
"description": "用于获取图片的摄像头实体ID"
|
||||
},
|
||||
"config_entry_id": {
|
||||
"name": "配置项ID",
|
||||
"description": "使用的配置项ID(多配置时指定)"
|
||||
},
|
||||
"max_face_num": {
|
||||
"name": "最大人脸数量",
|
||||
"description": "最多分析的人脸数量"
|
||||
},
|
||||
"need_rotate_check": {
|
||||
"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
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user