diff --git a/custom_components/zhipuai/config_flow.py b/custom_components/zhipuai/config_flow.py index 4f500d4..1a42290 100644 --- a/custom_components/zhipuai/config_flow.py +++ b/custom_components/zhipuai/config_flow.py @@ -336,16 +336,16 @@ def zhipuai_config_option_schema( CONF_RECOMMENDED, default=options.get(CONF_RECOMMENDED, False) ): bool, - vol.Optional( - CONF_MAX_HISTORY_MESSAGES, - description={"suggested_value": options.get(CONF_MAX_HISTORY_MESSAGES)}, - default=RECOMMENDED_MAX_HISTORY_MESSAGES, - ): int, vol.Optional( CONF_CHAT_MODEL, description={"suggested_value": options.get(CONF_CHAT_MODEL, RECOMMENDED_CHAT_MODEL)}, default=RECOMMENDED_CHAT_MODEL, ): SelectSelector(SelectSelectorConfig(options=ZHIPUAI_MODELS)), + vol.Optional( + CONF_MAX_HISTORY_MESSAGES, + description={"suggested_value": options.get(CONF_MAX_HISTORY_MESSAGES)}, + default=RECOMMENDED_MAX_HISTORY_MESSAGES, + ): int, vol.Optional( CONF_MAX_TOOL_ITERATIONS, description={"suggested_value": options.get(CONF_MAX_TOOL_ITERATIONS, DEFAULT_MAX_TOOL_ITERATIONS)}, diff --git a/custom_components/zhipuai/const.py b/custom_components/zhipuai/const.py index 3988afe..14cb13f 100644 --- a/custom_components/zhipuai/const.py +++ b/custom_components/zhipuai/const.py @@ -21,7 +21,7 @@ RECOMMENDED_MAX_HISTORY_MESSAGES = 5 CONF_MAX_TOOL_ITERATIONS = "max_tool_iterations" -DEFAULT_MAX_TOOL_ITERATIONS = 5 +DEFAULT_MAX_TOOL_ITERATIONS = 20 CONF_COOLDOWN_PERIOD = "cooldown_period" DEFAULT_COOLDOWN_PERIOD = 1 diff --git a/custom_components/zhipuai/conversation.py b/custom_components/zhipuai/conversation.py index 73255d5..b76e0be 100644 --- a/custom_components/zhipuai/conversation.py +++ b/custom_components/zhipuai/conversation.py @@ -94,28 +94,15 @@ def is_service_call(user_input: str) -> bool: "press": ["按", "按下", "点击"], "select": ["选择", "下一个", "上一个", "第一个", "最后一个"], "trigger": ["触发", "调用"], - "media": ["暂停", "继续播放", "播放", "停止", "下一首", "下一曲", "下一个", "切歌", "换歌","上一首", "上一曲", "上一个", "返回上一首", "音量"], - "climate": ["制冷", "制热", "风速", "模式", "调温", "调到", "设置", - "空调", "冷气", "暖气", "冷风", "暖风", "自动模式", "除湿", "送风", - "高档", "低档", "高速", "低速", "自动高", "自动低", "强劲", "自动"] + "number": ["数字", "数值"], + "media": ["暂停", "继续播放", "播放", "停止", "下一首", "下一曲", "下一个", "切歌", "换歌","上一首", "上一曲", "上一个", "返回上一首", "音量"] } } - has_pattern = bool(user_input and ( + return bool(user_input and ( any(k in user_input for k in patterns["control"]) or any(k in user_input for action in patterns["action"].values() for k in (action if isinstance(action, list) else [])) )) - - has_climate = ( - "空调" in user_input and ( - bool(re.search(r"\d+", user_input)) or - any(k in user_input for k in patterns["action"]["climate"]) - ) - ) - - has_entity = bool(re.search(r"([\w_]+\.[\w_]+)", user_input)) - - return has_pattern or has_climate or has_entity def extract_service_info(user_input: str, hass: HomeAssistant) -> Optional[Dict[str, Any]]: def find_entity(domain: str, text: str) -> Optional[str]: @@ -165,48 +152,8 @@ def clean_text(text: str, patterns: List[str]) -> str: for domain, service in [("script", "turn_on"), ("automation", "trigger"), ("scene", "turn_on")] if (entity_id := find_entity(domain, name))), None) - climate_patterns = {"温度": "set_temperature", "制冷": "set_hvac_mode", "制热": "set_hvac_mode", - "风速": "set_fan_mode", "模式": "set_hvac_mode", "湿度": "set_humidity", - "调温": "set_temperature", "调到": "set_temperature", "设置": "set_hvac_mode", - "度": "set_temperature"} - - if entity_id := find_entity("climate", user_input): - temperature_match = re.search(r'(\d+)(?:\s*度)?', user_input) - if temperature_match and ("温度" in user_input or "调温" in user_input or "调到" in user_input or "度" in user_input): - temperature = int(temperature_match.group(1)) - current_temp = hass.states.get(entity_id).attributes.get('current_temperature') - hvac_mode = "cool" if current_temp > temperature else "heat" if current_temp is not None else None - return {"domain": "climate", "service": "set_temperature", - "data": {"entity_id": entity_id, "temperature": temperature, "hvac_mode": hvac_mode}} if hvac_mode else {"domain": "climate", "service": "set_temperature", - "data": {"entity_id": entity_id, "temperature": temperature}} - - for pattern, service in climate_patterns.items(): - if pattern in user_input.lower(): - if service == "set_temperature": - temperature_match = re.search(r'(\d+)', user_input) - if temperature_match: - temperature = int(temperature_match.group(1)) - current_temp = hass.states.get(entity_id).attributes.get('current_temperature') - hvac_mode = "cool" if current_temp > temperature else "heat" if current_temp is not None else None - return {"domain": "climate", "service": service, - "data": {"entity_id": entity_id, "temperature": temperature, "hvac_mode": hvac_mode}} if hvac_mode else {"domain": "climate", "service": service, - "data": {"entity_id": entity_id, "temperature": temperature}} - elif service == "set_hvac_mode": - hvac_mode_map = {"制冷": "cool", "制热": "heat", "自动": "auto", "除湿": "dry", - "送风": "fan_only", "关闭": "off", "停止": "off"} - return {"domain": "climate", "service": service, - "data": {"entity_id": entity_id, "hvac_mode": next((mode for cn, mode in hvac_mode_map.items() if cn in user_input), "auto")}} - elif service == "set_fan_mode": - fan_mode_map = {"高档": "on_high", "高速": "on_high", "强劲": "on_high", - "低档": "on_low", "低速": "on_low", "自动高": "auto_high", - "自动高档": "auto_high", "自动低": "auto_low", "自动低档": "auto_low", - "关闭": "off", "停止": "off"} - return {"domain": "climate", "service": service, - "data": {"entity_id": entity_id, "fan_mode": next((mode for cn, mode in fan_mode_map.items() if cn in user_input), "auto_low")}} - elif service == "set_humidity": - humidity_match = re.search(r'(\d+)', user_input) - return {"domain": "climate", "service": service, - "data": {"entity_id": entity_id, "humidity": int(humidity_match.group(1))}} if humidity_match else None + if any(p in user_input for p in ["数字", "数值"]) and (number_match := re.search(r'\d+(?:\.\d+)?', user_input)) and (entity_id := find_entity("number", clean_text(user_input, ["数字", "数值"]))): + return {"domain": "number", "service": "set_value", "data": {"entity_id": entity_id, "value": number_match.group(0)}} return None @@ -223,14 +170,14 @@ def __init__(self, entry: ConfigEntry, hass: HomeAssistant) -> None: self._attr_device_info = dr.DeviceInfo( identifiers={(DOMAIN, entry.entry_id)}, name=entry.title, - manufacturer="智谱清言", - model="ChatGLM Pro", + manufacturer="北京智谱华章科技", + model="ChatGLM AI", entry_type=dr.DeviceEntryType.SERVICE, ) if self.entry.options.get(CONF_LLM_HASS_API) and self.entry.options.get(CONF_LLM_HASS_API) != "none": self._attr_supported_features = conversation.ConversationEntityFeature.CONTROL self.last_request_time = 0 - self.max_tool_iterations = min(entry.options.get(CONF_MAX_TOOL_ITERATIONS, DEFAULT_MAX_TOOL_ITERATIONS), 5) + self.max_tool_iterations = min(entry.options.get(CONF_MAX_TOOL_ITERATIONS, DEFAULT_MAX_TOOL_ITERATIONS), 30) self.cooldown_period = entry.options.get(CONF_COOLDOWN_PERIOD, DEFAULT_COOLDOWN_PERIOD) self.llm_api = None self.intent_handler = IntentHandler(hass) @@ -315,7 +262,7 @@ async def async_process(self, user_input: conversation.ConversationInput) -> con try: if options.get(CONF_LLM_HASS_API) and options[CONF_LLM_HASS_API] != "none": self.llm_api = await llm.async_get_api(self.hass, options[CONF_LLM_HASS_API], llm_context) - tools = [_format_tool(tool, self.llm_api.custom_serializer) for tool in self.llm_api.tools][:8] + tools = [_format_tool(tool, self.llm_api.custom_serializer) for tool in self.llm_api.tools] if not options.get(CONF_WEB_SEARCH, DEFAULT_WEB_SEARCH): if any(term in user_input.text.lower() for term in ["联网", "查询", "网页", "search"]): @@ -392,7 +339,7 @@ async def async_process(self, user_input: conversation.ConversationInput) -> con start_time = end_time - timedelta(days=days) history_text = [] - history_text.append(f"\n以下是询问者所关注的实体的历史数据分析({days}天内):") + history_text.append("以下是询问者所关注的实体的历史数据分析({}天内):".format(days)) instance = get_instance(self.hass) history_data = await instance.async_add_executor_job( @@ -409,13 +356,13 @@ async def async_process(self, user_input: conversation.ConversationInput) -> con for entity_id in entities: state = self.hass.states.get(entity_id) if state is None or entity_id not in history_data or not history_data[entity_id]: - history_text.append(f"\n{entity_id} (当前状态):") + history_text.append("{} (当前状态):".format(entity_id)) history_text.append( - f"- {state.state if state else 'unknown'} ({state.last_updated.astimezone().strftime('%m-%d %H:%M:%S') if state else 'unknown'})" + "- {} ({})".format(state.state if state else 'unknown', state.last_updated.astimezone().strftime('%m-%d %H:%M:%S') if state else 'unknown') ) else: states = history_data[entity_id] - history_text.append(f"\n{entity_id} (历史状态变化):") + history_text.append("{} (历史状态变化):".format(entity_id)) last_state_text = None last_time = None for state in states: @@ -427,13 +374,18 @@ async def async_process(self, user_input: conversation.ConversationInput) -> con if last_time and (current_time - last_time).total_seconds() < interval_minutes * 60: continue - state_text = f"- {state.state} ({current_time.strftime('%m-%d %H:%M:%S')})" + state_text = "- {} ({})".format(state.state, current_time.strftime('%m-%d %H:%M:%S')) if state_text != last_state_text: history_text.append(state_text) last_state_text = state_text last_time = current_time if len(history_text) > 1: - prompt_parts.append("\n".join(history_text)) + history_text_str = "\n".join(history_text).strip() + if history_text_str: + prompt_parts.append({ + "type": "history_analysis", + "content": history_text + }) except Exception as err: LOGGER.warning(f"获取历史数据时出错: {err}") @@ -445,13 +397,70 @@ async def async_process(self, user_input: conversation.ConversationInput) -> con return conversation.ConversationResult(response=intent_response, conversation_id=conversation_id) if self.llm_api: - prompt_parts.append(self.llm_api.api_prompt) + api_instructions = [line.strip() if line.startswith(" ") else line for line in self.llm_api.api_prompt.split('\n') if line.strip()] + prompt_parts.append({ + "type": "api_instructions", + "content": api_instructions + }) + + base_prompt = prompt_parts[0] + base_instructions = [line.strip() if line.startswith(" ") else line for line in base_prompt.split('\n') if line.strip()] + prompt_parts[0] = { + "type": "system_instructions", + "content": base_instructions + } - prompt = "\n".join(prompt_parts) - LOGGER.info("提示部件: %s", prompt_parts) + def format_modes(modes): + if not modes: + return [] + if isinstance(modes, str): + return [m.strip() for m in modes.split(',')] + return [str(mode) for mode in modes] + + climate_entities = [state for state in self.hass.states.async_all() if state.domain == "climate"] + if climate_entities: + content = [] + for entity in climate_entities: + attrs = entity.attributes + hvac_modes = format_modes(attrs.get('hvac_modes', [])) + fan_modes = format_modes(attrs.get('fan_modes', [])) + swing_modes = format_modes(attrs.get('swing_modes', [])) + + entity_info = [ + f"- names: {attrs.get('friendly_name', entity.entity_id)}", + f"domain: climate", + f"state: {entity.state}", + "attributes:", + f"current_temperature: {attrs.get('current_temperature')}", + f"temperature: {attrs.get('temperature')}", + f"min_temp: {attrs.get('min_temp')}", + f"max_temp: {attrs.get('max_temp')}", + f"target_temp_step: {attrs.get('target_temp_step')}", + f"hvac_modes: {hvac_modes}", + f"fan_modes: {fan_modes}", + f"swing_modes: {swing_modes}", + f"hvac_action: {attrs.get('hvac_action')}", + f"fan_mode: {attrs.get('fan_mode')}", + f"swing_mode: {attrs.get('swing_mode')}", + f"current_humidity: {attrs.get('current_humidity')}", + f"humidity: {attrs.get('humidity')}" + ] + content.extend(entity_info) + + prompt_parts.append({"type": "climate_status", "content": content}) + + prompt_json = json.dumps(prompt_parts, ensure_ascii=False, separators=(',', ':')) + LOGGER.info("提示部件: %s", prompt_json) + + all_lines = [] + for part in prompt_parts: + if isinstance(part["content"], list): + all_lines.extend([line.strip() if line.startswith(" ") else line for line in part["content"]]) + else: + all_lines.extend([line.strip() if line.startswith(" ") else line for line in part["content"].split('\n') if line.strip()]) messages = [ - ChatCompletionMessageParam(role="system", content=prompt), + ChatCompletionMessageParam(role="system", content="\n".join(all_lines)), *(messages if use_history else []), ChatCompletionMessageParam(role="user", content=user_input.text), ] @@ -467,7 +476,7 @@ async def async_process(self, user_input: conversation.ConversationInput) -> con "max_tokens": min(options.get(CONF_MAX_TOKENS, RECOMMENDED_MAX_TOKENS), 4096), "top_p": options.get(CONF_TOP_P, RECOMMENDED_TOP_P), "temperature": options.get(CONF_TEMPERATURE, RECOMMENDED_TEMPERATURE), - "request_id": conversation_id, + "request_id": conversation_id } if tools: payload["tools"] = tools @@ -497,7 +506,7 @@ async def async_process(self, user_input: conversation.ConversationInput) -> con if isinstance(tool_response, dict) and "error" in tool_response: raise Exception(tool_response["error"]) - formatted_response = json.dumps(tool_response) + formatted_response = json.dumps(tool_response, ensure_ascii=False) if isinstance(tool_response, (dict, list)) else str(tool_response) messages.append( ChatCompletionMessageParam( role="tool", diff --git a/custom_components/zhipuai/intents.py b/custom_components/zhipuai/intents.py index 2ca1b2b..64d62ad 100644 --- a/custom_components/zhipuai/intents.py +++ b/custom_components/zhipuai/intents.py @@ -2,6 +2,7 @@ import re import os import yaml +import asyncio from datetime import timedelta, datetime from typing import Any, Dict, List, Optional, Set import voluptuous as vol @@ -9,10 +10,14 @@ from homeassistant.components.conversation import DOMAIN as CONVERSATION_DOMAIN from homeassistant.components.lock import LockState from homeassistant.components.timer import ( - ATTR_DURATION, ATTR_REMAINING, - CONF_DURATION, CONF_ICON, + ATTR_DURATION, + ATTR_REMAINING, + CONF_DURATION, + CONF_ICON, DOMAIN as TIMER_DOMAIN, - SERVICE_START, SERVICE_PAUSE, SERVICE_CANCEL + SERVICE_CANCEL, + SERVICE_PAUSE, + SERVICE_START, ) from homeassistant.const import ATTR_DEVICE_CLASS, ATTR_ENTITY_ID from homeassistant.core import Context, HomeAssistant, ServiceResponse, State @@ -36,6 +41,9 @@ def _load_yaml(): INTENT_NOTIFY = "HassNotifyIntent" INTENT_COVER_GET_STATE = "ZHIPUAI_CoverGetStateIntent" INTENT_COVER_SET_POSITION = "ZHIPUAI_CoverSetPositionIntent" +INTENT_CLIMATE_SET_TEMP = "ClimateSetTemperature" +INTENT_CLIMATE_SET_MODE = "ClimateSetMode" +INTENT_CLIMATE_SET_FAN = "ClimateSetFanMode" INTENT_NEVERMIND = "nevermind" SERVICE_PROCESS = "process" ERROR_NO_CAMERA = "no_camera" @@ -46,6 +54,7 @@ def _load_yaml(): ERROR_NO_MESSAGE = "no_message" ERROR_INVALID_POSITION = "invalid_position" + async def async_setup_intents(hass: HomeAssistant) -> None: yaml_path = os.path.join(os.path.dirname(__file__), "intents.yaml") intents_config = await async_load_yaml_config(hass, yaml_path) @@ -56,6 +65,11 @@ async def async_setup_intents(hass: HomeAssistant) -> None: intent.async_register(hass, WebSearchIntent(hass)) intent.async_register(hass, HassTimerIntent(hass)) intent.async_register(hass, HassNotifyIntent(hass)) + intent.async_register(hass, ClimateSetTemperatureIntent(hass)) + intent.async_register(hass, ClimateSetModeIntent(hass)) + intent.async_register(hass, ClimateSetFanModeIntent(hass)) + intent.async_register(hass, ClimateSetHumidityIntent(hass)) + intent.async_register(hass, CoverControlAllIntent(hass)) class CameraAnalyzeIntent(intent.IntentHandler): @@ -152,7 +166,7 @@ async def async_handle(self, intent_obj: intent.Intent) -> intent.IntentResponse return self._set_error_response(response, ERROR_NO_QUERY, "未提供搜索内容") if time_query: - now = datetime.now() # 简化时间处理 + now = datetime.now() if time_query in ["昨天", "昨日", "yesterday"]: date = (now - timedelta(days=1)).strftime('%Y-%m-%d') elif time_query in ["明天", "明日", "tomorrow"]: @@ -380,6 +394,259 @@ def get_slot_value(self, slot_data): return None if not slot_data else slot_data.get('value') if isinstance(slot_data, dict) else getattr(slot_data, 'value', None) if hasattr(slot_data, 'value') else str(slot_data) +class ClimateBaseIntent(intent.IntentHandler): + + def __init__(self, hass: HomeAssistant): + super().__init__() + self.hass = hass + + def _set_error_response(self, response, code: str, message: str) -> intent.IntentResponse: + response.async_set_error(code=code, message=message) + return response + + def _set_speech_response(self, response, message) -> intent.IntentResponse: + response.async_set_speech(message) + return response + + def async_validate_slots(self, slots): + validated = {} + for key, value in slots.items(): + if isinstance(value, dict) and "value" in value: + validated[key] = value["value"] + else: + validated[key] = value + return validated + + def get_slot_value(self, slot_data): + return None if not slot_data else slot_data.get('value') if isinstance(slot_data, dict) else getattr(slot_data, 'value', None) if hasattr(slot_data, 'value') else str(slot_data) + + def find_climate_entity(self, name: str) -> Optional[State]: + return next((state for state in self.hass.states.async_all() if state.domain == "climate" and (str(name).lower() in state.attributes.get('friendly_name', '').lower() or str(name).lower() in state.entity_id.lower())), None) + + def get_mode_value(self, mode) -> str: + return mode.value if hasattr(mode, 'value') else str(mode).strip("'[]") + + def normalize_mode_list(self, modes) -> List[str]: + return [self.get_mode_value(mode) for mode in (modes.strip("[]").replace("'", "").split(", ") if isinstance(modes, str) else modes)] + + async def ensure_entity_on(self, entity_id: str) -> None: + state = self.hass.states.get(entity_id) + state.state == 'off' and await self.hass.services.async_call("climate", "turn_on", {"entity_id": entity_id}) and await asyncio.sleep(1) + +class ClimateSetModeIntent(ClimateBaseIntent): + intent_type = INTENT_CLIMATE_SET_MODE + slot_schema = {vol.Required("name"): str, vol.Required("mode"): str} + + async def async_handle(self, intent_obj: intent.Intent) -> intent.IntentResponse: + slots = self.async_validate_slots(intent_obj.slots) + name, mode = self.get_slot_value(slots.get("name")), self.get_slot_value(slots.get("mode")) + response = intent.IntentResponse(intent=intent_obj, language="zh-cn") + return (self._set_error_response(response, "invalid_slots", "缺少必要的参数") if not name or not mode else + self._set_error_response(response, "not_found", f"找不到名为 {name} 的空调") if not (state := self.find_climate_entity(name)) else + await self._handle_mode_setting(response, state, name, mode)) + + async def _handle_mode_setting(self, response, state, name, mode): + available_modes = self.normalize_mode_list(state.attributes.get('hvac_modes', [])) + mode_maps = {"制冷": ["cool", "cooling"], "制热": ["heat", "heating"], "自动": ["auto", "heat_cool", "automatic"], + "除湿": ["dry", "dehumidify"], "送风": ["fan_only", "fan"], "关闭": ["off"], "停止": ["off"]} + mode_lower = mode.lower() + target_mode = (mode_lower if mode_lower in available_modes else + next((m for cn_mode, en_modes in mode_maps.items() + for m in en_modes if m in available_modes + and (mode_lower == cn_mode.lower() or any(em in mode_lower for em in en_modes))), None)) + return (self._set_error_response(response, "invalid_mode", + f"不支持的模式: {mode}。支持的模式: {', '.join([cn_mode for cn_mode, en_modes in mode_maps.items() if any(m in available_modes for m in en_modes)])}") + if not target_mode else + await self._set_mode(response, state, name, mode, target_mode)) + + async def _set_mode(self, response, state, name, mode, target_mode): + try: + await self.hass.services.async_call("climate", "set_hvac_mode", + {"entity_id": state.entity_id, "hvac_mode": target_mode}) + return self._set_speech_response(response, f"已将{name}设置为{mode}模式") + except Exception as e: + return self._set_error_response(response, "operation_failed", f"设置模式失败: {str(e)}") + +class ClimateSetFanModeIntent(ClimateBaseIntent): + intent_type = INTENT_CLIMATE_SET_FAN + slot_schema = {vol.Required("name"): str, vol.Required("fan_mode"): str} + + async def async_handle(self, intent_obj: intent.Intent) -> intent.IntentResponse: + slots = self.async_validate_slots(intent_obj.slots) + name, fan_mode = self.get_slot_value(slots.get("name")), self.get_slot_value(slots.get("fan_mode")) + response = intent.IntentResponse(intent=intent_obj, language="zh-cn") + return (self._set_error_response(response, "invalid_slots", "缺少必要的参数") if not name or not fan_mode else + self._set_error_response(response, "not_found", f"找不到名为 {name} 的空调") if not (state := self.find_climate_entity(name)) else + await self._handle_fan_mode_setting(response, state, name, fan_mode)) + + async def _handle_fan_mode_setting(self, response, state, name, fan_mode): + await self.ensure_entity_on(state.entity_id) + available_modes = self.normalize_mode_list(state.attributes.get('fan_modes', [])) + fan_mode_maps = { + "自动": ["auto", "auto_low", "auto_high", "自动"], + "低速": ["on_low", "low", "一档", "一挡", "低风"], + "中速": ["medium", "mid", "二档", "二挡", "中风"], + "高速": ["on_high", "high", "七档", "高风"], + "关闭": ["off"] + } + fan_mode_clean = str(fan_mode).rstrip('档挡') + target_mode = (fan_mode if fan_mode in available_modes else + next((mode for mode in available_modes if str(fan_mode_clean) in mode or + f"{'一二三四五六七八九十'[int(fan_mode_clean)-1]}" in mode or + mode.rstrip('档挡') == fan_mode_clean), None) if fan_mode_clean.isdigit() else + next((m for cn_mode, en_modes in fan_mode_maps.items() + for m in en_modes if m in available_modes + and (fan_mode_clean == cn_mode or any(em in fan_mode_clean for em in en_modes))), None)) + return (self._set_error_response(response, "invalid_fan_mode", + f"不支持的风速模式: {fan_mode}。支持的模式: {', '.join(available_modes)}") if not target_mode else + await self._set_fan_mode(response, state, name, target_mode)) + + async def _set_fan_mode(self, response, state, name, target_mode): + try: + await self.hass.services.async_call("climate", "set_fan_mode", + {"entity_id": state.entity_id, "fan_mode": target_mode}) + return self._set_speech_response(response, f"已将{name}的风速设置为{target_mode}") + except Exception as e: + return (self._set_speech_response(response, f"已开启{name}并设置风速为{target_mode}") + if "设备在当前状态下无法执行此操作" in str(e) and + not await self.ensure_entity_on(state.entity_id) and + not await self.hass.services.async_call("climate", "set_fan_mode", + {"entity_id": state.entity_id, "fan_mode": target_mode}) else + self._set_error_response(response, "operation_failed", f"设置风速失败: {str(e)}")) + +class ClimateSetTemperatureIntent(ClimateBaseIntent): + intent_type = INTENT_CLIMATE_SET_TEMP + slot_schema = {vol.Required("name"): str, vol.Required("temperature"): int} + + async def async_handle(self, intent_obj: intent.Intent) -> intent.IntentResponse: + slots = self.async_validate_slots(intent_obj.slots) + name = self.get_slot_value(slots.get("name")) + temperature = self.get_slot_value(slots.get("temperature")) + + try: + temperature = int(temperature) if temperature is not None else None + except (TypeError, ValueError): + response = intent.IntentResponse(intent=intent_obj, language="zh-cn") + return self._set_error_response(response, "invalid_temperature", "温度必须是一个有效的数字") + + if not name or temperature is None: + response = intent.IntentResponse(intent=intent_obj, language="zh-cn") + return self._set_error_response(response, "invalid_slots", "缺少必要的参数") + + state = self.find_climate_entity(name) + response = intent.IntentResponse(intent=intent_obj, language="zh-cn") + + if not state: + return self._set_error_response(response, "not_found", f"找不到名为 {name} 的空调") + + await self.ensure_entity_on(state.entity_id) + + min_temp = float(state.attributes.get('min_temp', 16)) + max_temp = float(state.attributes.get('max_temp', 30)) + + if temperature < min_temp or temperature > max_temp: + return self._set_error_response(response, "invalid_temperature", + f"温度必须在{min_temp}度到{max_temp}度之间") + + current_temp = state.attributes.get('current_temperature') + if current_temp is not None: + current_temp = float(current_temp) + if current_temp > temperature: + await self.hass.services.async_call("climate", "set_hvac_mode", + {"entity_id": state.entity_id, "hvac_mode": "cool"}) + elif current_temp < temperature: + await self.hass.services.async_call("climate", "set_hvac_mode", + {"entity_id": state.entity_id, "hvac_mode": "heat"}) + + try: + await self.hass.services.async_call("climate", "set_temperature", + {"entity_id": state.entity_id, "temperature": temperature}) + return self._set_speech_response(response, f"已将{name}温度设置为{temperature}度") + except Exception as e: + LOGGER.error("设置温度失败: %s", str(e)) + return self._set_error_response(response, "operation_failed", f"设置温度失败: {str(e)}") + +class ClimateSetHumidityIntent(ClimateBaseIntent): + intent_type = "ClimateSetHumidity" + slot_schema = {vol.Required("name"): str, vol.Required("humidity"): int} + + def __init__(self, hass: HomeAssistant) -> None: + self.hass = hass + + async def async_handle(self, intent_obj: intent.Intent) -> intent.IntentResponse: + slots = self.async_validate_slots(intent_obj.slots) + name = self.get_slot_value(slots.get("name")) + humidity = self.get_slot_value(slots.get("humidity")) + + try: + humidity = int(humidity) if humidity is not None else None + except (TypeError, ValueError): + response = intent.IntentResponse(intent=intent_obj, language="zh-cn") + return self._set_error_response(response, "invalid_humidity", "湿度必须是一个有效的数字") + + if not name or humidity is None: + response = intent.IntentResponse(intent=intent_obj, language="zh-cn") + return self._set_error_response(response, "invalid_slots", "缺少必要的参数") + + state = self.find_climate_entity(name) + response = intent.IntentResponse(intent=intent_obj, language="zh-cn") + + if not state: + return self._set_error_response(response, "not_found", f"未找到名为 {name} 的空调") + + await self.ensure_entity_on(state.entity_id) + + if humidity < 10 or humidity > 100: + return self._set_error_response(response, "invalid_humidity", + "湿度必须在10%到100%之间") + + try: + await self.hass.services.async_call("climate", "set_humidity", + {"entity_id": state.entity_id, "humidity": humidity}) + return self._set_speech_response(response, f"已将{name}湿度设置为{humidity}%") + except Exception as e: + LOGGER.error("设置湿度失败: %s", str(e)) + return self._set_error_response(response, "operation_failed", f"设置湿度失败: {str(e)}") + + +class CoverControlAllIntent(intent.IntentHandler): + intent_type = "CoverControlAll" + slot_schema = {} + + def __init__(self, hass: HomeAssistant) -> None: + self.hass = hass + + async def async_handle(self, intent_obj: intent.Intent) -> intent.IntentResponse: + response = intent.IntentResponse(intent=intent_obj, language="zh-cn") + + covers = [state for state in self.hass.states.async_all() + if state.entity_id.startswith("cover.")] + + if not covers: + return self._set_error_response(response, "not_found", "未找到任何窗帘设备") + + try: + for cover in covers: + await self.hass.services.async_call( + "cover", "close_cover", + {"entity_id": cover.entity_id} + ) + return self._set_speech_response(response, f"已关闭所有{len(covers)}个窗帘") + except Exception as e: + return self._set_error_response(response, "operation_failed", f"操作失败: {str(e)}") + + def _set_speech_response(self, response: intent.IntentResponse, speech: str) -> intent.IntentResponse: + response.response_type = intent.IntentResponseType.ACTION_DONE + response.speech = {"plain": {"speech": speech, "extra_data": None}} + return response + + def _set_error_response(self, response: intent.IntentResponse, error: str, message: str) -> intent.IntentResponse: + response.response_type = intent.IntentResponseType.ERROR + response.error_code = error + response.speech = {"plain": {"speech": message, "extra_data": None}} + return response + + def extract_intent_info(user_input: str, hass: HomeAssistant) -> Optional[Dict[str, Any]]: entity_match = re.search(r'([\w_]+\.[\w_]+)', user_input) entity_id = entity_match.group(1) if entity_match else None @@ -431,6 +698,7 @@ def __init__(self, hass: HomeAssistant): automation_actions = {"turn_on": "启用", "turn_off": "禁用", "trigger": "触发", "toggle": "切换"} boolean_actions = {"turn_on": "打开", "turn_off": "关闭", "toggle": "切换"} timer_actions = {"start": "启动", "pause": "暂停", "cancel": "取消", "finish": "结束", "reload": "重新加载"} + select_actions = {"select_next": "下一个", "select_previous": "上一个", "select_first": "第一个", "select_last": "最后一个", "select_option": "选择"} async def call_service(self, domain: str, service: str, data: Dict[str, Any]) -> Dict[str, Any]: try: @@ -465,6 +733,8 @@ async def call_service(self, domain: str, service: str, data: Dict[str, Any]) -> {"success": True, "message": f"已设置 {friendly_name} 吸力为{data['fan_speed']}"} if domain == "vacuum" and service == "set_fan_speed" and "fan_speed" in data else {"success": True, "message": f"已{self.vacuum_actions.get(service, service)} {friendly_name}"} if domain == "vacuum" else {"success": True, "message": f"已按下 {friendly_name}"} if domain == "button" and service == "press" else + {"success": True, "message": f"已设置 {friendly_name} 为 {data['value']}"} if domain == "number" and service == "set_value" else + {"success": True, "message": f"已切换到{friendly_name}{self.select_actions.get(service, '选项')}"} if domain == "select" else {"success": True, "message": f"已执行 {friendly_name} {service}"}) except Exception as e: return {"success": False, "message": str(e)} diff --git a/custom_components/zhipuai/intents.yaml b/custom_components/zhipuai/intents.yaml index 53f7bfe..a167221 100644 --- a/custom_components/zhipuai/intents.yaml +++ b/custom_components/zhipuai/intents.yaml @@ -330,23 +330,101 @@ intents: - "[记住|记录|记一下|提醒我|提醒|提醒一下][这个|这件事]{message}" - "帮我[记住|记录|记一下][这个|这件事]{message}" - "帮我[提醒|提醒一下][这个|这件事]{message}" + - "[帮我|请|麻烦|给我][记住|记录|记一下][这个|这件事]{message}" + - "帮我[提醒|提醒一下][这个|这件事]{message}" + + ClimateSetTemperature: + data: + - sentences: + - "[把|将]{name}[温度|]调[到|成|为]{temperature}[度|]" + - "[把|将]{name}调[到|成|为]{temperature}[度|]" + - "设置{name}[温度|]为{temperature}[度|]" + - "{name}[温度|]调[到|成|为]{temperature}[度|]" + - "调{name}[温度|]到{temperature}[度|]" + - "把{name}的温度调[到|成|为]{temperature}[度|]" + - "给{name}[调|设置]温度[到|为]{temperature}[度|]" + slots: + name: + type: text + temperature: + type: number + + ClimateSetMode: + data: + - sentences: + - "[把|将]{name}设[为|成]{mode}[模式|]" + - "设置{name}为{mode}[模式|]" + - "{name}[切换到|改成|改为]{mode}[模式|]" + - "[打开|启动]{name}[的|]{mode}[模式|]" + - "{name}{mode}[模式|]" + slots: + name: + type: text + mode: + type: list + values: + - 制冷 + - 制热 + - 自动 + - 除湿 + - 送风 + - 关闭 + - 停止 -responses: - intents: - MassPlayMediaAssist: - default: "ok" + ClimateSetFanMode: + data: + - sentences: + - "[把|将]{name}[的|]风速调[到|成|为]{fan_mode}" + - "设置{name}[的|]风速[为|到]{fan_mode}" + - "{name}风速调[成|为]{fan_mode}" + - "调[整|]{name}[的|]风速[为|到]{fan_mode}" + - "{name}调[成|为]{fan_mode}风速" + slots: + name: + type: text + fan_mode: + type: list + values: + - 高档 + - 低档 + - 高速 + - 低速 + - 自动高 + - 自动低 + - 强劲 + - 自动 + + CoverControlAll: + data: + - sentences: + - "关闭(?:所有|全部)(?:的)?窗帘" + - "(?:所有|全部)窗帘关闭" + - "把(?:所有|全部)(?:的)?窗帘(?:都)?关(?:上|闭)" + - "全部关闭(?:所有)?(?:的)?窗帘" + response: + success: + text: 已关闭所有{count}个窗帘 + error: + not_found: 未找到任何窗帘设备 + operation_failed: 操作失败,{error} + + ClimateSetHumidity: + data: + - sentences: + - "[把|将]{name}[的|]湿度[设置|调][到|成|为]{humidity}[%|度]" + - "设置{name}[的|]湿度[为|到]{humidity}[%|度]" + - "{name}湿度[设置|调][成|为]{humidity}[%|度]" + - "调[整|]{name}[的|]湿度[为|到]{humidity}[%|度]" + slots: + name: + type: text + humidity: + type: number + response: + success: + text: 已将{name}湿度设置为{humidity}% + error: + not_found: 未找到名为{name}的空调 + operation_failed: 设置湿度失败,{error} + invalid_humidity: 湿度必须在0%到100%之间 -lists: - artist: - wildcard: true - album: - wildcard: true - track: - wildcard: true - playlist: - wildcard: true - radio: - wildcard: true - radio_mode: - values: - - "radio mode" \ No newline at end of file diff --git a/custom_components/zhipuai/translations/en.json b/custom_components/zhipuai/translations/en.json index 6e3dbf9..d3768e3 100644 --- a/custom_components/zhipuai/translations/en.json +++ b/custom_components/zhipuai/translations/en.json @@ -1,5 +1,5 @@ { - "title": "Clear words of wisdom", + "title": "Zhipu AI", "config": { "step": { "user": { @@ -71,12 +71,12 @@ "max_tokens": "Set the maximum number of tokens returned in the response", "temperature": "Controls the randomness of the output (0-2)", "top_p": "Control output diversity (0-1)", - "llm_hass_api": "Opt-in Home Assistant LLM", + "llm_hass_api": "Opt-in LLM API", "recommended": "Use recommended model settings", - "max_history_messages": "Set the maximum number of historical messages to retain. Function: Control the memory function of input content. The memory function can ensure smooth contextual dialogue. Generally, it is best to control home equipment within 5 times. It is effective for requests that cannot be processed smoothly. For other daily conversations, the threshold can be set to more than 10 times.", - "max_tool_iterations": "Set the maximum number of tool calls in a single session. Its function is to set the call threshold for the system LLM call request. If an error occurs, it can ensure that the system will not freeze. Especially for the design of various small hosts with weak performance, it is recommended to set it 20-30 times.", - "cooldown_period": "Set the minimum interval between two conversation requests (0-10 seconds). Function: The request will be delayed for a period of time before being sent. It is recommended to set it within 3 seconds to ensure that the content sending request fails due to frequency factors.", - "request_timeout": "Set the timeout for AI requests (10-120 seconds). Function: Control the maximum time to wait for AI response. You may need to increase this value when generating longer text. Suggestion: If it is a short quick response dialogue, or remove the AI ​​timeout setting, you can set it to about 10 seconds. If AI errors often occur, you can increase this value appropriately. Generally, 30 seconds is enough for a conversation. If you need to generate a long text of more than 1,000 words, it is recommended to set it for more than 60 seconds. If timeout errors often occur, you can increase this value appropriately." + "max_history_messages": "Set the maximum number of historical messages to be retained. Function: Control the memory function of input content. The memory function can ensure smooth contextual dialogue. Generally, it is best to control home equipment within 5 times. It is effective for requests that cannot be processed smoothly. Other daily conversations can be set. The threshold is above 10 times.", + "max_tool_iterations": "Set the maximum number of tool calls in a single conversation. Its function is to set the call threshold for system LLM call requests. If an error occurs, it can ensure that the system will not freeze. Especially for the design of various small hosts with weak performance, it is recommended to set 20 -30 times.", + "cooldown_period": "Set the minimum (0-10 seconds) interval between two conversation requests. The effect: the request will be delayed for a period of time before being sent. It is recommended to set it within 3 seconds to ensure that the content sending request fails due to frequency factors.", + "request_timeout": "Set the timeout time for AI requests (10-120 seconds). Function: Control the maximum time to wait for AI response. This value may need to be increased when generating longer text. Suggestion: If it is a short quick response conversation, you can set it to about 10 seconds. If an AI error occurs, this value can be increased appropriately. If you need to generate long text of more than 1,000 words, it is recommended to set it to more than 60 seconds." } }, "history": { @@ -84,7 +84,7 @@ "description": "Provides **entity historical data analysis** in scenarios that cannot be achieved by **Jinja2 template** (Home Assistant's template system) to ensure that AI understands and analyzes your device data. For example: it can be used to automatically help you analyze home security , personnel activity trajectories, daily life summary, UI text template introduction, etc.\n\n• Support **AI-assisted analysis** historical data (let AI understand and analyze your device data)\n• Provide intelligent decision support for **device management**\n• It is recommended to control within the range of **1 day historical data** for best results\n• **Special Reminder**: For environmental sensors that update frequently such as temperature, humidity, and illumination, please avoid selecting Prevent AI Overflow (you can set it according to the default 10 minutes)", "data": { "history_entities": "Select entity", - "history_days": "Get the range of days the entity has been in the repository (1-15 days)", + "history_days": "Get the range of days (1-15 days) that the entity has been in the repository", "history_interval": "Get the update time (minutes) of the entity in the repository" } } diff --git a/custom_components/zhipuai/translations/zh-Hans.json b/custom_components/zhipuai/translations/zh-Hans.json index a5f3a45..3763326 100644 --- a/custom_components/zhipuai/translations/zh-Hans.json +++ b/custom_components/zhipuai/translations/zh-Hans.json @@ -71,12 +71,12 @@ "max_tokens": "设置响应中返回的最大令牌数", "temperature": "控制输出的随机性(0-2)", "top_p": "控制输出多样性(0-1)", - "llm_hass_api": "选择启用 Home Assistant LLM ", + "llm_hass_api": "选择启用 LLM API ", "recommended": "使用推荐的模型设置", - "max_history_messages": "设置要保留的最大历史消息数。功能:控制输入内容的记忆功能,记忆功能可以保证上下文对话顺畅,一般控制家居设备最好控制在5次以内,对请求不能顺利进行有效,其他日常对话可以设置阈值在10次以上。", - "max_tool_iterations": "设置单次对话中的最大工具调用次数。其功能是对系统LLM调用请求设置调用阈值,如果出错可以保证系统不会卡死,尤其是对各种性能较弱的小主机的设计,建议设置20-30次。", - "cooldown_period": "设置两次对话请求的最小间隔时间(0-10秒)。作用:请求会延迟一段时间再发送,建议设置在3秒以内,保证因为频率因素导致内容发送请求失败。", - "request_timeout": "设置AI请求的超时时间(10-120秒)。作用:控制等待AI响应的最长时间,生成较长文本时可能需要增加此值。建议:如果是较短快速响应对话,可以设置10秒左右。如果出现AI报错,可以适当增加此值。如需生成超过1000字的长文本,建议设置60秒以上。" + "max_history_messages": "设置要保留的最大历史消息数,功能:控制输入内容的记忆功能,记忆功能可以保证上下文对话顺畅,一般控制家居设备最好控制在5次以内,对请求不能顺利进行有效,其他日常对话可以设置阈值在10次以上。", + "max_tool_iterations": "设置单次对话中的最大工具调用次数,其功能是对系统LLM调用请求设置调用阈值,如果出错可以保证系统不会卡死,尤其是对各种性能较弱的小主机的设计,建议设置20-30次。", + "cooldown_period": "设置两次对话请求的最小(0-10秒)间隔时间,作用:请求会延迟一段时间再发送,建议设置在3秒以内,保证因为频率因素导致内容发送请求失败。", + "request_timeout": "设置AI请求的(10-120秒)超时时间,作用:控制等待AI响应的最长时间,生成较长文本时可能需要增加此值。建议:如果是较短快速响应对话,可以设置10秒左右。如果出现AI报错,可以适当增加此值。如需生成超过1000字的长文本,建议设置60秒以上。" } }, "history": { @@ -84,7 +84,7 @@ "description": "在**Jinja2模版**(Home Assistant的模板系统)无法实现的场景下提供**实体历史数据分析**,保证AI理解并分析您的设备数据,举例:可以用于自动化帮您分析家中安防、人员活动轨迹,日常生活总结,UI文本模版介绍等。\n\n• 支持**AI辅助分析**历史数据(让AI理解并分析您的设备数据)\n• 为**设备管理**提供智能决策支持\n• 建议控制在**1天历史数据**范围内以获得最佳效果\n• **特别提醒**:对于温湿度、光照度等频繁更新的环境传感器,请避免选择防止AI溢出(可以按照默认10分钟设置)", "data": { "history_entities": "选择实体", - "history_days": "获取实体在存储库中的天数范围 (1-15天)", + "history_days": "获取实体在存储库中的 (1-15天) 天数范围", "history_interval": "获取实体在存储库中的更新时间(分钟)" } }