Skip to content

Commit 376467b

Browse files
committed
chore: apply ruff format to PR-touched files (refs #1318)
Pre-existing format-debt on ``src/ha_mcp/tools/tools_config_automations.py`` became blocking once 4fd2852 added it to the PR's changed-files set — ``.github/workflows/pr.yml`` runs ``ruff format --check`` whole-file on every Python file in the PR diff, with the "grandfathered for untouched files" gate per the maintainer call on #1318. The 230-line diff on ``tools_config_automations.py`` is pure mechanical ``ruff format`` output: line-break placement on ``raise_tool_error(...)`` wrappers, multi-line argument splits, and dict-literal expansion. No logic, no semantic, no behavior change. Belongs to the #1318 sweep but has to land in this PR because the file is now in the touched set. The 1-line diff on ``test_poll_cadence_measurement.py`` (``isinstance(w, str) and w.startswith(...)`` collapsed to a single line) is the natural follow-on to the constant rename in 4fd2852 — the shorter name fits within 88 chars.
1 parent 4fd2852 commit 376467b

2 files changed

Lines changed: 142 additions & 92 deletions

File tree

src/ha_mcp/tools/tools_config_automations.py

Lines changed: 141 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -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."

tests/src/e2e/workflows/automation/test_poll_cadence_measurement.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -93,8 +93,7 @@ async def test_poll_cadence_p50_worst(
9393
# silently print VERDICT=VALIDATED.
9494
warnings = create_data.get("warnings") or []
9595
if any(
96-
isinstance(w, str)
97-
and w.startswith(NOT_VERIFIED_WARNING_PREFIX)
96+
isinstance(w, str) and w.startswith(NOT_VERIFIED_WARNING_PREFIX)
9897
for w in warnings
9998
):
10099
not_verified_count += 1

0 commit comments

Comments
 (0)