feat: 添加 HA 管理面板功能
- config_flow.py: 新增人员管理步骤(async_step_manage/list/create) 支持在 HA 配置 -> 选项 中查看人员列表、创建和删除 - __init__.py: 新增 3 个 WebSocket API 命令(list/create/delete) 前端可通过 HA WebSocket 直接调用 - blueprints/face_detection_automation.yaml: 自动化蓝图 支持人脸检测/搜索 + 通知推送 - manifest.json: 版本号 2.2.0 用户现可通过 HA 界面对人员进行可视化管理
This commit is contained in:
+111
-2
@@ -4,12 +4,16 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import base64
|
||||
import logging
|
||||
from typing import Any, Dict
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError
|
||||
|
||||
from homeassistant.components import websocket_api
|
||||
from homeassistant.components.camera import async_get_image
|
||||
|
||||
from .const import (
|
||||
CONF_PERSON_GROUP_ID,
|
||||
@@ -69,7 +73,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
# 4. 注册服务
|
||||
await async_setup_services(hass)
|
||||
|
||||
# 5. 前向设置平台(sensor)
|
||||
# 5. 注册 WebSocket API 命令
|
||||
websocket_api.async_register_command(hass, ws_list_persons)
|
||||
websocket_api.async_register_command(hass, ws_create_person)
|
||||
websocket_api.async_register_command(hass, ws_delete_person)
|
||||
|
||||
# 6. 前向设置平台(sensor)
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
# 6. 配置项更新时自动重新加载平台
|
||||
@@ -118,3 +127,103 @@ async def async_remove_entry(hass: HomeAssistant, entry: ConfigEntry) -> None:
|
||||
await hass.async_add_executor_job(data["client"].close)
|
||||
except Exception: # noqa: BLE001
|
||||
pass
|
||||
|
||||
|
||||
# --- WebSocket API ---------------------------------------------------------
|
||||
|
||||
|
||||
@websocket_api.websocket_command({
|
||||
"type": "tencent_face_recognition/list_persons",
|
||||
})
|
||||
@websocket_api.async_response
|
||||
async def ws_list_persons(
|
||||
hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: Dict[str, Any]
|
||||
) -> None:
|
||||
"""列出所有人员。"""
|
||||
entry_data = None
|
||||
for data in hass.data.get(DOMAIN, {}).values():
|
||||
if isinstance(data, dict) and "client" in data:
|
||||
entry_data = data
|
||||
break
|
||||
if not entry_data:
|
||||
connection.send_error(msg["id"], "not_found", "未找到配置")
|
||||
return
|
||||
|
||||
client: TencentCloudClient = entry_data["client"]
|
||||
group_id = entry_data.get("person_group_id", "Hass")
|
||||
try:
|
||||
result = await hass.async_add_executor_job(client.get_person_list_all, group_id)
|
||||
connection.send_result(msg["id"], result)
|
||||
except Exception as ex:
|
||||
_LOGGER.error("列出人员失败: %s", ex)
|
||||
connection.send_error(msg["id"], "api_error", str(ex))
|
||||
|
||||
|
||||
@websocket_api.websocket_command({
|
||||
"type": "tencent_face_recognition/create_person",
|
||||
"data": {
|
||||
"person_id": str,
|
||||
"person_name": str,
|
||||
"camera_entity_id": str,
|
||||
},
|
||||
})
|
||||
@websocket_api.async_response
|
||||
async def ws_create_person(
|
||||
hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: Dict[str, Any]
|
||||
) -> None:
|
||||
"""通过摄像头创建人员。"""
|
||||
entry_data = None
|
||||
for data in hass.data.get(DOMAIN, {}).values():
|
||||
if isinstance(data, dict) and "client" in data:
|
||||
entry_data = data
|
||||
break
|
||||
if not entry_data:
|
||||
connection.send_error(msg["id"], "not_found", "未找到配置")
|
||||
return
|
||||
|
||||
client: TencentCloudClient = entry_data["client"]
|
||||
group_id = entry_data.get("person_group_id", "Hass")
|
||||
person_id = msg["data"]["person_id"]
|
||||
person_name = msg["data"]["person_name"]
|
||||
camera_entity_id = msg["data"]["camera_entity_id"]
|
||||
|
||||
try:
|
||||
image = await async_get_image(hass, camera_entity_id)
|
||||
image_base64 = base64.b64encode(image.content).decode("utf-8")
|
||||
result = await hass.async_add_executor_job(
|
||||
client.create_person,
|
||||
person_id, person_name, group_id,
|
||||
None, None, None, image_base64,
|
||||
)
|
||||
connection.send_result(msg["id"], result)
|
||||
except Exception as ex:
|
||||
_LOGGER.error("创建人员失败: %s", ex)
|
||||
connection.send_error(msg["id"], "api_error", str(ex))
|
||||
|
||||
|
||||
@websocket_api.websocket_command({
|
||||
"type": "tencent_face_recognition/delete_person",
|
||||
"data": {"person_id": str},
|
||||
})
|
||||
@websocket_api.async_response
|
||||
async def ws_delete_person(
|
||||
hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: Dict[str, Any]
|
||||
) -> None:
|
||||
"""删除人员。"""
|
||||
entry_data = None
|
||||
for data in hass.data.get(DOMAIN, {}).values():
|
||||
if isinstance(data, dict) and "client" in data:
|
||||
entry_data = data
|
||||
break
|
||||
if not entry_data:
|
||||
connection.send_error(msg["id"], "not_found", "未找到配置")
|
||||
return
|
||||
|
||||
client: TencentCloudClient = entry_data["client"]
|
||||
person_id = msg["data"]["person_id"]
|
||||
try:
|
||||
result = await hass.async_add_executor_job(client.delete_person, person_id)
|
||||
connection.send_result(msg["id"], result)
|
||||
except Exception as ex:
|
||||
_LOGGER.error("删除人员失败: %s", ex)
|
||||
connection.send_error(msg["id"], "api_error", str(ex))
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
blueprint:
|
||||
name: 腾讯云人脸检测自动化
|
||||
description: 检测到人脸时自动触发,可配合摄像头抓拍
|
||||
domain: automation
|
||||
input:
|
||||
camera_entity:
|
||||
name: 摄像头
|
||||
description: 用于人脸检测的摄像头实体
|
||||
selector:
|
||||
entity:
|
||||
domain: camera
|
||||
face_recognition_action:
|
||||
name: 检测到人脸时的操作
|
||||
description: 当检测到人脸时执行的操作(可以选择人脸搜索、检测等)
|
||||
default: detect
|
||||
selector:
|
||||
select:
|
||||
options:
|
||||
- label: "仅检测人脸"
|
||||
value: "detect"
|
||||
- label: "搜索人脸(人员库匹配)"
|
||||
value: "search"
|
||||
person_group_id:
|
||||
name: 人员库ID
|
||||
description: 人脸搜索时使用的人员库(仅搜索模式)
|
||||
default: "Hass"
|
||||
max_face_num:
|
||||
name: 最大人脸数
|
||||
description: 检测的最大人脸数量
|
||||
default: 3
|
||||
selector:
|
||||
number:
|
||||
min: 1
|
||||
max: 10
|
||||
mode: slider
|
||||
notification_action:
|
||||
name: 检测到已知人员时的通知
|
||||
description: 当匹配到已知人员时发送通知
|
||||
default: true
|
||||
selector:
|
||||
boolean:
|
||||
|
||||
trigger_variables:
|
||||
camera_entity: !input camera_entity
|
||||
face_recognition_action: !input face_recognition_action
|
||||
person_group_id: !input person_group_id
|
||||
max_face_num: !input max_face_num
|
||||
notification_action: !input notification_action
|
||||
|
||||
trigger:
|
||||
- platform: state
|
||||
entity_id: !input camera_entity
|
||||
attribute: motion_detected
|
||||
to: "true"
|
||||
- platform: homeassistant
|
||||
event: start
|
||||
|
||||
condition:
|
||||
- condition: template
|
||||
value_template: >
|
||||
{% if face_recognition_action == "search" %}
|
||||
{{ person_group_id | length > 0 }}
|
||||
{% else %}
|
||||
true
|
||||
{% endif %}
|
||||
|
||||
action:
|
||||
- variables:
|
||||
camera: !input camera_entity
|
||||
action_type: !input face_recognition_action
|
||||
alias: "开始人脸检测"
|
||||
- if:
|
||||
- condition: template
|
||||
value_template: "{{ action_type == 'detect' }}"
|
||||
then:
|
||||
- alias: "执行人脸检测"
|
||||
action: tencent_face_recognition.detect_face
|
||||
data:
|
||||
camera_entity_id: "{{ camera }}"
|
||||
max_face_num: "{{ max_face_num | int }}"
|
||||
response_variable: detection_result
|
||||
else:
|
||||
- alias: "执行人脸搜索"
|
||||
action: tencent_face_recognition.face_search
|
||||
data:
|
||||
camera_entity_id: "{{ camera }}"
|
||||
group_id: "{{ person_group_id }}"
|
||||
max_face_num: "{{ max_face_num | int }}"
|
||||
response_variable: detection_result
|
||||
|
||||
- alias: "检查检测结果"
|
||||
choose:
|
||||
- conditions:
|
||||
- condition: template
|
||||
value_template: >
|
||||
{{ detection_result.get('success', false) and
|
||||
detection_result.get('faces', []) | length > 0 }}
|
||||
sequence:
|
||||
- alias: "已知人员匹配到"
|
||||
if:
|
||||
- condition: template
|
||||
value_template: "{{ action_type == 'search' }}"
|
||||
then:
|
||||
- event: face_detected
|
||||
event_data:
|
||||
person_id: >
|
||||
{{ detection_result.faces[0].candidates[0].person_id
|
||||
if detection_result.faces[0].candidates else 'unknown' }}
|
||||
person_name: >
|
||||
{{ detection_result.faces[0].candidates[0].person_name
|
||||
if detection_result.faces[0].candidates else '未知' }}
|
||||
score: >
|
||||
{{ detection_result.faces[0].candidates[0].score
|
||||
if detection_result.faces[0].candidates else 0 }}
|
||||
- if:
|
||||
- condition: template
|
||||
value_template: "{{ notification_action }}"
|
||||
then:
|
||||
- alias: "发送通知"
|
||||
action: persistent_notification.create
|
||||
data:
|
||||
title: "人脸识别 - 已知人员"
|
||||
message: >
|
||||
检测到已知人员:{{ detection_result.faces[0].candidates[0].person_name
|
||||
if detection_result.faces[0].candidates else '未知' }}
|
||||
(匹配度:{{ detection_result.faces[0].candidates[0].score
|
||||
if detection_result.faces[0].candidates else 0 }}%)
|
||||
else:
|
||||
- event: face_detected
|
||||
event_data:
|
||||
face_count: "{{ detection_result.faces | length }}"
|
||||
details: "{{ detection_result.faces }}"
|
||||
|
||||
mode: single
|
||||
+131
-1
@@ -239,7 +239,8 @@ class TencentFaceRecognitionOptionsFlow(config_entries.OptionsFlow):
|
||||
"options": [
|
||||
{"label": "配置设置", "value": "config"},
|
||||
{"label": "测试连接", "value": "test_connection"},
|
||||
{"label": "重新认证", "value": "reauth"}
|
||||
{"label": "重新认证", "value": "reauth"},
|
||||
{"label": "人员管理", "value": "manage"}
|
||||
]
|
||||
}
|
||||
})
|
||||
@@ -249,6 +250,135 @@ class TencentFaceRecognitionOptionsFlow(config_entries.OptionsFlow):
|
||||
}
|
||||
)
|
||||
|
||||
async def async_step_manage(self, user_input=None):
|
||||
"""人员管理步骤:查看列表、创建、删除"""
|
||||
errors: dict[str, str] = {}
|
||||
client = None
|
||||
try:
|
||||
client = TencentCloudClient(
|
||||
self._data.get(CONF_SECRET_ID, ""),
|
||||
self._data.get(CONF_SECRET_KEY, ""),
|
||||
self._data.get(CONF_REGION, DEFAULT_REGION),
|
||||
)
|
||||
result = await self.hass.async_add_executor_job(
|
||||
client.get_person_list_all,
|
||||
self._data.get(CONF_PERSON_GROUP_ID, "Hass"),
|
||||
)
|
||||
persons = result.get("persons", []) if result.get("success", False) else []
|
||||
|
||||
if user_input is not None:
|
||||
action = user_input.get("person_action")
|
||||
if action == "refresh":
|
||||
return await self.async_step_manage()
|
||||
elif action == "create":
|
||||
return await self.async_step_create_person()
|
||||
elif action == "delete":
|
||||
selected = user_input.get("selected_person")
|
||||
if selected:
|
||||
await self.hass.async_add_executor_job(
|
||||
client.delete_person, selected
|
||||
)
|
||||
_LOGGER.info("人员已删除: %s", selected)
|
||||
return await self.async_step_manage()
|
||||
else:
|
||||
errors["base"] = "请选择要删除的人员"
|
||||
|
||||
person_options = [
|
||||
{"label": f"{p.get('person_name', '未知')} ({p.get('person_id', '')})",
|
||||
"value": p.get("person_id", "")}
|
||||
for p in persons
|
||||
]
|
||||
|
||||
schema = vol.Schema({
|
||||
vol.Required("person_action"): selector({
|
||||
"select": {
|
||||
"options": [
|
||||
{"label": "刷新列表", "value": "refresh"},
|
||||
{"label": "创建人员", "value": "create"},
|
||||
{"label": "删除选中人员", "value": "delete"},
|
||||
]
|
||||
}
|
||||
}),
|
||||
})
|
||||
if person_options:
|
||||
schema = schema.extend({
|
||||
vol.Optional("selected_person"): selector({
|
||||
"select": {
|
||||
"options": person_options
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="manage",
|
||||
data_schema=schema,
|
||||
errors=errors,
|
||||
description_placeholders={
|
||||
"person_count": str(len(persons)),
|
||||
"group_id": self._data.get(CONF_PERSON_GROUP_ID, "Hass"),
|
||||
}
|
||||
)
|
||||
except Exception as ex:
|
||||
_LOGGER.error("人员管理加载失败: %s", ex)
|
||||
errors["base"] = f"加载人员列表失败: {ex}"
|
||||
return self.async_show_form(
|
||||
step_id="manage",
|
||||
data_schema=vol.Schema({}),
|
||||
errors=errors,
|
||||
)
|
||||
finally:
|
||||
if client is not None:
|
||||
await self.hass.async_add_executor_job(client.close)
|
||||
|
||||
async def async_step_create_person(self, user_input=None):
|
||||
"""创建人员步骤"""
|
||||
errors: dict[str, str] = {}
|
||||
client = None
|
||||
if user_input is not None:
|
||||
person_id = user_input.get("person_id")
|
||||
person_name = user_input.get("person_name")
|
||||
camera = user_input.get("camera_entity")
|
||||
try:
|
||||
client = TencentCloudClient(
|
||||
self._data.get(CONF_SECRET_ID, ""),
|
||||
self._data.get(CONF_SECRET_KEY, ""),
|
||||
self._data.get(CONF_REGION, DEFAULT_REGION),
|
||||
)
|
||||
from homeassistant.components.camera import async_get_image
|
||||
import base64
|
||||
image = await async_get_image(self.hass, camera)
|
||||
image_base64 = base64.b64encode(image.content).decode("utf-8")
|
||||
|
||||
result = await self.hass.async_add_executor_job(
|
||||
client.create_person,
|
||||
person_id, person_name,
|
||||
self._data.get(CONF_PERSON_GROUP_ID, "Hass"),
|
||||
None, None, None, image_base64,
|
||||
)
|
||||
if result.get("success", False):
|
||||
_LOGGER.info("人员创建成功: %s", person_id)
|
||||
return await self.async_step_manage()
|
||||
else:
|
||||
errors["base"] = f"创建失败: {result.get('error_message', '未知错误')}"
|
||||
except Exception as ex:
|
||||
errors["base"] = f"创建人员失败: {ex}"
|
||||
_LOGGER.error("创建人员失败: %s", ex)
|
||||
finally:
|
||||
if client is not None:
|
||||
await self.hass.async_add_executor_job(client.close)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="create_person",
|
||||
data_schema=vol.Schema({
|
||||
vol.Required("person_id"): str,
|
||||
vol.Required("person_name"): str,
|
||||
vol.Required("camera_entity"): selector({
|
||||
"entity": {"domain": "camera"}
|
||||
}),
|
||||
}),
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
async def async_step_config(self, user_input=None):
|
||||
"""配置设置步骤"""
|
||||
errors = {}
|
||||
|
||||
+1
-1
@@ -11,7 +11,7 @@
|
||||
],
|
||||
"dependencies": [],
|
||||
"codeowners": ["@xyzmos"],
|
||||
"version": "2.1.0",
|
||||
"version": "2.2.0",
|
||||
"iot_class": "cloud_polling",
|
||||
"resources": []
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user