Skip to content

Commit 6063a4b

Browse files
julienldclaude
andauthored
refactor: split registry.py into focused modules (2106 → 76 lines) (#21)
* refactor: split registry.py into focused modules (2106 → 76 lines) Split the monolithic registry.py (2106 lines) into 7 specialized modules following a flat directory structure with clear separation between tools and utilities: **New Modules:** - util_helpers.py (99 lines) - Shared helper functions - parse_json_param() - JSON parameter parsing - parse_string_list_param() - String list parsing - add_timezone_metadata() - Timezone metadata wrapper - tools_search.py (324 lines) - 4 search/discovery tools - ha_search_entities, ha_get_overview, ha_deep_search, ha_get_state - tools_service.py (242 lines) - 4 service call/operation tools - ha_call_service, ha_get_operation_status, ha_bulk_control, ha_get_bulk_status - tools_config_helpers.py (678 lines) - 3 helper config management tools - ha_config_list_helpers, ha_config_set_helper, ha_config_remove_helper - tools_config_scripts.py (232 lines) - 3 script config management tools - ha_config_get_script, ha_config_set_script, ha_config_remove_script - tools_config_automations.py (257 lines) - 3 automation config management tools - ha_config_get_automation, ha_config_set_automation, ha_config_remove_automation - tools_utility.py (343 lines) - 3 utility tools - ha_get_logbook, ha_eval_template, ha_get_domain_docs **Refactored registry.py (76 lines, 96% reduction):** - Now acts as orchestrator-only - Imports registration functions from specialized modules - Clean separation of concerns - Maintains 3 convenience delegator tools inline (simple one-liners) **Architecture Benefits:** - Clear module boundaries based on tool categories - Shared utilities extracted to util_helpers.py - Easier to navigate and maintain - Improved testability - Consistent tools_* prefix for tool modules **Verification:** - All syntax validated - Import chain tested successfully - ToolsRegistry instantiation confirmed - All 7 registration functions imported correctly Addresses tech debt issue: Split registry.py into Separate Modules 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * chore: update uv.lock after refactoring --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 012559c commit 6063a4b

9 files changed

Lines changed: 2186 additions & 2063 deletions

src/ha_mcp/tools/registry.py

Lines changed: 31 additions & 2062 deletions
Large diffs are not rendered by default.
Lines changed: 257 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,257 @@
1+
"""
2+
Configuration management tools for Home Assistant automations.
3+
4+
This module provides tools for retrieving, creating, updating, and removing
5+
Home Assistant automation configurations.
6+
"""
7+
8+
import logging
9+
from typing import Annotated, Any, cast
10+
11+
from pydantic import Field
12+
13+
from .helpers import log_tool_usage
14+
from .util_helpers import parse_json_param
15+
16+
logger = logging.getLogger(__name__)
17+
18+
19+
def register_config_automation_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
20+
"""Register Home Assistant automation configuration tools."""
21+
22+
@mcp.tool
23+
@log_tool_usage
24+
async def ha_config_get_automation(
25+
identifier: Annotated[
26+
str,
27+
Field(
28+
description="Automation entity_id (e.g., 'automation.morning_routine') or unique_id"
29+
),
30+
],
31+
) -> dict[str, Any]:
32+
"""
33+
Retrieve Home Assistant automation configuration.
34+
35+
Returns the complete configuration including triggers, conditions, actions, and mode settings.
36+
37+
EXAMPLES:
38+
- Get automation: ha_config_get_automation("automation.morning_routine")
39+
- Get by unique_id: ha_config_get_automation("my_unique_automation_id")
40+
41+
For comprehensive automation documentation, use: ha_get_domain_docs("automation")
42+
"""
43+
try:
44+
config_result = await client.get_automation_config(identifier)
45+
return {
46+
"success": True,
47+
"action": "get",
48+
"identifier": identifier,
49+
"config": config_result,
50+
}
51+
except Exception as e:
52+
# Handle 404 errors gracefully (often used to verify deletion)
53+
error_str = str(e)
54+
if (
55+
"404" in error_str
56+
or "not found" in error_str.lower()
57+
or "entity not found" in error_str.lower()
58+
):
59+
logger.debug(
60+
f"Automation {identifier} not found (expected for deletion verification)"
61+
)
62+
return {
63+
"success": False,
64+
"action": "get",
65+
"identifier": identifier,
66+
"error": f"Automation {identifier} does not exist",
67+
"reason": "not_found",
68+
}
69+
70+
logger.error(f"Error getting automation: {e}")
71+
return {
72+
"success": False,
73+
"action": "get",
74+
"identifier": identifier,
75+
"error": str(e),
76+
"suggestions": [
77+
"Verify automation exists using ha_search_entities(domain_filter='automation')",
78+
"Check Home Assistant connection",
79+
"Use ha_get_domain_docs('automation') for configuration help",
80+
],
81+
}
82+
83+
@mcp.tool
84+
@log_tool_usage
85+
async def ha_config_set_automation(
86+
config: Annotated[
87+
str | dict[str, Any],
88+
Field(
89+
description="Complete automation configuration with required fields: 'alias', 'trigger', 'action'. Optional: 'description', 'condition', 'mode', 'max', 'initial_state', 'variables'"
90+
),
91+
],
92+
identifier: Annotated[
93+
str | None,
94+
Field(
95+
description="Automation entity_id or unique_id for updates. Omit to create new automation with generated unique_id.",
96+
default=None,
97+
),
98+
] = None,
99+
) -> dict[str, Any]:
100+
"""
101+
Create or update a Home Assistant automation.
102+
103+
Creates a new automation (if identifier omitted) or updates existing automation with provided configuration.
104+
105+
REQUIRED CONFIG FIELDS:
106+
- alias: Human-readable automation name
107+
- trigger: List of trigger conditions (time, state, event, etc.)
108+
- action: List of actions to execute
109+
110+
OPTIONAL CONFIG FIELDS:
111+
- description: Detailed description
112+
- condition: Additional conditions that must be met
113+
- mode: 'single' (default), 'restart', 'queued', 'parallel'
114+
- max: Maximum concurrent executions (for queued/parallel modes)
115+
- initial_state: Whether automation starts enabled (true/false)
116+
- variables: Variables for use in automation
117+
118+
BASIC EXAMPLES:
119+
120+
Simple time-based automation:
121+
ha_config_set_automation({
122+
"alias": "Morning Lights",
123+
"trigger": [{"platform": "time", "at": "07:00:00"}],
124+
"action": [{"service": "light.turn_on", "target": {"area_id": "bedroom"}}]
125+
})
126+
127+
Motion-activated lighting with condition:
128+
ha_config_set_automation({
129+
"alias": "Motion Light",
130+
"trigger": [{"platform": "state", "entity_id": "binary_sensor.motion", "to": "on"}],
131+
"condition": [{"condition": "sun", "after": "sunset"}],
132+
"action": [
133+
{"service": "light.turn_on", "target": {"entity_id": "light.hallway"}},
134+
{"delay": {"minutes": 5}},
135+
{"service": "light.turn_off", "target": {"entity_id": "light.hallway"}}
136+
],
137+
"mode": "restart"
138+
})
139+
140+
Update existing automation:
141+
ha_config_set_automation(
142+
identifier="automation.morning_routine",
143+
config={
144+
"alias": "Updated Morning Routine",
145+
"trigger": [{"platform": "time", "at": "06:30:00"}],
146+
"action": [
147+
{"service": "light.turn_on", "target": {"area_id": "bedroom"}},
148+
{"service": "climate.set_temperature", "target": {"entity_id": "climate.bedroom"}, "data": {"temperature": 22}}
149+
]
150+
}
151+
)
152+
153+
TRIGGER TYPES: time, time_pattern, sun, state, numeric_state, event, device, zone, template, and more
154+
CONDITION TYPES: state, numeric_state, time, sun, template, device, zone, and more
155+
ACTION TYPES: service calls, delays, wait_for_trigger, wait_template, if/then/else, choose, repeat, parallel
156+
157+
For comprehensive automation documentation with all trigger/condition/action types and advanced examples:
158+
- Use: ha_get_domain_docs("automation")
159+
- Or visit: https://www.home-assistant.io/docs/automation/
160+
161+
TROUBLESHOOTING:
162+
- Use ha_get_state() to verify entity_ids exist
163+
- Use ha_search_entities() to find correct entity_ids
164+
- Use ha_eval_template() to test Jinja2 templates before using in automations
165+
- Use ha_search_entities(domain_filter='automation') to find existing automations
166+
"""
167+
try:
168+
# Parse JSON config if provided as string
169+
try:
170+
parsed_config = parse_json_param(config, "config")
171+
except ValueError as e:
172+
return {
173+
"success": False,
174+
"error": f"Invalid config parameter: {e}",
175+
"provided_config_type": type(config).__name__,
176+
}
177+
178+
# Ensure config is a dict
179+
if parsed_config is None or not isinstance(parsed_config, dict):
180+
return {
181+
"success": False,
182+
"error": "Config parameter must be a JSON object",
183+
"provided_type": type(parsed_config).__name__,
184+
}
185+
186+
config_dict = cast(dict[str, Any], parsed_config)
187+
188+
# Validate required fields
189+
required_fields = ["alias", "trigger", "action"]
190+
missing_fields = [f for f in required_fields if f not in config_dict]
191+
if missing_fields:
192+
return {
193+
"success": False,
194+
"error": f"Missing required fields: {', '.join(missing_fields)}",
195+
"required_fields": required_fields,
196+
"missing_fields": missing_fields,
197+
}
198+
199+
result = await client.upsert_automation_config(
200+
config_dict, identifier
201+
)
202+
return {
203+
"success": True,
204+
**result,
205+
"config_provided": config_dict,
206+
}
207+
208+
except Exception as e:
209+
logger.error(f"Error upserting automation: {e}")
210+
return {
211+
"success": False,
212+
"identifier": identifier,
213+
"error": str(e),
214+
"suggestions": [
215+
"Check automation configuration format",
216+
"Ensure required fields: alias, trigger, action",
217+
"Use entity_id format: automation.morning_routine or unique_id",
218+
"Use ha_search_entities(domain_filter='automation') to find automations",
219+
"Use ha_get_domain_docs('automation') for comprehensive configuration help",
220+
],
221+
}
222+
223+
@mcp.tool
224+
@log_tool_usage
225+
async def ha_config_remove_automation(
226+
identifier: Annotated[
227+
str,
228+
Field(
229+
description="Automation entity_id (e.g., 'automation.old_automation') or unique_id to delete"
230+
),
231+
],
232+
) -> dict[str, Any]:
233+
"""
234+
Delete a Home Assistant automation.
235+
236+
EXAMPLES:
237+
- Delete automation: ha_config_remove_automation("automation.old_automation")
238+
- Delete by unique_id: ha_config_remove_automation("my_unique_id")
239+
240+
**WARNING:** Deleting an automation removes it permanently from your Home Assistant configuration.
241+
"""
242+
try:
243+
result = await client.delete_automation_config(identifier)
244+
return {"success": True, "action": "delete", **result}
245+
except Exception as e:
246+
logger.error(f"Error deleting automation: {e}")
247+
return {
248+
"success": False,
249+
"action": "delete",
250+
"identifier": identifier,
251+
"error": str(e),
252+
"suggestions": [
253+
"Verify automation exists using ha_search_entities(domain_filter='automation')",
254+
"Use entity_id format: automation.morning_routine or unique_id",
255+
"Check Home Assistant connection",
256+
],
257+
}

0 commit comments

Comments
 (0)