@@ -280,7 +280,9 @@ async def _resolve_automation_entity_id(self, identifier: str) -> str | None:
280280 ):
281281 return str (state ["entity_id" ])
282282 except Exception as e :
283- logger .debug (f"Failed to resolve entity_id for automation { identifier } : { e } " )
283+ logger .debug (
284+ f"Failed to resolve entity_id for automation { identifier } : { e } "
285+ )
284286 return None
285287
286288 @tool (
@@ -335,13 +337,17 @@ async def ha_config_get_automation(
335337 "Use ha_search_entities(domain_filter='automation') to list automations" ,
336338 ],
337339 )
338- normalized_config , config_hash = await self ._get_automation_config_internal (identifier )
340+ normalized_config , config_hash = await self ._get_automation_config_internal (
341+ identifier
342+ )
339343
340344 # Resolve entity_id and fetch category from entity registry
341345 # (injected after hash so transient registry failures don't affect the hash)
342346 entity_id = await self ._resolve_automation_entity_id (identifier )
343347 if entity_id :
344- cat_id = await fetch_entity_category (self ._client , entity_id , "automation" )
348+ cat_id = await fetch_entity_category (
349+ self ._client , entity_id , "automation"
350+ )
345351 if cat_id :
346352 normalized_config ["category" ] = cat_id
347353
@@ -645,7 +651,10 @@ async def ha_config_set_automation(
645651 "Provide the automation entity_id or unique_id" ,
646652 "Use ha_search_entities(domain_filter='automation') to find automations" ,
647653 ],
648- context = {"action" : "python_transform" , "identifier" : identifier },
654+ context = {
655+ "action" : "python_transform" ,
656+ "identifier" : identifier ,
657+ },
649658 )
650659 )
651660 if config_hash is None :
@@ -657,7 +666,10 @@ async def ha_config_set_automation(
657666 "Call ha_config_get_automation() first" ,
658667 "Use the config_hash from that response" ,
659668 ],
660- context = {"action" : "python_transform" , "identifier" : identifier },
669+ context = {
670+ "action" : "python_transform" ,
671+ "identifier" : identifier ,
672+ },
661673 )
662674 )
663675
@@ -676,7 +688,10 @@ async def ha_config_set_automation(
676688 ErrorCode .VALIDATION_FAILED ,
677689 message ,
678690 suggestions = suggestions ,
679- context = {"action" : "python_transform" , "identifier" : identifier },
691+ context = {
692+ "action" : "python_transform" ,
693+ "identifier" : identifier ,
694+ },
680695 )
681696 )
682697
@@ -699,11 +714,20 @@ async def ha_config_set_automation(
699714
700715 # Re-apply category if present
701716 entity_id = result .get ("entity_id" )
702- if not entity_id and identifier and identifier .startswith ("automation." ):
717+ if (
718+ not entity_id
719+ and identifier
720+ and identifier .startswith ("automation." )
721+ ):
703722 entity_id = identifier
704723 if transform_category and entity_id :
705724 await apply_entity_category (
706- self ._client , entity_id , transform_category , "automation" , result , "automation"
725+ self ._client ,
726+ entity_id ,
727+ transform_category ,
728+ "automation" ,
729+ result ,
730+ "automation" ,
707731 )
708732
709733 response : dict [str , Any ] = {
@@ -763,7 +787,9 @@ async def ha_config_set_automation(
763787 self ._client , config_dict
764788 )
765789
766- result = await self ._client .upsert_automation_config (config_dict , identifier )
790+ result = await self ._client .upsert_automation_config (
791+ config_dict , identifier
792+ )
767793
768794 # If the client could not verify the entity was registered, warn but don't hard-fail.
769795 if result .get ("entity_not_verified" ):
@@ -785,7 +811,9 @@ async def ha_config_set_automation(
785811 if wait_bool and entity_id :
786812 action_word = "created" if identifier is None else "updated"
787813 try :
788- registered = await wait_for_entity_registered (self ._client , entity_id )
814+ registered = await wait_for_entity_registered (
815+ self ._client , entity_id
816+ )
789817 if not registered :
790818 result .setdefault ("warnings" , []).append (
791819 f"Automation { action_word } but { entity_id } not yet queryable. "
@@ -799,7 +827,12 @@ async def ha_config_set_automation(
799827 # Apply category to entity registry if provided
800828 if effective_category and entity_id :
801829 await apply_entity_category (
802- self ._client , entity_id , effective_category , "automation" , result , "automation"
830+ self ._client ,
831+ entity_id ,
832+ effective_category ,
833+ "automation" ,
834+ result ,
835+ "automation" ,
803836 )
804837
805838 if bp_warnings :
@@ -852,9 +885,7 @@ async def _list_automation_entity_ids(self) -> list[str]:
852885 {"type" : "config/entity_registry/list" }
853886 )
854887 except Exception as e :
855- logger .debug (
856- "Failed to list automation entity_ids from registry: %s" , e
857- )
888+ logger .debug ("Failed to list automation entity_ids from registry: %s" , e )
858889 return []
859890 entries = result .get ("result" , []) if isinstance (result , dict ) else result
860891 if not isinstance (entries , list ):
@@ -913,7 +944,9 @@ async def _fetch_and_verify_hash(
913944 Returns the current normalized config dict.
914945 Raises ToolError if the hash does not match (conflict).
915946 """
916- current_config , current_hash = await self ._get_automation_config_internal (identifier )
947+ current_config , current_hash = await self ._get_automation_config_internal (
948+ identifier
949+ )
917950 if current_hash != config_hash :
918951 raise_tool_error (
919952 create_error_response (
@@ -934,22 +967,26 @@ def _parse_and_validate_config(config: str | dict[str, Any]) -> dict[str, Any]:
934967 try :
935968 parsed_config = parse_json_param (config , "config" )
936969 except ValueError as e :
937- raise_tool_error (create_error_response (
938- code = ErrorCode .VALIDATION_INVALID_JSON ,
939- message = f"Invalid config parameter: { e } " ,
940- suggestions = [
941- "Pass 'config' as a dict, not a JSON string, to avoid escaping issues." ,
942- "Check for JSON syntax errors: unquoted keys, trailing commas, or invalid escape sequences." ,
943- ],
944- context = {"parameter" : "config" },
945- ))
970+ raise_tool_error (
971+ create_error_response (
972+ code = ErrorCode .VALIDATION_INVALID_JSON ,
973+ message = f"Invalid config parameter: { e } " ,
974+ suggestions = [
975+ "Pass 'config' as a dict, not a JSON string, to avoid escaping issues." ,
976+ "Check for JSON syntax errors: unquoted keys, trailing commas, or invalid escape sequences." ,
977+ ],
978+ context = {"parameter" : "config" },
979+ )
980+ )
946981
947982 if parsed_config is None or not isinstance (parsed_config , dict ):
948- raise_tool_error (create_validation_error (
949- "Config parameter must be a JSON object" ,
950- parameter = "config" ,
951- details = f"Received type: { type (parsed_config ).__name__ } " ,
952- ))
983+ raise_tool_error (
984+ create_validation_error (
985+ "Config parameter must be a JSON object" ,
986+ parameter = "config" ,
987+ details = f"Received type: { type (parsed_config ).__name__ } " ,
988+ )
989+ )
953990
954991 return cast (dict [str , Any ], parsed_config )
955992
@@ -978,24 +1015,28 @@ def _validate_required_fields(
9781015 context : dict [str , Any ] = {"missing_fields" : missing_fields }
9791016 if identifier :
9801017 context ["identifier" ] = identifier
981- raise_tool_error (create_error_response (
982- code = ErrorCode .CONFIG_MISSING_REQUIRED_FIELDS ,
983- message = f"Missing required fields: { ', ' .join (missing_fields )} " ,
984- details = (
985- "Config contains 'sequence', which belongs to scripts. "
986- "Automations use 'trigger' and 'action'; scripts use 'sequence'."
987- ),
988- suggestions = [
989- "Did you mean ha_config_set_script? Scripts use 'sequence' directly." ,
990- "For an automation, replace 'sequence' with 'action' and add a 'trigger'." ,
991- ],
992- context = context ,
993- ))
994- raise_tool_error (create_config_error (
995- f"Missing required fields: { ', ' .join (missing_fields )} " ,
996- identifier = identifier ,
997- missing_fields = missing_fields ,
998- ))
1018+ raise_tool_error (
1019+ create_error_response (
1020+ code = ErrorCode .CONFIG_MISSING_REQUIRED_FIELDS ,
1021+ message = f"Missing required fields: { ', ' .join (missing_fields )} " ,
1022+ details = (
1023+ "Config contains 'sequence', which belongs to scripts. "
1024+ "Automations use 'trigger' and 'action'; scripts use 'sequence'."
1025+ ),
1026+ suggestions = [
1027+ "Did you mean ha_config_set_script? Scripts use 'sequence' directly." ,
1028+ "For an automation, replace 'sequence' with 'action' and add a 'trigger'." ,
1029+ ],
1030+ context = context ,
1031+ )
1032+ )
1033+ raise_tool_error (
1034+ create_config_error (
1035+ f"Missing required fields: { ', ' .join (missing_fields )} " ,
1036+ identifier = identifier ,
1037+ missing_fields = missing_fields ,
1038+ )
1039+ )
9991040
10001041 # Issue #1169: reject configs that wrap ``scene.create`` in an
10011042 # automation with no functional trigger. Models occasionally produce
@@ -1020,62 +1061,68 @@ def _validate_required_fields(
10201061 if _action_contains_scene_create (a )
10211062 ]
10221063 if scene_create_indices :
1023- raise_tool_error (create_error_response (
1024- code = ErrorCode .VALIDATION_INVALID_PARAMETER ,
1025- message = (
1026- "Empty trigger paired with a scene.create action — "
1027- "this automation can never fire. For a state snapshot "
1028- "of one or more entities, use ha_config_set_scene "
1029- "directly instead of wrapping scene.create in an "
1030- "automation."
1031- ),
1032- suggestions = [
1033- "ha_config_set_scene(scene_id='...', config={'name': "
1034- "'...', 'entities': {'<entity_id>': {...}}}) creates "
1035- "a scene without a trigger." ,
1036- "If the snapshot really should be the result of an "
1037- "event, add the trigger that should fire it and keep "
1038- "the automation." ,
1039- "For a state-derived value that recomputes when its "
1040- "inputs change, use "
1041- "ha_config_set_helper(helper_type='template') instead." ,
1042- ],
1043- context = {
1044- "scene_create_action_indices" : scene_create_indices ,
1045- "identifier" : identifier ,
1046- },
1047- ))
1064+ raise_tool_error (
1065+ create_error_response (
1066+ code = ErrorCode .VALIDATION_INVALID_PARAMETER ,
1067+ message = (
1068+ "Empty trigger paired with a scene.create action — "
1069+ "this automation can never fire. For a state snapshot "
1070+ "of one or more entities, use ha_config_set_scene "
1071+ "directly instead of wrapping scene.create in an "
1072+ "automation."
1073+ ),
1074+ suggestions = [
1075+ "ha_config_set_scene(scene_id='...', config={'name': "
1076+ "'...', 'entities': {'<entity_id>': {...}}}) creates "
1077+ "a scene without a trigger." ,
1078+ "If the snapshot really should be the result of an "
1079+ "event, add the trigger that should fire it and keep "
1080+ "the automation." ,
1081+ "For a state-derived value that recomputes when its "
1082+ "inputs change, use "
1083+ "ha_config_set_helper(helper_type='template') instead." ,
1084+ ],
1085+ context = {
1086+ "scene_create_action_indices" : scene_create_indices ,
1087+ "identifier" : identifier ,
1088+ },
1089+ )
1090+ )
10481091
10491092 # HA accepts conditions with 'platform' (trigger syntax) but then crashes
10501093 # with an unhelpful 500 rather than a 400 validation error.
10511094 for idx , cond in enumerate (coerce_to_list (config_dict .get ("condition" ))):
10521095 if not isinstance (cond , dict ):
10531096 continue
10541097 if "platform" in cond and "condition" not in cond :
1055- raise_tool_error (create_error_response (
1056- code = ErrorCode .VALIDATION_INVALID_PARAMETER ,
1057- message = (
1058- f"Condition at index { idx } uses 'platform' (trigger syntax). "
1059- "Conditions use 'condition', not 'platform'."
1060- ),
1061- suggestions = [
1062- f"Replace 'platform' with 'condition': "
1063- f"{{'condition': '{ cond ['platform' ]} ', ...}}" ,
1064- "Triggers use 'platform'; conditions use 'condition'." ,
1065- ],
1066- context = {"condition_index" : idx , "found_key" : "platform" },
1067- ))
1098+ raise_tool_error (
1099+ create_error_response (
1100+ code = ErrorCode .VALIDATION_INVALID_PARAMETER ,
1101+ message = (
1102+ f"Condition at index { idx } uses 'platform' (trigger syntax). "
1103+ "Conditions use 'condition', not 'platform'."
1104+ ),
1105+ suggestions = [
1106+ f"Replace 'platform' with 'condition': "
1107+ f"{{'condition': '{ cond ['platform' ]} ', ...}}" ,
1108+ "Triggers use 'platform'; conditions use 'condition'." ,
1109+ ],
1110+ context = {"condition_index" : idx , "found_key" : "platform" },
1111+ )
1112+ )
10681113
10691114 # Prevent duplicate creation when config contains an existing automation id
10701115 if identifier is None and "id" in config_dict :
10711116 existing_id = config_dict ["id" ]
1072- raise_tool_error (create_validation_error (
1073- f"Config contains 'id' field ('{ existing_id } ') but no identifier was provided. "
1074- "This would create a duplicate automation instead of updating the existing one." ,
1075- parameter = "identifier" ,
1076- details = f"To update, pass identifier='{ existing_id } ' (or the automation's entity_id). "
1077- "To create a genuinely new automation, remove the 'id' field from the config." ,
1078- ))
1117+ raise_tool_error (
1118+ create_validation_error (
1119+ f"Config contains 'id' field ('{ existing_id } ') but no identifier was provided. "
1120+ "This would create a duplicate automation instead of updating the existing one." ,
1121+ parameter = "identifier" ,
1122+ details = f"To update, pass identifier='{ existing_id } ' (or the automation's entity_id). "
1123+ "To create a genuinely new automation, remove the 'id' field from the config." ,
1124+ )
1125+ )
10791126
10801127 @tool (
10811128 name = "ha_config_remove_automation" ,
@@ -1140,7 +1187,9 @@ async def ha_config_remove_automation(
11401187 wait_bool = coerce_bool_param (wait , "wait" , default = True )
11411188 if wait_bool and entity_id_for_wait :
11421189 try :
1143- removed = await wait_for_entity_removed (self ._client , entity_id_for_wait )
1190+ removed = await wait_for_entity_removed (
1191+ self ._client , entity_id_for_wait
1192+ )
11441193 if not removed :
11451194 result .setdefault ("warnings" , []).append (
11461195 f"Deletion confirmed by API but { entity_id_for_wait } may still appear briefly."
0 commit comments