Skip to content

Commit 7f8a4c0

Browse files
committed
refactor: 符合 Home Assistant 集成规范
P0 修复: - 实体类命名规范化 (AIHubTTSEntity, AIHubSTTEntity, AIHubConversationAgent, AIHubHealthCheckSensor) - 服务合并 (tts_speech + tts_stream → tts_say) - 补充核心测试 (test_conversation, test_config_flow, test_services) P1 修复: - 错误消息国际化 (使用字符串键) - 优化实体唯一标识符 (使用 DOMAIN 前缀) - 添加 HA 新特性支持 (TTS quality 参数, STT language 检测) P2 修复: - 优化实体类别使用 (Button 非诊断类别, Sensor device_class) - 代码注释国际化 - PEP8 和 ruff 检查通过 - 删除不必要的 pyproject.toml
1 parent 8ae3f44 commit 7f8a4c0

16 files changed

Lines changed: 1003 additions & 136 deletions

custom_components/ai_hub/button/__init__.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88

99
from homeassistant.components.button import ButtonEntity
1010
from homeassistant.config_entries import ConfigEntry
11-
from homeassistant.const import EntityCategory
1211
from homeassistant.core import HomeAssistant
1312
from homeassistant.helpers import config_entry_flow
1413
from homeassistant.helpers import device_registry as dr
@@ -70,7 +69,7 @@ class _AIHubServiceButton(ButtonEntity):
7069

7170
_attr_has_entity_name = False
7271
_attr_should_poll = False
73-
_attr_entity_category = EntityCategory.DIAGNOSTIC
72+
_attr_entity_category = None
7473

7574
def __init__(
7675
self,
@@ -88,7 +87,7 @@ def __init__(
8887
config = _BUTTON_CONFIGS[button_type]
8988

9089
# Set attributes from configuration
91-
self._attr_unique_id = f"{subentry.subentry_id}_{config['unique_id_suffix']}"
90+
self._attr_unique_id = f"{DOMAIN}_{subentry.subentry_id}_{config['unique_id_suffix']}"
9291
self._attr_name = config["name"]
9392
self._attr_icon = config["icon"]
9493
self._service = config["service"]

custom_components/ai_hub/config_flow.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -117,10 +117,10 @@ async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> None:
117117
timeout=aiohttp.ClientTimeout(total=10),
118118
) as response:
119119
if response.status == 401:
120-
raise ValueError("Invalid API key")
120+
raise ValueError("invalid_auth")
121121
if response.status != 200:
122-
error_text = await response.text()
123-
raise Exception(f"API test failed: {error_text}")
122+
await response.text() # Read response but don't use it
123+
raise ValueError("cannot_connect")
124124

125125

126126
class AIHubConfigFlow(ConfigFlow, domain=DOMAIN):

custom_components/ai_hub/const.py

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,22 @@
11
"""Constants for the AI Hub integration.
22
33
This module contains all constants organized by category:
4-
- Domain and API URLs
5-
- Timeouts and Retry Configuration
6-
- Cache and Audio Limits
7-
- Configuration Keys
8-
- Model Lists
9-
- Default Values and Recommended Options
10-
- Services
4+
5+
SECTION 1: Domain and API URLs
6+
SECTION 2: Timeouts and Retry Configuration
7+
SECTION 3: Cache and Audio Limits
8+
SECTION 4: Configuration Keys
9+
SECTION 5: Default Values and Recommended Options
10+
SECTION 6: Model Lists
11+
SECTION 7: Error Messages
12+
SECTION 8: Services
13+
14+
For better organization, consider extracting these to separate modules:
15+
- const/urls.py - API endpoints
16+
- const/timeouts.py - Timeout configurations
17+
- const/models.py - Model lists
18+
- const/defaults.py - Default values
19+
- const/errors.py - Error messages
1120
"""
1221

1322
from __future__ import annotations
@@ -362,8 +371,7 @@ def get_localized_name(hass: HomeAssistant, zh_name: str, en_name: str) -> str:
362371
SERVICES: Final = {
363372
"generate_image": "generate_image",
364373
"analyze_image": "analyze_image",
365-
"tts_speech": "tts_speech",
366-
"tts_stream": "tts_stream",
374+
"tts_say": "tts_say",
367375
"stt_transcribe": "stt_transcribe",
368376
"send_wechat_message": "send_wechat_message",
369377
"translate_components": "translate_components",
@@ -373,8 +381,10 @@ def get_localized_name(hass: HomeAssistant, zh_name: str, en_name: str) -> str:
373381
# Legacy service constants
374382
SERVICE_GENERATE_IMAGE: Final = SERVICES["generate_image"]
375383
SERVICE_ANALYZE_IMAGE: Final = SERVICES["analyze_image"]
376-
SERVICE_TTS_SPEECH: Final = SERVICES["tts_speech"]
377-
SERVICE_TTS_STREAM: Final = SERVICES["tts_stream"]
384+
SERVICE_TTS_SAY: Final = SERVICES["tts_say"]
385+
# Deprecated: use SERVICE_TTS_SAY instead
386+
SERVICE_TTS_SPEECH: Final = "tts_speech"
387+
SERVICE_TTS_STREAM: Final = "tts_stream"
378388
SERVICE_STT_TRANSCRIBE: Final = SERVICES["stt_transcribe"]
379389
SERVICE_SEND_WECHAT_MESSAGE: Final = SERVICES["send_wechat_message"]
380390
SERVICE_TRANSLATE_COMPONENTS: Final = SERVICES["translate_components"]

custom_components/ai_hub/conversation.py

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,16 @@
1-
"""Conversation support for AI Hub."""
1+
"""Conversation agent support for AI Hub integration.
2+
3+
This module implements the ConversationEntity for AI-powered
4+
dialogue interactions, supporting:
5+
- Streaming responses
6+
- Tool calling (Home Assistant control)
7+
- Image understanding (vision models)
8+
- Context-aware conversations
9+
- Three-tier intent processing (local → HA built-in → LLM)"""
210

311
from __future__ import annotations
412

513
import logging
6-
import time
714
from typing import Any, Literal
815

916
from homeassistant.components import conversation
@@ -39,13 +46,13 @@ async def async_setup_entry(
3946
continue
4047

4148
async_add_entities(
42-
[AIHubConversationEntity(config_entry, subentry)],
49+
[AIHubConversationAgent(config_entry, subentry)],
4350
config_subentry_id=subentry.subentry_id,
4451
)
45-
_LOGGER.debug("Created conversation entity for subentry: %s", subentry.subentry_id)
52+
_LOGGER.debug("Created conversation agent for subentry: %s", subentry.subentry_id)
4653

4754

48-
class AIHubConversationEntity(
55+
class AIHubConversationAgent(
4956
conversation.ConversationEntity,
5057
conversation.AbstractConversationAgent,
5158
AIHubBaseLLMEntity,
@@ -162,8 +169,10 @@ async def _async_handle_message(
162169
_LOGGER.info("HA 内置意图处理成功: %s, type: %s", user_input.text, response_type)
163170
return result
164171
else:
165-
_LOGGER.debug("HA 内置意图未匹配(has_error=%s, is_no_match=%s),交给 LLM 处理",
166-
has_error, is_no_match)
172+
_LOGGER.debug(
173+
"HA 内置意图未匹配(has_error=%s, is_no_match=%s),交给 LLM 处理",
174+
has_error, is_no_match
175+
)
167176
else:
168177
_LOGGER.warning("HA 默认 agent 不可用")
169178

@@ -238,9 +247,11 @@ async def _async_handle_message(
238247
original_content = str(last_content.content)
239248
filtered_content = filter_markdown_content(original_content)
240249
if filtered_content != original_content:
241-
_LOGGER.debug("Filtered markdown from chat_log before returning: '%s' -> '%s'",
242-
original_content[:50] if len(original_content) > 50 else original_content,
243-
filtered_content[:50] if len(filtered_content) > 50 else filtered_content)
250+
_LOGGER.debug(
251+
"Filtered markdown from chat_log before returning: '%s' -> '%s'",
252+
original_content[:50] if len(original_content) > 50 else original_content,
253+
filtered_content[:50] if len(filtered_content) > 50 else filtered_content
254+
)
244255
last_content.content = filtered_content
245256

246257
# Return result from chat log

custom_components/ai_hub/entity.py

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -607,7 +607,11 @@ def _convert_assistant_message(
607607
"type": "function",
608608
"function": {
609609
"name": str(tool_call.tool_name) if tool_call.tool_name else "",
610-
"arguments": json.dumps(tool_call.tool_args, ensure_ascii=False) if tool_call.tool_args else "{}",
610+
"arguments": (
611+
json.dumps(tool_call.tool_args, ensure_ascii=False)
612+
if tool_call.tool_args
613+
else "{}"
614+
),
611615
},
612616
})
613617
message["tool_calls"] = tool_calls_list
@@ -632,7 +636,11 @@ def _convert_tool_message(
632636
return {
633637
"role": "tool",
634638
"tool_call_id": tool_call_id,
635-
"content": json.dumps(content.tool_result, ensure_ascii=False, default=str) if content.tool_result is not None else "{}",
639+
"content": (
640+
json.dumps(content.tool_result, ensure_ascii=False, default=str)
641+
if content.tool_result is not None
642+
else "{}"
643+
),
636644
}
637645

638646
def _format_tool(
@@ -748,7 +756,12 @@ async def _transform_stream(
748756
}
749757

750758
# Update tool call data - only update id if it's a valid non-empty string
751-
if "id" in tc_delta and tc_delta["id"] and isinstance(tc_delta["id"], str) and tc_delta["id"].strip():
759+
if (
760+
"id" in tc_delta
761+
and tc_delta["id"]
762+
and isinstance(tc_delta["id"], str)
763+
and tc_delta["id"].strip()
764+
):
752765
tool_call_buffer[index]["id"] = tc_delta["id"]
753766
if "function" in tc_delta:
754767
func = tc_delta["function"]

custom_components/ai_hub/intents/loader.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import asyncio
66
import logging
77
from pathlib import Path
8-
from typing import Any
8+
from typing import Any, Dict
99

1010
from homeassistant.core import HomeAssistant
1111

custom_components/ai_hub/sensor.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ async def async_setup_entry(
6666
entities = []
6767

6868
# Main integration health sensor (always added)
69-
entities.append(AIHubHealthSensor(hass, entry))
69+
entities.append(AIHubHealthCheckSensor(hass, entry))
7070

7171
# Edge TTS health sensor (always available, no API key needed)
7272
entities.append(EdgeTTSHealthSensor(hass, entry))
@@ -81,11 +81,12 @@ async def async_setup_entry(
8181
async_add_entities(entities)
8282

8383

84-
class AIHubHealthSensor(SensorEntity):
84+
class AIHubHealthCheckSensor(SensorEntity):
8585
"""Sensor for overall AI Hub health status."""
8686

8787
_attr_has_entity_name = True
8888
_attr_entity_category = EntityCategory.DIAGNOSTIC
89+
_attr_device_class = SensorDeviceClass.ENUM
8990
_attr_icon = "mdi:heart-pulse"
9091
_attr_should_poll = True
9192

@@ -97,7 +98,7 @@ def __init__(
9798
"""Initialize the health sensor."""
9899
self.hass = hass
99100
self._entry = entry
100-
self._attr_unique_id = f"{entry.entry_id}_health"
101+
self._attr_unique_id = f"{DOMAIN}_{entry.entry_id}_health_sensor"
101102
self._attr_name = "Health Status"
102103
self._attr_device_info = _get_diagnostic_device_info(entry)
103104

@@ -260,7 +261,7 @@ def __init__(
260261
"""Initialize the sensor."""
261262
self.hass = hass
262263
self._entry = entry
263-
self._attr_unique_id = f"{entry.entry_id}_{self._name_suffix}_latency"
264+
self._attr_unique_id = f"{DOMAIN}_{entry.entry_id}_{self._name_suffix}_latency"
264265
self._attr_name = f"{self._name_suffix.replace('_', ' ').title()} Latency"
265266
self._attr_device_info = _get_diagnostic_device_info(entry)
266267

@@ -328,6 +329,6 @@ class EdgeTTSHealthSensor(_BaseHealthSensor):
328329
class BemfaHealthSensor(_BaseHealthSensor):
329330
"""Sensor for Bemfa API health and latency."""
330331

331-
_check_url = "https://apis.bemfa.com"
332+
_check_url = "https://apis.bemfa.com/vb/wechat/v1/wechatAlertJson"
332333
_name_suffix = "bemfa"
333334
_attr_icon = "mdi:message-text"

custom_components/ai_hub/services.py

Lines changed: 14 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from homeassistant.core import HomeAssistant, ServiceCall
99

1010
from .const import (
11+
CONF_API_KEY,
1112
CONF_BEMFA_UID,
1213
CONF_CHAT_MODEL,
1314
CONF_CHAT_URL,
@@ -21,8 +22,7 @@
2122
SERVICE_STT_TRANSCRIBE,
2223
SERVICE_TRANSLATE_BLUEPRINTS,
2324
SERVICE_TRANSLATE_COMPONENTS,
24-
SERVICE_TTS_SPEECH,
25-
SERVICE_TTS_STREAM,
25+
SERVICE_TTS_SAY,
2626
)
2727
from .services_lib import (
2828
BLUEPRINTS_TRANSLATION_SCHEMA,
@@ -32,7 +32,6 @@
3232
STT_SCHEMA,
3333
TRANSLATION_SCHEMA,
3434
TTS_SCHEMA,
35-
TTS_STREAM_SCHEMA,
3635
WECHAT_SCHEMA,
3736
async_translate_all_blueprints,
3837
async_translate_all_components,
@@ -111,15 +110,16 @@ async def _handle_generate_image(call: ServiceCall) -> dict:
111110
return {"success": False, "error": "API密钥未配置"}
112111
return await handle_generate_image(hass, call, effective_key, image_url)
113112

114-
# ========== TTS 语音合成服务 ==========
115-
async def _handle_tts_speech(call: ServiceCall) -> dict:
116-
if not has_api_key():
117-
return {"success": False, "error": "API密钥未配置"}
118-
return await handle_tts_speech(hass, call, api_key)
119-
120-
# ========== TTS 流式语音服务 ==========
121-
async def _handle_tts_stream(call: ServiceCall) -> dict:
122-
return await handle_tts_stream(hass, call)
113+
# ========== TTS 语音合成服务(统一) ==========
114+
async def _handle_tts_say(call: ServiceCall) -> dict:
115+
"""Handle TTS service with optional streaming support."""
116+
stream = call.data.get("stream", False)
117+
if stream:
118+
return await handle_tts_stream(hass, call)
119+
else:
120+
if not has_api_key():
121+
return {"success": False, "error": "API密钥未配置"}
122+
return await handle_tts_speech(hass, call, api_key)
123123

124124
# ========== STT 语音转文字服务 ==========
125125
async def _handle_stt_transcribe(call: ServiceCall) -> dict:
@@ -200,15 +200,10 @@ async def _handle_translate_blueprints(call: ServiceCall) -> dict:
200200
)
201201

202202
hass.services.async_register(
203-
DOMAIN, SERVICE_TTS_SPEECH, _handle_tts_speech,
203+
DOMAIN, SERVICE_TTS_SAY, _handle_tts_say,
204204
schema=vol.Schema(TTS_SCHEMA), supports_response=True
205205
)
206206

207-
hass.services.async_register(
208-
DOMAIN, SERVICE_TTS_STREAM, _handle_tts_stream,
209-
schema=vol.Schema(TTS_STREAM_SCHEMA), supports_response=True
210-
)
211-
212207
hass.services.async_register(
213208
DOMAIN, SERVICE_STT_TRANSCRIBE, _handle_stt_transcribe,
214209
schema=vol.Schema(STT_SCHEMA), supports_response=True
@@ -240,8 +235,7 @@ async def async_unload_services(hass: HomeAssistant) -> None:
240235
"""
241236
hass.services.async_remove(DOMAIN, SERVICE_ANALYZE_IMAGE)
242237
hass.services.async_remove(DOMAIN, SERVICE_GENERATE_IMAGE)
243-
hass.services.async_remove(DOMAIN, SERVICE_TTS_SPEECH)
244-
hass.services.async_remove(DOMAIN, SERVICE_TTS_STREAM)
238+
hass.services.async_remove(DOMAIN, SERVICE_TTS_SAY)
245239
hass.services.async_remove(DOMAIN, SERVICE_STT_TRANSCRIBE)
246240
hass.services.async_remove(DOMAIN, SERVICE_SEND_WECHAT_MESSAGE)
247241
hass.services.async_remove(DOMAIN, SERVICE_TRANSLATE_COMPONENTS)

0 commit comments

Comments
 (0)