diff --git a/README.md b/README.md index 121dfbe6..e00e7345 100644 --- a/README.md +++ b/README.md @@ -352,7 +352,9 @@ VAD: ### 6、我想通过小智控制电灯、空调、远程开关机等操作 💡 -建议:在配置文件中将 `LLM` 设置为 `HomeAssistant`,通过 调用`HomeAssistant`接口实现相关控制。 +方法1:在配置文件中将 `LLM` 设置为 `HomeAssistant`,通过 调用`HomeAssistant`接口实现相关控制。 + +方法2:以工具调用的方式控制HomeAssistant设备,LLM中配置HomeAssistant的api_key、base_url,不选择HomeAssistant做LLM,但必须配置一款支持tool调用的LLM,最后在提示词prompt里以示例的格式设置需要控制的设备,音乐播放需要在homeassistant中对接好musicassistant,同时音乐有削刮出媒体信息,对应的媒体播放器配置在设备列表中即可。 ### 7、更多问题,可联系我们反馈 💬 diff --git a/main/xiaozhi-server/config.yaml b/main/xiaozhi-server/config.yaml index 21f37c5b..e9ce5e17 100644 --- a/main/xiaozhi-server/config.yaml +++ b/main/xiaozhi-server/config.yaml @@ -53,6 +53,14 @@ prompt: | 请注意,要像一个人一样说话,请不要回复表情符号、代码、和xml标签。 现在我正在和你进行语音聊天,我们开始吧。 如果用户希望结束对话,请在最后说“拜拜”或“再见”。 + #-- hass 相关function的配置格式 请将你家需要控制的设备以下列格式追加在prompt里-- + # 下面是我家智能设备,可以通过homeassistant控制 + # 房间,设备别名,设备id + # 书房,吸顶灯,light.649e3159aa36_light + # 书房,homepod,media_player.shu_fang + # 书房,灯带,light.plug_158df955a6167a + # 书房,窗帘,cover.curtain_158d000710f507 + # 使用完声音文件后删除文件(Delete the sound file when you are done using it) delete_audio: true @@ -101,6 +109,10 @@ Intent: functions: - change_role - get_weather + - play_music + #- hass_play_music + #- hass_get_state + #- hass_set_state # 插件的基础配置 plugins: @@ -469,4 +481,4 @@ manager: enabled: false ip: 0.0.0.0 port: 8002 -use_private_config: false \ No newline at end of file +use_private_config: false diff --git a/main/xiaozhi-server/core/handle/functionHandler.py b/main/xiaozhi-server/core/handle/functionHandler.py index 8d54fe8a..36ac22f8 100644 --- a/main/xiaozhi-server/core/handle/functionHandler.py +++ b/main/xiaozhi-server/core/handle/functionHandler.py @@ -47,7 +47,7 @@ class FunctionHandler: def register_nessary_functions(self): """注册必要的函数""" self.function_registry.register_function("handle_exit_intent") - self.function_registry.register_function("play_music") + #self.function_registry.register_function("play_music") self.function_registry.register_function("plugin_loader") self.function_registry.register_function("get_time") @@ -80,4 +80,4 @@ class FunctionHandler: except Exception as e: logger.bind(tag=TAG).error(f"处理function call错误: {e}") - return None \ No newline at end of file + return None diff --git a/main/xiaozhi-server/plugins_func/functions/hass_get_state.py b/main/xiaozhi-server/plugins_func/functions/hass_get_state.py new file mode 100644 index 00000000..34634622 --- /dev/null +++ b/main/xiaozhi-server/plugins_func/functions/hass_get_state.py @@ -0,0 +1,70 @@ +from plugins_func.register import register_function,ToolType, ActionResponse, Action +from config.logger import setup_logging +import asyncio +import requests + +TAG = __name__ +logger = setup_logging() + +HASS_CACHE = {} + +hass_get_state_function_desc ={ + "type": "function", + "function": { + "name": "hass_get_state", + "description": "获取homeassistant里设备的状态,包括灯光亮度,媒体播放器的音量,设备的暂停、继续操作", + "parameters": { + "type": "object", + "properties": { + "entity_id": { + "type": "string", + "description": "需要操作的设备id,homeassistant里的entity_id" + } + }, + "required": ["entity_id"] + } + } + } + + + +@register_function("hass_get_state", hass_get_state_function_desc, ToolType.SYSTEM_CTL) +def hass_get_state(conn, entity_id=''): + try: + + future = asyncio.run_coroutine_threadsafe( + handle_hass_get_state(conn, entity_id), + conn.loop + ) + ha_response = future.result() + return ActionResponse(action=Action.REQLLM, result="执行成功", response=ha_response) + except Exception as e: + logger.bind(tag=TAG).error(f"处理设置属性意图错误: {e}") +def initialize_hass_handler(conn): + config = conn.config + global HASS_CACHE + if HASS_CACHE == {}: + logger.bind(tag=TAG).info(f"实例化HASS:") + if "HomeAssistant" in config["LLM"]: + HASS_CACHE['base_url'] = config["LLM"]['HomeAssistant']['base_url'] + HASS_CACHE['api_key'] = config["LLM"]['HomeAssistant']['api_key'] + else: + logger.bind(tag=TAG).error(f"使用前请在config文件中配置: LLM.HomeAssistant.base_url LLM.HomeAssistant.api_key") +async def handle_hass_get_state(conn, entity_id): + initialize_hass_handler(conn) + global HASS_CACHE + api_key = HASS_CACHE['api_key'] + base_url = HASS_CACHE['base_url'] + url = f"{base_url}/api/states/{entity_id}" + headers = { + "Authorization": f"Bearer {api_key}", + "Content-Type": "application/json" + } + response = requests.get(url, headers=headers) + #logger.bind(tag=TAG).info(f"获取状态: url:{url},return_code:{response.status_code},{response.json()}") + if response.status_code == 200: + return response.json()['state'] + #return response.json()['attributes'] + #response.attributes + else: + return f"切换失败,错误码: {response.status_code}" diff --git a/main/xiaozhi-server/plugins_func/functions/hass_play_music.py b/main/xiaozhi-server/plugins_func/functions/hass_play_music.py new file mode 100644 index 00000000..1a4fe74f --- /dev/null +++ b/main/xiaozhi-server/plugins_func/functions/hass_play_music.py @@ -0,0 +1,85 @@ +from plugins_func.register import register_function,ToolType, ActionResponse, Action +from config.logger import setup_logging +import asyncio +import requests + +TAG = __name__ +logger = setup_logging() + +HASS_CACHE = {} + +hass_play_music_function_desc = { + "type": "function", + "function": { + "name": "hass_play_music", + "description": "用户想听音乐、有声书的时候使用,在房间的媒体播放器(media_player)里播放对应音频", + "parameters": { + "type": "object", + "properties": { + "media_content_id": { + "type": "string", + "description": "可以是音乐或有声书的专辑名称、歌曲名、演唱者,如果未指定就填random" + }, + "entity_id": { + "type": "string", + "description": "需要操作的音箱的设备id,homeassistant里的entity_id,media_player开头" + } + }, + "required": ["media_content_id", "entity_id"] + } + } + } + + +@register_function('hass_play_music', hass_play_music_function_desc, ToolType.SYSTEM_CTL) + +def hass_play_music(conn, entity_id='', media_content_id='random'): + try: + #logger.bind(tag=TAG).error(f"arguments: {arguments}") + + #entity_id = arguments["entity_id"] + #media_content_id = arguments["media_content_id"] + + #logger.bind(tag=TAG).error(f"entity_id: {entity_id}") + + + # 执行音乐播放命令 + future = asyncio.run_coroutine_threadsafe( + handle_hass_play_music(conn, entity_id, media_content_id), + conn.loop + ) + ha_response = future.result() + return ActionResponse(action=Action.RESPONSE, result="退出意图已处理", response=ha_response) + except Exception as e: + logger.bind(tag=TAG).error(f"处理音乐意图错误: {e}") + +def initialize_hass_handler(conn): + config = conn.config + global HASS_CACHE + if HASS_CACHE == {}: + logger.bind(tag=TAG).info(f"实例化HASS:") + if "HomeAssistant" in config["LLM"]: + HASS_CACHE['base_url'] = config["LLM"]['HomeAssistant']['base_url'] + HASS_CACHE['api_key'] = config["LLM"]['HomeAssistant']['api_key'] + else: + logger.bind(tag=TAG).error(f"使用前请在config文件中配置: LLM.HomeAssistant.base_url LLM.HomeAssistant.api_key") + +async def handle_hass_play_music( conn, entity_id, media_content_id): + initialize_hass_handler(conn) + global HASS_CACHE + api_key = HASS_CACHE['api_key'] + base_url = HASS_CACHE['base_url'] + url = f"{base_url}/api/services/music_assistant/play_media" + headers = { + "Authorization": f"Bearer {api_key}", + "Content-Type": "application/json" + } + data = { + "entity_id": entity_id, + "media_id": media_content_id + } + response = requests.post(url, headers=headers, json=data) + if response.status_code == 200: + return f"正在播放{media_content_id}的音乐" + else: + return f"音乐播放失败,错误码: {response.status_code}" diff --git a/main/xiaozhi-server/plugins_func/functions/hass_set_state.py b/main/xiaozhi-server/plugins_func/functions/hass_set_state.py new file mode 100644 index 00000000..da5d1a28 --- /dev/null +++ b/main/xiaozhi-server/plugins_func/functions/hass_set_state.py @@ -0,0 +1,173 @@ +from plugins_func.register import register_function,ToolType, ActionResponse, Action +from config.logger import setup_logging +import asyncio +import requests +TAG = __name__ +logger = setup_logging() + +HASS_CACHE = {} + +hass_set_state_function_desc ={ + "type": "function", + "function": { + "name": "hass_set_state", + "description": "设置homeassistant里设备的状态,包括开、关,调整灯光亮度,调整播放器的音量,设备的暂停、继续、静音操作", + "parameters": { + "type": "object", + "properties": { + "state": { + "type": "object", + "properties": { + "type":{ + "type":"string", + "description":"需要操作的动作,打开设备:turn_on,关闭设备:turn_off,增加亮度:brightness_up,降低亮度:brightness_down,设置亮度:brightness_value,增加>音量:,volume_up降低音量:volume_down,设置音量:volume_set,设备暂停:pause,设备继续:continue,静音/取消静音:volume_mute" + }, + "input":{ + "type":"int", + "description": "只有在设置音量,设置亮度时候才需要,有效值为1-100,对应音量和亮度的1%-100%" + }, + "is_muted":{ + "type":"string", + "description": "只有在设置静音操作时才需要,设置静音的时候该值为true,取消静音时该值为false" + } + }, + "required": ["type"] + }, + "entity_id": { + "type": "string", + "description": "需要操作的设备id,homeassistant里的entity_id" + } + }, + "required": ["state", "entity_id"] + } + } + } + + + +@register_function('hass_set_state', hass_set_state_function_desc, ToolType.SYSTEM_CTL) +def hass_set_state(conn, entity_id='', state={}): + try: + + future = asyncio.run_coroutine_threadsafe( + handle_hass_set_state(conn, entity_id, state), + conn.loop + ) + ha_response = future.result() + return ActionResponse(action=Action.REQLLM, result="执行成功", response=ha_response) + except Exception as e: + logger.bind(tag=TAG).error(f"处理设置属性意图错误: {e}") +def initialize_hass_handler(conn): + config = conn.config + global HASS_CACHE + if HASS_CACHE == {}: + logger.bind(tag=TAG).info(f"实例化HASS:") + if "HomeAssistant" in config["LLM"]: + HASS_CACHE['base_url'] = config["LLM"]['HomeAssistant']['base_url'] + HASS_CACHE['api_key'] = config["LLM"]['HomeAssistant']['api_key'] + else: + logger.bind(tag=TAG).error(f"使用前请在config文件中配置: LLM.HomeAssistant.base_url LLM.HomeAssistant.api_key") + +async def handle_hass_set_state(conn, entity_id, state): + initialize_hass_handler(conn) + global HASS_CACHE + api_key = HASS_CACHE['api_key'] + base_url = HASS_CACHE['base_url'] + ''' + state = { "type":"brightness_up","input":"80","is_muted":"true"} + ''' + domains = entity_id.split(".") + if len(domains) > 1: + domain = domains[0] + else: + return "执行失败,错误的设备id" + action = '' + arg = '' + value = '' + if state['type'] == 'turn_on': + description = "设备已打开" + if domain == "cover": + action = "open_cover" + elif domain == "vacuum": + action = "start" + else: + action = "turn_on" + action = 'turn_on' + elif state['type'] == 'turn_off': + description = "设备已关闭" + if domain == 'cover': + action = "close_cover" + elif domain == 'vacuum': + action = "stop" + else: + action = "turn_off" + elif state['type'] == 'brightness_up': + description = "灯光已调亮" + action = 'turn_on' + arg = 'brightness_step_pct' + value = 10 + elif state['type'] == 'brightness_down': + description = "灯光已调暗" + action = 'turn_on' + arg = 'brightness_step_pct' + value = -10 + elif state['type'] == 'brightness_value': + description = f"亮度已调整到{state['input']}" + action = 'turn_on' + arg = 'brightness_pct' + value = state['input'] + elif state['type'] == 'volume_up': + description = "音量已调大" + action = state['type'] + elif state['type'] == 'volume_down': + description = "音量已调小" + action = state['type'] + elif state['type'] == 'volume_set': + description = f"音量已调整到{state['input']}" + action = state['type'] + arg = 'volume_level' + value = state['input'] + elif state['type'] == 'volume_mute': + description = f"设备已静音" + action = state['type'] + arg = 'is_volume_muted' + value = state['is_muted'] + elif state['type'] == 'pause': + description = f"设备已暂停" + action = state['type'] + if domain == 'media_player': + action = 'media_pause' + if domain == 'cover': + action = 'stop_cover' + if domain == 'vacuum': + action = 'pause' + elif state['type'] == 'continue': + description = f"设备已继续" + if domain == 'media_player': + action = 'media_play' + if domain == 'vacuum': + action = 'start' + else: + return f"{domain} {state.type}功能尚未支持" + + if arg == '': + data = { + "entity_id": entity_id, + } + else: + data = { + "entity_id": entity_id, + arg: value + } + url = f"{base_url}/api/services/{domain}/{action}" + headers = { + "Authorization": f"Bearer {api_key}", + "Content-Type": "application/json" + } + response = requests.post(url, headers=headers, json=data) + logger.bind(tag=TAG).info(f"设置状态:url:{url},return_code:{response.status_code}") + if response.status_code == 200: + return description + else: + return f"设置失败,错误码: {response.status_code}" +