@@ -290,7 +290,9 @@ async def _resolve_automation_entity_id(self, identifier: str) -> str | None:
290290 ):
291291 return str (state ["entity_id" ])
292292 except Exception as e :
293- logger .debug (f"Failed to resolve entity_id for automation { identifier } : { e } " )
293+ logger .debug (
294+ f"Failed to resolve entity_id for automation { identifier } : { e } "
295+ )
294296 return None
295297
296298 @tool (
@@ -345,13 +347,17 @@ async def ha_config_get_automation(
345347 "Use ha_search_entities(domain_filter='automation') to list automations" ,
346348 ],
347349 )
348- normalized_config , config_hash = await self ._get_automation_config_internal (identifier )
350+ normalized_config , config_hash = await self ._get_automation_config_internal (
351+ identifier
352+ )
349353
350354 # Resolve entity_id and fetch category from entity registry
351355 # (injected after hash so transient registry failures don't affect the hash)
352356 entity_id = await self ._resolve_automation_entity_id (identifier )
353357 if entity_id :
354- cat_id = await fetch_entity_category (self ._client , entity_id , "automation" )
358+ cat_id = await fetch_entity_category (
359+ self ._client , entity_id , "automation"
360+ )
355361 if cat_id :
356362 normalized_config ["category" ] = cat_id
357363
@@ -655,7 +661,10 @@ async def ha_config_set_automation(
655661 "Provide the automation entity_id or unique_id" ,
656662 "Use ha_search_entities(domain_filter='automation') to find automations" ,
657663 ],
658- context = {"action" : "python_transform" , "identifier" : identifier },
664+ context = {
665+ "action" : "python_transform" ,
666+ "identifier" : identifier ,
667+ },
659668 )
660669 )
661670 if config_hash is None :
@@ -667,7 +676,10 @@ async def ha_config_set_automation(
667676 "Call ha_config_get_automation() first" ,
668677 "Use the config_hash from that response" ,
669678 ],
670- context = {"action" : "python_transform" , "identifier" : identifier },
679+ context = {
680+ "action" : "python_transform" ,
681+ "identifier" : identifier ,
682+ },
671683 )
672684 )
673685
@@ -686,7 +698,10 @@ async def ha_config_set_automation(
686698 ErrorCode .VALIDATION_FAILED ,
687699 message ,
688700 suggestions = suggestions ,
689- context = {"action" : "python_transform" , "identifier" : identifier },
701+ context = {
702+ "action" : "python_transform" ,
703+ "identifier" : identifier ,
704+ },
690705 )
691706 )
692707
@@ -709,11 +724,20 @@ async def ha_config_set_automation(
709724
710725 # Re-apply category if present
711726 entity_id = result .get ("entity_id" )
712- if not entity_id and identifier and identifier .startswith ("automation." ):
727+ if (
728+ not entity_id
729+ and identifier
730+ and identifier .startswith ("automation." )
731+ ):
713732 entity_id = identifier
714733 if transform_category and entity_id :
715734 await apply_entity_category (
716- self ._client , entity_id , transform_category , "automation" , result , "automation"
735+ self ._client ,
736+ entity_id ,
737+ transform_category ,
738+ "automation" ,
739+ result ,
740+ "automation" ,
717741 )
718742
719743 response : dict [str , Any ] = {
@@ -773,7 +797,9 @@ async def ha_config_set_automation(
773797 self ._client , config_dict
774798 )
775799
776- result = await self ._client .upsert_automation_config (config_dict , identifier )
800+ result = await self ._client .upsert_automation_config (
801+ config_dict , identifier
802+ )
777803
778804 # If the client could not verify the entity was registered, warn but don't hard-fail.
779805 if result .get ("entity_not_verified" ):
@@ -795,7 +821,9 @@ async def ha_config_set_automation(
795821 if wait_bool and entity_id :
796822 action_word = "created" if identifier is None else "updated"
797823 try :
798- registered = await wait_for_entity_registered (self ._client , entity_id )
824+ registered = await wait_for_entity_registered (
825+ self ._client , entity_id
826+ )
799827 if not registered :
800828 result .setdefault ("warnings" , []).append (
801829 f"Automation { action_word } but { entity_id } not yet queryable. "
@@ -809,7 +837,12 @@ async def ha_config_set_automation(
809837 # Apply category to entity registry if provided
810838 if effective_category and entity_id :
811839 await apply_entity_category (
812- self ._client , entity_id , effective_category , "automation" , result , "automation"
840+ self ._client ,
841+ entity_id ,
842+ effective_category ,
843+ "automation" ,
844+ result ,
845+ "automation" ,
813846 )
814847
815848 if bp_warnings :
@@ -870,7 +903,9 @@ async def _fetch_and_verify_hash(
870903 Returns the current normalized config dict.
871904 Raises ToolError if the hash does not match (conflict).
872905 """
873- current_config , current_hash = await self ._get_automation_config_internal (identifier )
906+ current_config , current_hash = await self ._get_automation_config_internal (
907+ identifier
908+ )
874909 if current_hash != config_hash :
875910 raise_tool_error (
876911 create_error_response (
@@ -891,22 +926,26 @@ def _parse_and_validate_config(config: str | dict[str, Any]) -> dict[str, Any]:
891926 try :
892927 parsed_config = parse_json_param (config , "config" )
893928 except ValueError as e :
894- raise_tool_error (create_error_response (
895- code = ErrorCode .VALIDATION_INVALID_JSON ,
896- message = f"Invalid config parameter: { e } " ,
897- suggestions = [
898- "Pass 'config' as a dict, not a JSON string, to avoid escaping issues." ,
899- "Check for JSON syntax errors: unquoted keys, trailing commas, or invalid escape sequences." ,
900- ],
901- context = {"parameter" : "config" },
902- ))
929+ raise_tool_error (
930+ create_error_response (
931+ code = ErrorCode .VALIDATION_INVALID_JSON ,
932+ message = f"Invalid config parameter: { e } " ,
933+ suggestions = [
934+ "Pass 'config' as a dict, not a JSON string, to avoid escaping issues." ,
935+ "Check for JSON syntax errors: unquoted keys, trailing commas, or invalid escape sequences." ,
936+ ],
937+ context = {"parameter" : "config" },
938+ )
939+ )
903940
904941 if parsed_config is None or not isinstance (parsed_config , dict ):
905- raise_tool_error (create_validation_error (
906- "Config parameter must be a JSON object" ,
907- parameter = "config" ,
908- details = f"Received type: { type (parsed_config ).__name__ } " ,
909- ))
942+ raise_tool_error (
943+ create_validation_error (
944+ "Config parameter must be a JSON object" ,
945+ parameter = "config" ,
946+ details = f"Received type: { type (parsed_config ).__name__ } " ,
947+ )
948+ )
910949
911950 return cast (dict [str , Any ], parsed_config )
912951
@@ -935,24 +974,28 @@ def _validate_required_fields(
935974 context : dict [str , Any ] = {"missing_fields" : missing_fields }
936975 if identifier :
937976 context ["identifier" ] = identifier
938- raise_tool_error (create_error_response (
939- code = ErrorCode .CONFIG_MISSING_REQUIRED_FIELDS ,
940- message = f"Missing required fields: { ', ' .join (missing_fields )} " ,
941- details = (
942- "Config contains 'sequence', which belongs to scripts. "
943- "Automations use 'trigger' and 'action'; scripts use 'sequence'."
944- ),
945- suggestions = [
946- "Did you mean ha_config_set_script? Scripts use 'sequence' directly." ,
947- "For an automation, replace 'sequence' with 'action' and add a 'trigger'." ,
948- ],
949- context = context ,
950- ))
951- raise_tool_error (create_config_error (
952- f"Missing required fields: { ', ' .join (missing_fields )} " ,
953- identifier = identifier ,
954- missing_fields = missing_fields ,
955- ))
977+ raise_tool_error (
978+ create_error_response (
979+ code = ErrorCode .CONFIG_MISSING_REQUIRED_FIELDS ,
980+ message = f"Missing required fields: { ', ' .join (missing_fields )} " ,
981+ details = (
982+ "Config contains 'sequence', which belongs to scripts. "
983+ "Automations use 'trigger' and 'action'; scripts use 'sequence'."
984+ ),
985+ suggestions = [
986+ "Did you mean ha_config_set_script? Scripts use 'sequence' directly." ,
987+ "For an automation, replace 'sequence' with 'action' and add a 'trigger'." ,
988+ ],
989+ context = context ,
990+ )
991+ )
992+ raise_tool_error (
993+ create_config_error (
994+ f"Missing required fields: { ', ' .join (missing_fields )} " ,
995+ identifier = identifier ,
996+ missing_fields = missing_fields ,
997+ )
998+ )
956999
9571000 # Issue #1169: reject configs that wrap ``scene.create`` in an
9581001 # automation with no functional trigger. Models occasionally produce
@@ -977,62 +1020,68 @@ def _validate_required_fields(
9771020 if _action_contains_scene_create (a )
9781021 ]
9791022 if scene_create_indices :
980- raise_tool_error (create_error_response (
981- code = ErrorCode .VALIDATION_INVALID_PARAMETER ,
982- message = (
983- "Empty trigger paired with a scene.create action — "
984- "this automation can never fire. For a state snapshot "
985- "of one or more entities, use ha_config_set_scene "
986- "directly instead of wrapping scene.create in an "
987- "automation."
988- ),
989- suggestions = [
990- "ha_config_set_scene(scene_id='...', config={'name': "
991- "'...', 'entities': {'<entity_id>': {...}}}) creates "
992- "a scene without a trigger." ,
993- "If the snapshot really should be the result of an "
994- "event, add the trigger that should fire it and keep "
995- "the automation." ,
996- "For a state-derived value that recomputes when its "
997- "inputs change, use "
998- "ha_config_set_helper(helper_type='template') instead." ,
999- ],
1000- context = {
1001- "scene_create_action_indices" : scene_create_indices ,
1002- "identifier" : identifier ,
1003- },
1004- ))
1023+ raise_tool_error (
1024+ create_error_response (
1025+ code = ErrorCode .VALIDATION_INVALID_PARAMETER ,
1026+ message = (
1027+ "Empty trigger paired with a scene.create action — "
1028+ "this automation can never fire. For a state snapshot "
1029+ "of one or more entities, use ha_config_set_scene "
1030+ "directly instead of wrapping scene.create in an "
1031+ "automation."
1032+ ),
1033+ suggestions = [
1034+ "ha_config_set_scene(scene_id='...', config={'name': "
1035+ "'...', 'entities': {'<entity_id>': {...}}}) creates "
1036+ "a scene without a trigger." ,
1037+ "If the snapshot really should be the result of an "
1038+ "event, add the trigger that should fire it and keep "
1039+ "the automation." ,
1040+ "For a state-derived value that recomputes when its "
1041+ "inputs change, use "
1042+ "ha_config_set_helper(helper_type='template') instead." ,
1043+ ],
1044+ context = {
1045+ "scene_create_action_indices" : scene_create_indices ,
1046+ "identifier" : identifier ,
1047+ },
1048+ )
1049+ )
10051050
10061051 # HA accepts conditions with 'platform' (trigger syntax) but then crashes
10071052 # with an unhelpful 500 rather than a 400 validation error.
10081053 for idx , cond in enumerate (coerce_to_list (config_dict .get ("condition" ))):
10091054 if not isinstance (cond , dict ):
10101055 continue
10111056 if "platform" in cond and "condition" not in cond :
1012- raise_tool_error (create_error_response (
1013- code = ErrorCode .VALIDATION_INVALID_PARAMETER ,
1014- message = (
1015- f"Condition at index { idx } uses 'platform' (trigger syntax). "
1016- "Conditions use 'condition', not 'platform'."
1017- ),
1018- suggestions = [
1019- f"Replace 'platform' with 'condition': "
1020- f"{{'condition': '{ cond ['platform' ]} ', ...}}" ,
1021- "Triggers use 'platform'; conditions use 'condition'." ,
1022- ],
1023- context = {"condition_index" : idx , "found_key" : "platform" },
1024- ))
1057+ raise_tool_error (
1058+ create_error_response (
1059+ code = ErrorCode .VALIDATION_INVALID_PARAMETER ,
1060+ message = (
1061+ f"Condition at index { idx } uses 'platform' (trigger syntax). "
1062+ "Conditions use 'condition', not 'platform'."
1063+ ),
1064+ suggestions = [
1065+ f"Replace 'platform' with 'condition': "
1066+ f"{{'condition': '{ cond ['platform' ]} ', ...}}" ,
1067+ "Triggers use 'platform'; conditions use 'condition'." ,
1068+ ],
1069+ context = {"condition_index" : idx , "found_key" : "platform" },
1070+ )
1071+ )
10251072
10261073 # Prevent duplicate creation when config contains an existing automation id
10271074 if identifier is None and "id" in config_dict :
10281075 existing_id = config_dict ["id" ]
1029- raise_tool_error (create_validation_error (
1030- f"Config contains 'id' field ('{ existing_id } ') but no identifier was provided. "
1031- "This would create a duplicate automation instead of updating the existing one." ,
1032- parameter = "identifier" ,
1033- details = f"To update, pass identifier='{ existing_id } ' (or the automation's entity_id). "
1034- "To create a genuinely new automation, remove the 'id' field from the config." ,
1035- ))
1076+ raise_tool_error (
1077+ create_validation_error (
1078+ f"Config contains 'id' field ('{ existing_id } ') but no identifier was provided. "
1079+ "This would create a duplicate automation instead of updating the existing one." ,
1080+ parameter = "identifier" ,
1081+ details = f"To update, pass identifier='{ existing_id } ' (or the automation's entity_id). "
1082+ "To create a genuinely new automation, remove the 'id' field from the config." ,
1083+ )
1084+ )
10361085
10371086 @tool (
10381087 name = "ha_config_remove_automation" ,
@@ -1097,7 +1146,9 @@ async def ha_config_remove_automation(
10971146 wait_bool = coerce_bool_param (wait , "wait" , default = True )
10981147 if wait_bool and entity_id_for_wait :
10991148 try :
1100- removed = await wait_for_entity_removed (self ._client , entity_id_for_wait )
1149+ removed = await wait_for_entity_removed (
1150+ self ._client , entity_id_for_wait
1151+ )
11011152 if not removed :
11021153 result .setdefault ("warnings" , []).append (
11031154 f"Deletion confirmed by API but { entity_id_for_wait } may still appear briefly."
0 commit comments