Compare commits

..
3 Commits
Author SHA1 Message Date
wengzh 4f6e9ec6b1 Merge pull request #3544 from xinnan-tech/py-hass-devices
Py hass devices
2026-11-05 16:37:11 +08:00
Sakura-RanChen 9f5f520576 fix: 修复ha读取异常 2026-11-05 15:36:04 +08:00
Bao1573 70f1a7bfd8 fix(hass): devices 写成列表时也能拼进提示词 (#3541)
config.yaml 示例和手机端智控台给的 devices 是列表,append_devices_to_prompt 却按字符串直接拼,每次连接都报 can only concatenate str (not "list") to str,设备清单进不了提示词,模型只能猜 entity_id。列表按每项一行拼,字符串原样用,其它当空。附测试。
2026-11-05 11:50:39 +08:00
4 changed files with 142 additions and 38 deletions

No files matched your search

@@ -5,6 +5,7 @@ if TYPE_CHECKING:
from core.connection import ConnectionHandler
from ..base import IntentProviderBase
from plugins_func.functions.play_music import initialize_music_handler
from plugins_func.functions.hass_init import _devices_text, _hass_plugin_config
from config.logger import setup_logging
from core.utils.util import get_system_error_response
import re
@@ -181,16 +182,11 @@ class IntentProvider(IntentProviderBase):
music_file_names = music_config["music_file_names"]
prompt_music = f"{self.promot}\n<musicNames>{music_file_names}\n</musicNames>"
home_assistant_cfg = conn.config["plugins"].get("home_assistant")
if home_assistant_cfg:
devices = home_assistant_cfg.get("devices", [])
else:
devices = []
if len(devices) > 0:
plugin_config = _hass_plugin_config(conn.config.get("plugins", {}))
devices_text = _devices_text(plugin_config.get("devices", ""))
if devices_text:
hass_prompt = "\n下面是我家智能设备列表(位置,设备名,entity_id),可以通过homeassistant控制\n"
for device in devices:
hass_prompt += device + "\n"
prompt_music += hass_prompt
prompt_music += hass_prompt + devices_text + "\n"
logger.bind(tag=TAG).debug(f"User prompt: {prompt_music}")
@@ -1,27 +1,53 @@
from config.logger import setup_logging
from core.utils.util import check_model_key
from plugins_func.register import module_func_map
TAG = __name__
logger = setup_logging()
def _expand_module_names(funcs):
"""把 functions 列表里的模块名(如 hass_state)展开成具体函数名,与工具加载口径一致。"""
expanded = []
for name in funcs:
expanded.extend(module_func_map.get(name, [name]))
return expanded
def _devices_text(devices) -> str:
"""把 devices 配置转成拼进提示词的文字。
config.yaml 示例和手机端智控台给的是列表,每项一条"位置,设备名,entity_id",这里拼成每行一条;
网页端智控台和 HA 集成文档是一段字符串,原样用;其它(如 None)当空。
"""
if isinstance(devices, (list, tuple)):
return "\n".join(
str(d).strip() for d in devices if d is not None and str(d).strip()
)
return devices if isinstance(devices, str) else ""
def _hass_plugin_config(plugins_config: dict) -> dict:
"""取 HA 插件配置:优先本地 config.yaml 的 home_assistant,其次智控台下发的 hass_state。"""
for key in ("home_assistant", "hass_state"):
cfg = plugins_config.get(key)
if cfg:
return cfg
return {}
def append_devices_to_prompt(conn):
if conn.intent_type == "function_call":
funcs = conn.config["Intent"][conn.config["selected_module"]["Intent"]].get(
"functions", []
)
# 安全地获取插件配置
plugins_config = conn.config.get("plugins", {})
config_source = (
"home_assistant"
if plugins_config.get("home_assistant")
else "hass_state"
funcs = _expand_module_names(
conn.config["Intent"][conn.config["selected_module"]["Intent"]].get(
"functions", []
)
)
if "hass_get_state" in funcs or "hass_set_state" in funcs:
prompt = "\n下面是我家智能设备列表(位置,设备名,entity_id),可以通过homeassistant控制\n"
deviceStr = plugins_config.get(config_source, {}).get("devices", "")
plugin_config = _hass_plugin_config(conn.config.get("plugins", {}))
deviceStr = _devices_text(plugin_config.get("devices", ""))
conn.prompt += prompt + deviceStr + "\n"
# 更新提示词
conn.dialogue.update_system_message(conn.prompt)
@@ -32,17 +58,10 @@ def initialize_hass_handler(conn):
if not conn.load_function_plugin:
return ha_config
# 安全地获取插件配置
plugins_config = conn.config.get("plugins", {})
# 确定配置来源
config_source = (
"home_assistant" if plugins_config.get("home_assistant") else "hass_state"
)
if not plugins_config.get(config_source):
plugin_config = _hass_plugin_config(conn.config.get("plugins", {}))
if not plugin_config:
return ha_config
# 统一获取配置
plugin_config = plugins_config[config_source]
ha_config["base_url"] = plugin_config.get("base_url")
ha_config["api_key"] = plugin_config.get("api_key")
@@ -121,10 +121,13 @@ async def handle_hass_get_state(conn: "ConnectionHandler", entity_id):
+ str(response.json()["attributes"]["media_title"])
+ " "
)
volume_level = response.json()["attributes"].get("volume_level")
if isinstance(volume_level, (int, float)):
# volume_level 量程 0~1,换算成百分比(同 volume_set 的 1~100)
responsetext = responsetext + "音量是:" + f"{round(volume_level * 100)}%" + " "
if "volume_level" in response.json()["attributes"]:
responsetext = (
responsetext
+ "音量是:"
+ str(response.json()["attributes"]["volume_level"])
+ " "
)
if "color_temp_kelvin" in response.json()["attributes"]:
responsetext = (
responsetext
@@ -139,11 +142,13 @@ async def handle_hass_get_state(conn: "ConnectionHandler", entity_id):
+ str(response.json()["attributes"]["rgb_color"])
+ " "
)
brightness = response.json()["attributes"].get("brightness")
if isinstance(brightness, (int, float)):
# brightness 量程 0~255,换算成百分比(同 brightness_pct 的 1~100),开灯最低报 1%
percent = max(1, round(brightness * 100 / 255)) if brightness > 0 else 0
responsetext = responsetext + "亮度是:" + f"{percent}%" + " "
if "brightness" in response.json()["attributes"]:
responsetext = (
responsetext
+ "亮度是:"
+ str(response.json()["attributes"]["brightness"])
+ " "
)
logger.bind(tag=TAG).info(f"查询返回内容: {responsetext}")
return responsetext
else:
@@ -0,0 +1,84 @@
"""append_devices_to_prompt:devices 写成列表或一段字符串都能拼进提示词。
config.yaml 示例和手机端智控台给的是列表,以前会报
can only concatenate str (not "list") to str,设备清单进不了提示词,模型只能猜 entity_id。
"""
import pytest
import plugins_func.functions.hass_state # noqa: F401 注册 hass_state 模块,填充 module_func_map
from plugins_func.functions.hass_init import append_devices_to_prompt
HEADER = "\n下面是我家智能设备列表(位置,设备名,entity_id),可以通过homeassistant控制\n"
BASE_PROMPT = "你是小智。"
class FakeDialogue:
def __init__(self):
self.system_messages = []
def update_system_message(self, prompt):
self.system_messages.append(prompt)
class FakeConn:
def __init__(self, plugin, source="home_assistant", functions=("hass_get_state", "hass_set_state")):
self.intent_type = "function_call"
self.config = {
"selected_module": {"Intent": "function_call"},
"Intent": {"function_call": {"functions": list(functions)}},
"plugins": {source: plugin},
}
self.prompt = BASE_PROMPT
self.dialogue = FakeDialogue()
def _plugin(**extra):
return {"base_url": "http://homeassistant.local:8123", "api_key": "test-token", **extra}
def test_list_devices_one_per_line():
conn = FakeConn(_plugin(devices=[
"客厅,玩具灯,switch.toy_lamp",
" 卧室,台灯,switch.desk_lamp ",
"",
]))
append_devices_to_prompt(conn)
expected = BASE_PROMPT + HEADER + "客厅,玩具灯,switch.toy_lamp\n卧室,台灯,switch.desk_lamp\n"
assert conn.prompt == expected
assert conn.dialogue.system_messages == [expected]
def test_string_devices_kept_as_is():
# 网页端智控台和 HA 集成文档给的是一段字符串
devices = "客厅,玩具灯,switch.toy_lamp;\n卧室,台灯,switch.desk_lamp;"
conn = FakeConn(_plugin(devices=devices))
append_devices_to_prompt(conn)
assert conn.prompt == BASE_PROMPT + HEADER + devices + "\n"
def test_mobile_console_hass_state_list():
# 智控台下发的插件配置键是 hass_state;手机端把 array 字段按行切成了列表
conn = FakeConn(_plugin(devices=["客厅,玩具灯,switch.toy_lamp"]), source="hass_state")
append_devices_to_prompt(conn)
assert conn.prompt.endswith(HEADER + "客厅,玩具灯,switch.toy_lamp\n")
def test_module_name_in_functions():
# functions 里写模块名 hass_state(config.yaml 注释的写法),展开后应能匹配
conn = FakeConn(_plugin(devices=["客厅,玩具灯,switch.toy_lamp"]), functions=("hass_state",))
append_devices_to_prompt(conn)
assert conn.prompt.endswith(HEADER + "客厅,玩具灯,switch.toy_lamp\n")
@pytest.mark.parametrize("plugin", [_plugin(), _plugin(devices=None), _plugin(devices=[])])
def test_missing_or_empty_devices_does_not_raise(plugin):
conn = FakeConn(plugin)
append_devices_to_prompt(conn)
assert conn.prompt == BASE_PROMPT + HEADER + "\n"
def test_without_hass_functions_prompt_unchanged():
conn = FakeConn(_plugin(devices=["客厅,玩具灯,switch.toy_lamp"]), functions=("get_weather",))
append_devices_to_prompt(conn)
assert conn.prompt == BASE_PROMPT
assert conn.dialogue.system_messages == []