"""腾讯云人脸识别插件配置流程""" import logging import re from typing import Dict, Any, Optional, List import voluptuous as vol from homeassistant import config_entries from homeassistant.core import callback from homeassistant.const import CONF_NAME from homeassistant.helpers.selector import selector from .const import ( DOMAIN, CONF_SECRET_ID, CONF_SECRET_KEY, CONF_REGION, CONF_PERSON_GROUP_ID, DEFAULT_REGION, ) from .errors import ( AuthenticationError, InvalidConfigError, NetworkError, RateLimitError, validate_config, ) from .tencent_cloud_client import TencentCloudClient _LOGGER = logging.getLogger(__name__) class TencentFaceRecognitionConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """腾讯云人脸识别配置流程""" VERSION = 1 # CONNECTION_CLASS 已在 HA 2024.7 废弃,连接类型现由 manifest.json 的 # iot_class 字段声明(本项目为 cloud_polling)。 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 self._config_info = {} async def async_step_user(self, user_input: Optional[Dict[str, Any]] = None): """处理用户配置步骤""" 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) # 防止重复配置:以 secret_id 作为唯一标识 await self.async_set_unique_id( f"{DOMAIN}_{user_input.get(CONF_SECRET_ID)}" ) self._abort_if_unique_id_configured() 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) errors["base"] = f"配置验证失败: {str(ex)}" return self._show_user_form(errors) def _show_user_form(self, errors: Dict[str, str]) -> dict: """显示用户配置表单""" return self.async_show_form( step_id="user", data_schema=vol.Schema({ vol.Required(CONF_SECRET_ID): vol.All(str, vol.Length(min=1)), vol.Required(CONF_SECRET_KEY): vol.All(str, vol.Length(min=1)), vol.Optional(CONF_REGION, default=DEFAULT_REGION): selector({ "select": { "options": [{"label": region, "value": region} for region in self.AVAILABLE_REGIONS], "mode": "dropdown" } }), vol.Optional(CONF_PERSON_GROUP_ID, default="Hass"): vol.All( str, vol.Length(min=1, max=64) ), vol.Optional(CONF_NAME, default="腾讯云人脸识别"): vol.All(str, vol.Length(min=1, max=50)), }), errors=errors, description_placeholders={ "secret_id_example": "AKIDxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", "secret_key_example": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", "person_group_id_example": "Hass", "region_example": "ap-shanghai" } ) def _validate_input_format(self, user_input: Dict[str, Any], errors: Dict[str, str]) -> None: """验证输入格式""" secret_id = user_input.get(CONF_SECRET_ID, "") if not self.SECRET_ID_REGEX.match(secret_id): errors[CONF_SECRET_ID] = "Secret ID格式不正确,应以AKID开头,后跟36个以上字符" secret_key = user_input.get(CONF_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 self.AVAILABLE_REGIONS: errors[CONF_REGION] = "无效的区域,请从列表中选择有效的区域" person_group_id = user_input.get(CONF_PERSON_GROUP_ID, "Hass") 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调用验证""" client = None try: _LOGGER.info("开始验证腾讯云凭据") client = TencentCloudClient( user_input[CONF_SECRET_ID], user_input[CONF_SECRET_KEY], user_input.get(CONF_REGION, DEFAULT_REGION) ) await self.hass.async_add_executor_job(client.verify_credentials) _LOGGER.info("腾讯云凭据验证通过") return {"success": True} except AuthenticationError as ex: _LOGGER.error("腾讯云认证失败: %s", ex) return { "success": False, "errors": { CONF_SECRET_ID: "认证失败,请检查Secret ID", CONF_SECRET_KEY: "认证失败,请检查Secret Key", "base": "腾讯云认证失败,请检查Secret ID和Secret Key是否正确" } } except NetworkError as ex: _LOGGER.error("腾讯云网络错误: %s", ex) return { "success": False, "errors": { CONF_REGION: "网络错误,请检查区域设置和网络连接", "base": "连接腾讯云失败,请检查网络连接和区域设置" } } except RateLimitError as ex: _LOGGER.error("腾讯云速率限制错误: %s", ex) return { "success": False, "errors": { "base": "腾讯云API调用频率过高,请稍后再试" } } except Exception as ex: _LOGGER.error("腾讯云凭据验证失败: %s", ex) return { "success": False, "errors": { "base": f"腾讯云API验证失败: {str(ex)}" } } finally: if client is not None: await self.hass.async_add_executor_job(client.close) @staticmethod @callback def async_get_options_flow(config_entry): """获取选项流程""" return TencentFaceRecognitionOptionsFlow(config_entry) class TencentFaceRecognitionOptionsFlow(config_entries.OptionsFlow): """腾讯云人脸识别选项流程""" AVAILABLE_REGIONS = TencentFaceRecognitionConfigFlow.AVAILABLE_REGIONS def __init__(self, config_entry): """初始化选项流程""" self._entry_id = config_entry.entry_id self._title = config_entry.title self._data = dict(config_entry.data) self._options = dict(config_entry.options) async def async_step_init(self, user_input=None): """管理选项流程""" 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({ vol.Required("action"): selector({ "select": { "options": [ {"label": "配置设置", "value": "config"}, {"label": "测试连接", "value": "test_connection"}, {"label": "重新认证", "value": "reauth"}, {"label": "人员管理", "value": "manage"} ] } }) }), description_placeholders={ "description": "请选择要管理的项目" } ) 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 = {} if user_input is not None: region = user_input.get(CONF_REGION) 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: client = None try: 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("配置验证成功") 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] = "认证失败,请检查区域设置" errors["base"] = "腾讯云认证失败,请检查区域设置" except NetworkError as ex: _LOGGER.error("腾讯云网络错误: %s", ex) errors[CONF_REGION] = "网络错误,请检查区域设置" errors["base"] = "连接腾讯云失败,请检查网络连接和区域设置" except Exception as ex: _LOGGER.error("配置验证失败: %s", ex) errors["base"] = f"配置验证失败: {str(ex)}" finally: if client is not None: await self.hass.async_add_executor_job(client.close) options = { vol.Optional( CONF_REGION, default=self._options.get(CONF_REGION, self._data.get(CONF_REGION, DEFAULT_REGION)) ): selector({ "select": { "options": [{"label": region, "value": region} for region in self.AVAILABLE_REGIONS], "mode": "dropdown" } }), vol.Optional( CONF_PERSON_GROUP_ID, default=self._options.get(CONF_PERSON_GROUP_ID, self._data.get(CONF_PERSON_GROUP_ID, "Hass")) ): str, } return self.async_show_form( step_id="config", 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: client = None try: client = TencentCloudClient( self._data.get(CONF_SECRET_ID), self._data.get(CONF_SECRET_KEY), self._data.get(CONF_REGION, DEFAULT_REGION) ) success = await self.hass.async_add_executor_job( client.test_api_connection, 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" _LOGGER.error("连接测试认证失败: %s", ex) except NetworkError as ex: connection_status = f"网络错误: {str(ex)}" errors["base"] = "网络错误,请检查网络连接和区域设置" _LOGGER.error("连接测试网络失败: %s", ex) except Exception as ex: connection_status = f"测试失败: {str(ex)}" errors["base"] = f"连接测试失败: {str(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="test_connection", data_schema=vol.Schema({}), errors=errors, description_placeholders={ "connection_status": connection_status } ) async def async_step_reauth(self, user_input=None): """重新认证步骤""" errors = {} if user_input is not None: temp_errors = {} self._validate_input_format(user_input, temp_errors) if temp_errors: errors = temp_errors return self.async_show_form( step_id="reauth", data_schema=vol.Schema({ vol.Required(CONF_SECRET_ID, default=self._data.get(CONF_SECRET_ID, "")): str, vol.Required(CONF_SECRET_KEY, default=self._data.get(CONF_SECRET_KEY, "")): str, }), errors=errors, ) new_client = None try: 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) 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" except NetworkError as ex: _LOGGER.error("重新认证网络错误: %s", ex) errors["base"] = "网络错误,请检查网络连接和区域设置" except Exception as ex: _LOGGER.error("重新认证失败: %s", ex) errors["base"] = f"重新认证失败: {str(ex)}" finally: if new_client is not None: await self.hass.async_add_executor_job(new_client.close) return self.async_show_form( step_id="reauth", data_schema=vol.Schema({ vol.Required(CONF_SECRET_ID, default=self._data.get(CONF_SECRET_ID, "")): str, vol.Required(CONF_SECRET_KEY, default=self._data.get(CONF_SECRET_KEY, "")): str, }), errors=errors, description_placeholders={ "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 = user_input.get(CONF_SECRET_ID, "") if not TencentFaceRecognitionConfigFlow.SECRET_ID_REGEX.match(secret_id): errors[CONF_SECRET_ID] = "Secret ID格式不正确,应以AKID开头,后跟36个以上字符" secret_key = user_input.get(CONF_SECRET_KEY, "") if not TencentFaceRecognitionConfigFlow.SECRET_KEY_REGEX.match(secret_key): errors[CONF_SECRET_KEY] = "Secret Key格式不正确,应为20个以上字符"