Skip to content

Commit 5b2f4a9

Browse files
committed
fix: improve intent list syncing and Assist fallback handling
1 parent 6464568 commit 5b2f4a9

5 files changed

Lines changed: 196 additions & 11 deletions

File tree

custom_components/ai_hub/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: AIHubConfigEntry) -> boo
155155
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
156156
_LOGGER.debug("Platforms setup completed")
157157

158+
# Sync auto-generated intent lists before loading local intent config
159+
from .intents.loader import async_sync_intent_lists
160+
await async_sync_intent_lists(hass)
161+
158162
# Set up intent handlers
159163
from .intents import async_setup_intents
160164
await async_setup_intents(hass)

custom_components/ai_hub/conversation.py

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -154,27 +154,44 @@ async def _async_handle_message(
154154
response_type = ha_response.response_type
155155
_LOGGER.debug("HA intents response_type: %s", response_type)
156156

157+
plain_speech = None
158+
if ha_response.speech and ha_response.speech.get("plain"):
159+
plain_speech = ha_response.speech["plain"].get("speech")
160+
157161
has_error = hasattr(ha_response, "error") and ha_response.error
158162
is_error_type = response_type == intent.IntentResponseType.ERROR
159163
is_no_match = (
160164
response_type == intent.IntentResponseType.NO_INTENT_MATCHED
161165
if hasattr(intent.IntentResponseType, "NO_INTENT_MATCHED")
162166
else False
163167
)
164-
response_has_content = bool(ha_response.speech and ha_response.speech.get("plain"))
168+
response_has_content = bool(plain_speech)
169+
normalized_speech = plain_speech.strip() if isinstance(plain_speech, str) else ""
170+
is_truncated_query_answer = (
171+
response_type == intent.IntentResponseType.QUERY_ANSWER
172+
and isinstance(plain_speech, str)
173+
and len(normalized_speech) <= 3
174+
)
165175

166-
if not has_error and not is_error_type and not is_no_match and response_has_content:
176+
if (
177+
not has_error
178+
and not is_error_type
179+
and not is_no_match
180+
and response_has_content
181+
and not is_truncated_query_answer
182+
):
167183
_LOGGER.info("HA 内置意图处理成功: %s, type: %s", user_input.text, response_type)
168184
return conversation.ConversationResult(
169185
response=ha_response,
170186
conversation_id=chat_log.conversation_id,
171187
)
172188

173189
_LOGGER.debug(
174-
"HA 内置意图未匹配或返回错误(has_error=%s, is_error_type=%s, is_no_match=%s),交给 LLM 处理",
190+
"HA 内置意图未匹配、返回错误或结果异常(has_error=%s, is_error_type=%s, is_no_match=%s, is_truncated_query_answer=%s),交给 LLM 处理",
175191
has_error,
176192
is_error_type,
177193
is_no_match,
194+
is_truncated_query_answer,
178195
)
179196

180197
except Exception as e:

custom_components/ai_hub/intents/config/lists.yaml

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,14 +71,44 @@ lists:
7171
- "电视"
7272
- "电视机"
7373
- "智能电视"
74+
- "大电视"
75+
- "小电视"
76+
- "电视屏幕"
7477
- "机顶盒"
7578
- "电视盒子"
7679
- "音箱"
7780
- "音响"
7881
- "扬声器"
82+
- "喇叭"
83+
- "功放机"
7984
- "低音炮"
8085
- "功放"
8186
- "播放器"
87+
- "媒体播放器"
88+
- "投影仪"
89+
- "投影"
90+
- "点歌机"
91+
- "背景音乐"
92+
93+
media_player_names:
94+
values:
95+
- "电视"
96+
- "电视机"
97+
- "智能电视"
98+
- "大电视"
99+
- "小电视"
100+
- "电视屏幕"
101+
- "机顶盒"
102+
- "电视盒子"
103+
- "音箱"
104+
- "音响"
105+
- "扬声器"
106+
- "喇叭"
107+
- "功放"
108+
- "功放机"
109+
- "低音炮"
110+
- "播放器"
111+
- "媒体播放器"
82112
- "投影仪"
83113
- "投影"
84114
- "点歌机"
@@ -89,6 +119,7 @@ lists:
89119
values:
90120
- "窗帘"
91121
- "窗纱"
122+
- "帘子"
92123
- "百叶窗"
93124
- "卷帘"
94125
- "罗马帘"
@@ -108,6 +139,7 @@ lists:
108139
- "电闸"
109140
- "空开"
110141
- "插座"
142+
- "智能插座"
111143
- "插排"
112144
- "排插"
113145
- "电源"
@@ -175,9 +207,11 @@ lists:
175207
- "传感器"
176208
- "温度传感器"
177209
- "湿度传感器"
210+
- "温湿度传感器"
178211
- "光照传感器"
179212
- "人体传感器"
180213
- "运动传感器"
214+
- "人体感应器"
181215
- "门窗传感器"
182216
- "烟雾报警器"
183217
- "燃气报警器"
@@ -201,16 +235,23 @@ lists:
201235
area_names:
202236
values:
203237
- "客厅"
238+
- "客餐厅"
239+
- "客厅餐厅"
204240
- "卧室"
205241
- "主卧"
206242
- "次卧"
243+
- "儿童房"
207244
- "厨房"
208245
- "餐厅"
209246
- "书房"
247+
- "电脑房"
210248
- "卫生间"
211249
- "浴室"
212250
- "洗手间"
213251
- "厕所"
252+
- "主厕所"
253+
- "客卫"
254+
- "主卫"
214255
- "阳台"
215256
- "露台"
216257
- "玄关"

custom_components/ai_hub/intents/handlers.py

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -176,19 +176,25 @@ def _parse_device_and_area(self, text_lower: str, global_config: dict) -> tuple:
176176
'climate': 'climate_names',
177177
'fan': 'fan_names',
178178
'cover': 'cover_names',
179-
'media_player': 'media_player_names',
179+
'media_player': ['media_player_names', 'media_names'],
180180
'lock': 'lock_names',
181181
'vacuum': 'vacuum_names',
182182
'valve': 'valve_names'
183183
}
184184

185-
for domain, list_name in domain_mapping.items():
186-
keywords_list = lists_config.get(list_name, {}).get('values', [])
187-
if keywords_list:
188-
for keyword in keywords_list:
189-
if keyword in text_lower:
190-
device_types.append(domain)
191-
break
185+
for domain, list_names in domain_mapping.items():
186+
if isinstance(list_names, str):
187+
list_names = [list_names]
188+
189+
for list_name in list_names:
190+
keywords_list = lists_config.get(list_name, {}).get('values', [])
191+
if keywords_list:
192+
for keyword in keywords_list:
193+
if keyword in text_lower:
194+
device_types.append(domain)
195+
break
196+
if domain in device_types:
197+
break
192198
else:
193199
for keyword, domain in device_type_keywords.items():
194200
if keyword in text_lower:

custom_components/ai_hub/intents/loader.py

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,13 @@
44

55
import asyncio
66
import logging
7+
from collections import defaultdict
78
from pathlib import Path
89
from typing import Any, Dict
910

11+
from homeassistant.components.homeassistant import exposed_entities
1012
from homeassistant.core import HomeAssistant
13+
from homeassistant.helpers import area_registry as ar, entity_registry as er
1114

1215
_LOGGER = logging.getLogger(__name__)
1316

@@ -19,11 +22,29 @@
1922
CONFIG_FILES = [
2023
"base.yaml",
2124
"lists.yaml",
25+
"auto_lists.yaml",
2226
"expansion.yaml",
2327
"intents.yaml",
2428
"local_control.yaml",
2529
]
2630

31+
DOMAIN_TO_LIST = {
32+
"light": "light_names",
33+
"climate": "climate_names",
34+
"fan": "fan_names",
35+
"media_player": "media_player_names",
36+
"cover": "cover_names",
37+
"switch": "switch_names",
38+
"vacuum": "vacuum_names",
39+
"camera": "camera_names",
40+
"lock": "lock_names",
41+
"valve": "valve_names",
42+
"sensor": "sensor_names",
43+
"device_tracker": "tracker_names",
44+
}
45+
46+
AUTO_LISTS_PATH = Path(__file__).parent / "config" / "auto_lists.yaml"
47+
2748

2849
def _get_fallback_config() -> dict[str, Any]:
2950
"""获取备用配置(仅当配置读取失败时使用)."""
@@ -51,6 +72,102 @@ def _deep_merge(base: Dict, override: Dict) -> Dict:
5172
return result
5273

5374

75+
def _normalize_list_value(value: str) -> str:
76+
"""Normalize values for list deduplication."""
77+
return " ".join(value.strip().split()).casefold()
78+
79+
80+
def _append_unique_value(target: list[str], seen: set[str], value: str | None) -> None:
81+
"""Append value if non-empty and unique."""
82+
if not value:
83+
return
84+
85+
value = value.strip()
86+
if not value:
87+
return
88+
89+
normalized = _normalize_list_value(value)
90+
if normalized in seen:
91+
return
92+
93+
seen.add(normalized)
94+
target.append(value)
95+
96+
97+
def _build_auto_lists_config(hass: HomeAssistant) -> dict[str, Any]:
98+
"""Build auto-generated lists from HA areas and entities."""
99+
area_reg = ar.async_get(hass)
100+
entity_reg = er.async_get(hass)
101+
102+
lists: dict[str, list[str]] = defaultdict(list)
103+
seen: dict[str, set[str]] = defaultdict(set)
104+
105+
for area in area_reg.areas.values():
106+
_append_unique_value(lists["area_names"], seen["area_names"], area.name)
107+
108+
for entity_id in hass.states.async_entity_ids():
109+
state = hass.states.get(entity_id)
110+
if state is None:
111+
continue
112+
113+
if not exposed_entities.async_should_expose(hass, "conversation", entity_id):
114+
continue
115+
116+
domain = entity_id.split(".", 1)[0]
117+
list_name = DOMAIN_TO_LIST.get(domain)
118+
if list_name is None:
119+
continue
120+
121+
_append_unique_value(lists[list_name], seen[list_name], state.name)
122+
123+
registry_entry = entity_reg.async_get(entity_id)
124+
if registry_entry is not None:
125+
_append_unique_value(lists[list_name], seen[list_name], registry_entry.original_name)
126+
_append_unique_value(lists[list_name], seen[list_name], registry_entry.name)
127+
128+
return {
129+
"lists": {
130+
key: {"values": values}
131+
for key, values in sorted(lists.items())
132+
if values
133+
}
134+
}
135+
136+
137+
def _sync_auto_lists_config_sync(hass: HomeAssistant) -> bool:
138+
"""Write auto-generated list config if content changed."""
139+
import yaml # type: ignore[import]
140+
141+
data = _build_auto_lists_config(hass)
142+
serialized = (
143+
"# This file is auto-generated by AI Hub.\n"
144+
"# It supplements lists.yaml with Home Assistant areas and entity names.\n"
145+
"# Manual edits may be overwritten.\n\n"
146+
+ yaml.safe_dump(data, allow_unicode=True, sort_keys=False)
147+
)
148+
149+
if AUTO_LISTS_PATH.exists():
150+
current = AUTO_LISTS_PATH.read_text(encoding="utf-8")
151+
if current == serialized:
152+
return False
153+
154+
AUTO_LISTS_PATH.parent.mkdir(parents=True, exist_ok=True)
155+
AUTO_LISTS_PATH.write_text(serialized, encoding="utf-8")
156+
return True
157+
158+
159+
async def async_sync_intent_lists(hass: HomeAssistant) -> bool:
160+
"""Sync HA areas and entity names into auto-generated lists.yaml overlay."""
161+
global _INTENTS_CONFIG, _CONFIG_LOADED
162+
163+
changed = await hass.async_add_executor_job(_sync_auto_lists_config_sync, hass)
164+
if changed:
165+
_INTENTS_CONFIG = None
166+
_CONFIG_LOADED = False
167+
_LOGGER.debug("Auto intent lists updated from Home Assistant registry")
168+
return changed
169+
170+
54171
def _load_intents_config_sync() -> dict[str, Any]:
55172
"""同步加载配置 - 支持多文件合并."""
56173
import yaml # type: ignore[import]

0 commit comments

Comments
 (0)