ha-mcp-dev 7.5.0.dev576__tar.gz → 7.5.0.dev577__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (114) hide show
  1. {ha_mcp_dev-7.5.0.dev576/src/ha_mcp_dev.egg-info → ha_mcp_dev-7.5.0.dev577}/PKG-INFO +1 -1
  2. {ha_mcp_dev-7.5.0.dev576 → ha_mcp_dev-7.5.0.dev577}/pyproject.toml +1 -2
  3. {ha_mcp_dev-7.5.0.dev576 → ha_mcp_dev-7.5.0.dev577}/src/ha_mcp/tools/best_practice_checker.py +34 -29
  4. {ha_mcp_dev-7.5.0.dev576 → ha_mcp_dev-7.5.0.dev577}/src/ha_mcp/tools/tools_bug_report.py +81 -59
  5. {ha_mcp_dev-7.5.0.dev576 → ha_mcp_dev-7.5.0.dev577}/src/ha_mcp/tools/tools_config_automations.py +243 -249
  6. {ha_mcp_dev-7.5.0.dev576 → ha_mcp_dev-7.5.0.dev577}/src/ha_mcp/tools/tools_config_entry_flow.py +315 -249
  7. {ha_mcp_dev-7.5.0.dev576 → ha_mcp_dev-7.5.0.dev577}/src/ha_mcp/tools/tools_service.py +67 -53
  8. {ha_mcp_dev-7.5.0.dev576 → ha_mcp_dev-7.5.0.dev577}/src/ha_mcp/tools/tools_updates.py +118 -69
  9. {ha_mcp_dev-7.5.0.dev576 → ha_mcp_dev-7.5.0.dev577}/src/ha_mcp/tools/tools_yaml_config.py +33 -22
  10. {ha_mcp_dev-7.5.0.dev576 → ha_mcp_dev-7.5.0.dev577}/src/ha_mcp/tools/util_helpers.py +437 -358
  11. {ha_mcp_dev-7.5.0.dev576 → ha_mcp_dev-7.5.0.dev577/src/ha_mcp_dev.egg-info}/PKG-INFO +1 -1
  12. {ha_mcp_dev-7.5.0.dev576 → ha_mcp_dev-7.5.0.dev577}/LICENSE +0 -0
  13. {ha_mcp_dev-7.5.0.dev576 → ha_mcp_dev-7.5.0.dev577}/MANIFEST.in +0 -0
  14. {ha_mcp_dev-7.5.0.dev576 → ha_mcp_dev-7.5.0.dev577}/README.md +0 -0
  15. {ha_mcp_dev-7.5.0.dev576 → ha_mcp_dev-7.5.0.dev577}/setup.cfg +0 -0
  16. {ha_mcp_dev-7.5.0.dev576 → ha_mcp_dev-7.5.0.dev577}/src/ha_mcp/__init__.py +0 -0
  17. {ha_mcp_dev-7.5.0.dev576 → ha_mcp_dev-7.5.0.dev577}/src/ha_mcp/__main__.py +0 -0
  18. {ha_mcp_dev-7.5.0.dev576 → ha_mcp_dev-7.5.0.dev577}/src/ha_mcp/_pypi_marker +0 -0
  19. {ha_mcp_dev-7.5.0.dev576 → ha_mcp_dev-7.5.0.dev577}/src/ha_mcp/_version.py +0 -0
  20. {ha_mcp_dev-7.5.0.dev576 → ha_mcp_dev-7.5.0.dev577}/src/ha_mcp/auth/__init__.py +0 -0
  21. {ha_mcp_dev-7.5.0.dev576 → ha_mcp_dev-7.5.0.dev577}/src/ha_mcp/auth/consent_form.py +0 -0
  22. {ha_mcp_dev-7.5.0.dev576 → ha_mcp_dev-7.5.0.dev577}/src/ha_mcp/auth/provider.py +0 -0
  23. {ha_mcp_dev-7.5.0.dev576 → ha_mcp_dev-7.5.0.dev577}/src/ha_mcp/client/__init__.py +0 -0
  24. {ha_mcp_dev-7.5.0.dev576 → ha_mcp_dev-7.5.0.dev577}/src/ha_mcp/client/rest_client.py +0 -0
  25. {ha_mcp_dev-7.5.0.dev576 → ha_mcp_dev-7.5.0.dev577}/src/ha_mcp/client/supervisor_client.py +0 -0
  26. {ha_mcp_dev-7.5.0.dev576 → ha_mcp_dev-7.5.0.dev577}/src/ha_mcp/client/websocket_client.py +0 -0
  27. {ha_mcp_dev-7.5.0.dev576 → ha_mcp_dev-7.5.0.dev577}/src/ha_mcp/client/websocket_listener.py +0 -0
  28. {ha_mcp_dev-7.5.0.dev576 → ha_mcp_dev-7.5.0.dev577}/src/ha_mcp/config.py +0 -0
  29. {ha_mcp_dev-7.5.0.dev576 → ha_mcp_dev-7.5.0.dev577}/src/ha_mcp/errors.py +0 -0
  30. {ha_mcp_dev-7.5.0.dev576 → ha_mcp_dev-7.5.0.dev577}/src/ha_mcp/py.typed +0 -0
  31. {ha_mcp_dev-7.5.0.dev576 → ha_mcp_dev-7.5.0.dev577}/src/ha_mcp/resources/skills-vendor/.claude/settings.json +0 -0
  32. {ha_mcp_dev-7.5.0.dev576 → ha_mcp_dev-7.5.0.dev577}/src/ha_mcp/resources/skills-vendor/.claude-plugin/marketplace.json +0 -0
  33. {ha_mcp_dev-7.5.0.dev576 → ha_mcp_dev-7.5.0.dev577}/src/ha_mcp/resources/skills-vendor/.claude-plugin/plugin.json +0 -0
  34. {ha_mcp_dev-7.5.0.dev576 → ha_mcp_dev-7.5.0.dev577}/src/ha_mcp/resources/skills-vendor/.github/ISSUE_TEMPLATE/skill-rca.md +0 -0
  35. {ha_mcp_dev-7.5.0.dev576 → ha_mcp_dev-7.5.0.dev577}/src/ha_mcp/resources/skills-vendor/.github/pull_request_template.md +0 -0
  36. {ha_mcp_dev-7.5.0.dev576 → ha_mcp_dev-7.5.0.dev577}/src/ha_mcp/resources/skills-vendor/AGENTS.md +0 -0
  37. {ha_mcp_dev-7.5.0.dev576 → ha_mcp_dev-7.5.0.dev577}/src/ha_mcp/resources/skills-vendor/CLAUDE.md +0 -0
  38. {ha_mcp_dev-7.5.0.dev576 → ha_mcp_dev-7.5.0.dev577}/src/ha_mcp/resources/skills-vendor/CONTRIBUTING.md +0 -0
  39. {ha_mcp_dev-7.5.0.dev576 → ha_mcp_dev-7.5.0.dev577}/src/ha_mcp/resources/skills-vendor/LICENSE +0 -0
  40. {ha_mcp_dev-7.5.0.dev576 → ha_mcp_dev-7.5.0.dev577}/src/ha_mcp/resources/skills-vendor/README.md +0 -0
  41. {ha_mcp_dev-7.5.0.dev576 → ha_mcp_dev-7.5.0.dev577}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/SKILL.md +0 -0
  42. {ha_mcp_dev-7.5.0.dev576 → ha_mcp_dev-7.5.0.dev577}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/evals/evals.json +0 -0
  43. {ha_mcp_dev-7.5.0.dev576 → ha_mcp_dev-7.5.0.dev577}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/automation-patterns.md +0 -0
  44. {ha_mcp_dev-7.5.0.dev576 → ha_mcp_dev-7.5.0.dev577}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-cards.md +0 -0
  45. {ha_mcp_dev-7.5.0.dev576 → ha_mcp_dev-7.5.0.dev577}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-guide.md +0 -0
  46. {ha_mcp_dev-7.5.0.dev576 → ha_mcp_dev-7.5.0.dev577}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/device-control.md +0 -0
  47. {ha_mcp_dev-7.5.0.dev576 → ha_mcp_dev-7.5.0.dev577}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/domain-docs.md +0 -0
  48. {ha_mcp_dev-7.5.0.dev576 → ha_mcp_dev-7.5.0.dev577}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/examples.yaml +0 -0
  49. {ha_mcp_dev-7.5.0.dev576 → ha_mcp_dev-7.5.0.dev577}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/helper-selection.md +0 -0
  50. {ha_mcp_dev-7.5.0.dev576 → ha_mcp_dev-7.5.0.dev577}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/safe-refactoring.md +0 -0
  51. {ha_mcp_dev-7.5.0.dev576 → ha_mcp_dev-7.5.0.dev577}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/template-guidelines.md +0 -0
  52. {ha_mcp_dev-7.5.0.dev576 → ha_mcp_dev-7.5.0.dev577}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/yaml-only-integrations.md +0 -0
  53. {ha_mcp_dev-7.5.0.dev576 → ha_mcp_dev-7.5.0.dev577}/src/ha_mcp/server.py +0 -0
  54. {ha_mcp_dev-7.5.0.dev576 → ha_mcp_dev-7.5.0.dev577}/src/ha_mcp/settings_ui.py +0 -0
  55. {ha_mcp_dev-7.5.0.dev576 → ha_mcp_dev-7.5.0.dev577}/src/ha_mcp/smoke_test.py +0 -0
  56. {ha_mcp_dev-7.5.0.dev576 → ha_mcp_dev-7.5.0.dev577}/src/ha_mcp/stdio_settings_sidecar.py +0 -0
  57. {ha_mcp_dev-7.5.0.dev576 → ha_mcp_dev-7.5.0.dev577}/src/ha_mcp/tools/__init__.py +0 -0
  58. {ha_mcp_dev-7.5.0.dev576 → ha_mcp_dev-7.5.0.dev577}/src/ha_mcp/tools/backup.py +0 -0
  59. {ha_mcp_dev-7.5.0.dev576 → ha_mcp_dev-7.5.0.dev577}/src/ha_mcp/tools/device_control.py +0 -0
  60. {ha_mcp_dev-7.5.0.dev576 → ha_mcp_dev-7.5.0.dev577}/src/ha_mcp/tools/enhanced.py +0 -0
  61. {ha_mcp_dev-7.5.0.dev576 → ha_mcp_dev-7.5.0.dev577}/src/ha_mcp/tools/helpers.py +0 -0
  62. {ha_mcp_dev-7.5.0.dev576 → ha_mcp_dev-7.5.0.dev577}/src/ha_mcp/tools/reference_validator.py +0 -0
  63. {ha_mcp_dev-7.5.0.dev576 → ha_mcp_dev-7.5.0.dev577}/src/ha_mcp/tools/registry.py +0 -0
  64. {ha_mcp_dev-7.5.0.dev576 → ha_mcp_dev-7.5.0.dev577}/src/ha_mcp/tools/smart_search.py +0 -0
  65. {ha_mcp_dev-7.5.0.dev576 → ha_mcp_dev-7.5.0.dev577}/src/ha_mcp/tools/tools_addons.py +0 -0
  66. {ha_mcp_dev-7.5.0.dev576 → ha_mcp_dev-7.5.0.dev577}/src/ha_mcp/tools/tools_areas.py +0 -0
  67. {ha_mcp_dev-7.5.0.dev576 → ha_mcp_dev-7.5.0.dev577}/src/ha_mcp/tools/tools_blueprints.py +0 -0
  68. {ha_mcp_dev-7.5.0.dev576 → ha_mcp_dev-7.5.0.dev577}/src/ha_mcp/tools/tools_calendar.py +0 -0
  69. {ha_mcp_dev-7.5.0.dev576 → ha_mcp_dev-7.5.0.dev577}/src/ha_mcp/tools/tools_camera.py +0 -0
  70. {ha_mcp_dev-7.5.0.dev576 → ha_mcp_dev-7.5.0.dev577}/src/ha_mcp/tools/tools_categories.py +0 -0
  71. {ha_mcp_dev-7.5.0.dev576 → ha_mcp_dev-7.5.0.dev577}/src/ha_mcp/tools/tools_code.py +0 -0
  72. {ha_mcp_dev-7.5.0.dev576 → ha_mcp_dev-7.5.0.dev577}/src/ha_mcp/tools/tools_config_dashboards.py +0 -0
  73. {ha_mcp_dev-7.5.0.dev576 → ha_mcp_dev-7.5.0.dev577}/src/ha_mcp/tools/tools_config_helpers.py +0 -0
  74. {ha_mcp_dev-7.5.0.dev576 → ha_mcp_dev-7.5.0.dev577}/src/ha_mcp/tools/tools_config_scenes.py +0 -0
  75. {ha_mcp_dev-7.5.0.dev576 → ha_mcp_dev-7.5.0.dev577}/src/ha_mcp/tools/tools_config_scripts.py +0 -0
  76. {ha_mcp_dev-7.5.0.dev576 → ha_mcp_dev-7.5.0.dev577}/src/ha_mcp/tools/tools_energy.py +0 -0
  77. {ha_mcp_dev-7.5.0.dev576 → ha_mcp_dev-7.5.0.dev577}/src/ha_mcp/tools/tools_entities.py +0 -0
  78. {ha_mcp_dev-7.5.0.dev576 → ha_mcp_dev-7.5.0.dev577}/src/ha_mcp/tools/tools_filesystem.py +0 -0
  79. {ha_mcp_dev-7.5.0.dev576 → ha_mcp_dev-7.5.0.dev577}/src/ha_mcp/tools/tools_groups.py +0 -0
  80. {ha_mcp_dev-7.5.0.dev576 → ha_mcp_dev-7.5.0.dev577}/src/ha_mcp/tools/tools_hacs.py +0 -0
  81. {ha_mcp_dev-7.5.0.dev576 → ha_mcp_dev-7.5.0.dev577}/src/ha_mcp/tools/tools_history.py +0 -0
  82. {ha_mcp_dev-7.5.0.dev576 → ha_mcp_dev-7.5.0.dev577}/src/ha_mcp/tools/tools_integrations.py +0 -0
  83. {ha_mcp_dev-7.5.0.dev576 → ha_mcp_dev-7.5.0.dev577}/src/ha_mcp/tools/tools_labels.py +0 -0
  84. {ha_mcp_dev-7.5.0.dev576 → ha_mcp_dev-7.5.0.dev577}/src/ha_mcp/tools/tools_mcp_component.py +0 -0
  85. {ha_mcp_dev-7.5.0.dev576 → ha_mcp_dev-7.5.0.dev577}/src/ha_mcp/tools/tools_registry.py +0 -0
  86. {ha_mcp_dev-7.5.0.dev576 → ha_mcp_dev-7.5.0.dev577}/src/ha_mcp/tools/tools_resources.py +0 -0
  87. {ha_mcp_dev-7.5.0.dev576 → ha_mcp_dev-7.5.0.dev577}/src/ha_mcp/tools/tools_search.py +0 -0
  88. {ha_mcp_dev-7.5.0.dev576 → ha_mcp_dev-7.5.0.dev577}/src/ha_mcp/tools/tools_services.py +0 -0
  89. {ha_mcp_dev-7.5.0.dev576 → ha_mcp_dev-7.5.0.dev577}/src/ha_mcp/tools/tools_system.py +0 -0
  90. {ha_mcp_dev-7.5.0.dev576 → ha_mcp_dev-7.5.0.dev577}/src/ha_mcp/tools/tools_todo.py +0 -0
  91. {ha_mcp_dev-7.5.0.dev576 → ha_mcp_dev-7.5.0.dev577}/src/ha_mcp/tools/tools_traces.py +0 -0
  92. {ha_mcp_dev-7.5.0.dev576 → ha_mcp_dev-7.5.0.dev577}/src/ha_mcp/tools/tools_utility.py +0 -0
  93. {ha_mcp_dev-7.5.0.dev576 → ha_mcp_dev-7.5.0.dev577}/src/ha_mcp/tools/tools_voice_assistant.py +0 -0
  94. {ha_mcp_dev-7.5.0.dev576 → ha_mcp_dev-7.5.0.dev577}/src/ha_mcp/tools/tools_zones.py +0 -0
  95. {ha_mcp_dev-7.5.0.dev576 → ha_mcp_dev-7.5.0.dev577}/src/ha_mcp/transforms/__init__.py +0 -0
  96. {ha_mcp_dev-7.5.0.dev576 → ha_mcp_dev-7.5.0.dev577}/src/ha_mcp/transforms/categorized_search.py +0 -0
  97. {ha_mcp_dev-7.5.0.dev576 → ha_mcp_dev-7.5.0.dev577}/src/ha_mcp/transforms/lite_docstrings.py +0 -0
  98. {ha_mcp_dev-7.5.0.dev576 → ha_mcp_dev-7.5.0.dev577}/src/ha_mcp/utils/__init__.py +0 -0
  99. {ha_mcp_dev-7.5.0.dev576 → ha_mcp_dev-7.5.0.dev577}/src/ha_mcp/utils/config_hash.py +0 -0
  100. {ha_mcp_dev-7.5.0.dev576 → ha_mcp_dev-7.5.0.dev577}/src/ha_mcp/utils/data_paths.py +0 -0
  101. {ha_mcp_dev-7.5.0.dev576 → ha_mcp_dev-7.5.0.dev577}/src/ha_mcp/utils/domain_handlers.py +0 -0
  102. {ha_mcp_dev-7.5.0.dev576 → ha_mcp_dev-7.5.0.dev577}/src/ha_mcp/utils/fuzzy_search.py +0 -0
  103. {ha_mcp_dev-7.5.0.dev576 → ha_mcp_dev-7.5.0.dev577}/src/ha_mcp/utils/kill_signal_diagnostics.py +0 -0
  104. {ha_mcp_dev-7.5.0.dev576 → ha_mcp_dev-7.5.0.dev577}/src/ha_mcp/utils/operation_manager.py +0 -0
  105. {ha_mcp_dev-7.5.0.dev576 → ha_mcp_dev-7.5.0.dev577}/src/ha_mcp/utils/python_sandbox.py +0 -0
  106. {ha_mcp_dev-7.5.0.dev576 → ha_mcp_dev-7.5.0.dev577}/src/ha_mcp/utils/usage_logger.py +0 -0
  107. {ha_mcp_dev-7.5.0.dev576 → ha_mcp_dev-7.5.0.dev577}/src/ha_mcp_dev.egg-info/SOURCES.txt +0 -0
  108. {ha_mcp_dev-7.5.0.dev576 → ha_mcp_dev-7.5.0.dev577}/src/ha_mcp_dev.egg-info/dependency_links.txt +0 -0
  109. {ha_mcp_dev-7.5.0.dev576 → ha_mcp_dev-7.5.0.dev577}/src/ha_mcp_dev.egg-info/entry_points.txt +0 -0
  110. {ha_mcp_dev-7.5.0.dev576 → ha_mcp_dev-7.5.0.dev577}/src/ha_mcp_dev.egg-info/requires.txt +0 -0
  111. {ha_mcp_dev-7.5.0.dev576 → ha_mcp_dev-7.5.0.dev577}/src/ha_mcp_dev.egg-info/top_level.txt +0 -0
  112. {ha_mcp_dev-7.5.0.dev576 → ha_mcp_dev-7.5.0.dev577}/tests/__init__.py +0 -0
  113. {ha_mcp_dev-7.5.0.dev576 → ha_mcp_dev-7.5.0.dev577}/tests/test_constants.py +0 -0
  114. {ha_mcp_dev-7.5.0.dev576 → ha_mcp_dev-7.5.0.dev577}/tests/test_env_manager.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ha-mcp-dev
3
- Version: 7.5.0.dev576
3
+ Version: 7.5.0.dev577
4
4
  Summary: Home Assistant MCP Server - Complete control of Home Assistant through MCP
5
5
  Author-email: Julien <github@qc-h.net>
6
6
  License: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "ha-mcp-dev"
7
- version = "7.5.0.dev576"
7
+ version = "7.5.0.dev577"
8
8
  description = "Home Assistant MCP Server - Complete control of Home Assistant through MCP"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.13,<3.14"
@@ -148,7 +148,6 @@ ignore = [
148
148
  "src/ha_mcp/tools/tools_search.py" = ["C901"]
149
149
  "src/ha_mcp/tools/tools_utility.py" = ["C901"]
150
150
  "src/ha_mcp/tools/smart_search.py" = ["C901"]
151
- "src/ha_mcp/tools/util_helpers.py" = ["C901"]
152
151
 
153
152
  [tool.pytest.ini_options]
154
153
  testpaths = ["tests"]
@@ -279,10 +279,7 @@ def _check_template_string(
279
279
  # reframes #695 from "enumerate bad shapes" to "surface every template
280
280
  # in a logic position". Specific detectors above keep their tailored
281
281
  # messages.
282
- if (
283
- len(warnings) == initial_count
284
- and _RE_ANY_TEMPLATE.search(template)
285
- ):
282
+ if len(warnings) == initial_count and _RE_ANY_TEMPLATE.search(template):
286
283
  warnings.append(
287
284
  f"Template detected in {position} — if this maps to a native option "
288
285
  "(`numeric_state`, `state`, `time`, `sun`, `zone`, `device`), use that "
@@ -304,9 +301,7 @@ def _check_choose_actions(
304
301
  _check_condition_templates(
305
302
  option.get("conditions", []), warnings, skill_prefix
306
303
  )
307
- _check_action_tree(
308
- option.get("sequence", []), warnings, skill_prefix
309
- )
304
+ _check_action_tree(option.get("sequence", []), warnings, skill_prefix)
310
305
 
311
306
 
312
307
  def _check_repeat_actions(
@@ -317,6 +312,31 @@ def _check_repeat_actions(
317
312
  _check_action_tree(repeat.get("sequence", []), warnings, skill_prefix)
318
313
 
319
314
 
315
+ def _check_control_flow_actions(
316
+ action: dict[str, Any], warnings: list[str], skill_prefix: str | None
317
+ ) -> None:
318
+ """Check choose/if/then/else/repeat/parallel sub-trees in a single action."""
319
+ if "choose" in action:
320
+ _check_choose_actions(action["choose"], warnings, skill_prefix)
321
+
322
+ if "if" in action:
323
+ _check_condition_templates(action["if"], warnings, skill_prefix)
324
+
325
+ for key in ("then", "else", "default"):
326
+ nested = action.get(key)
327
+ if isinstance(nested, list):
328
+ _check_action_tree(nested, warnings, skill_prefix)
329
+
330
+ if "repeat" in action and isinstance(action["repeat"], dict):
331
+ _check_repeat_actions(action["repeat"], warnings, skill_prefix)
332
+
333
+ # `parallel:` runs sub-actions concurrently — same shape as `sequence`,
334
+ # different semantics. Recurse so templates inside parallel branches
335
+ # are inspected the same as templates inside choose/repeat sequences.
336
+ if "parallel" in action and isinstance(action["parallel"], list):
337
+ _check_action_tree(action["parallel"], warnings, skill_prefix)
338
+
339
+
320
340
  def _check_action_tree(
321
341
  actions: Any, warnings: list[str], skill_prefix: str | None
322
342
  ) -> None:
@@ -359,26 +379,7 @@ def _check_action_tree(
359
379
  if isinstance(target, dict):
360
380
  _check_target_dict(target, warnings, skill_prefix)
361
381
 
362
- # Nested conditions in choose/if/repeat
363
- if "choose" in action:
364
- _check_choose_actions(action["choose"], warnings, skill_prefix)
365
-
366
- if "if" in action:
367
- _check_condition_templates(action["if"], warnings, skill_prefix)
368
-
369
- for key in ("then", "else", "default"):
370
- nested = action.get(key)
371
- if isinstance(nested, list):
372
- _check_action_tree(nested, warnings, skill_prefix)
373
-
374
- if "repeat" in action and isinstance(action["repeat"], dict):
375
- _check_repeat_actions(action["repeat"], warnings, skill_prefix)
376
-
377
- # `parallel:` runs sub-actions concurrently — same shape as `sequence`,
378
- # different semantics. Recurse so templates inside parallel branches
379
- # are inspected the same as templates inside choose/repeat sequences.
380
- if "parallel" in action and isinstance(action["parallel"], list):
381
- _check_action_tree(action["parallel"], warnings, skill_prefix)
382
+ _check_control_flow_actions(action, warnings, skill_prefix)
382
383
 
383
384
 
384
385
  def _check_service_template(
@@ -439,7 +440,9 @@ def _check_target_dict(
439
440
  f"hardcode the literal value instead. The self-reference is always "
440
441
  f"resolvable at write time, so the template adds runtime cost without "
441
442
  f"any flexibility."
442
- + _ref(skill_prefix, "template-guidelines.md#when-to-avoid-templates")
443
+ + _ref(
444
+ skill_prefix, "template-guidelines.md#when-to-avoid-templates"
445
+ )
443
446
  )
444
447
  else:
445
448
  warnings.append(
@@ -447,7 +450,9 @@ def _check_target_dict(
447
450
  f"or use a `choose` action with native conditions to dispatch to different "
448
451
  f"hardcoded targets. Templates in target fields fail silently if they "
449
452
  f"resolve to a non-existent entity."
450
- + _ref(skill_prefix, "template-guidelines.md#when-to-avoid-templates")
453
+ + _ref(
454
+ skill_prefix, "template-guidelines.md#when-to-avoid-templates"
455
+ )
451
456
  )
452
457
 
453
458
 
@@ -16,6 +16,7 @@ from urllib.parse import quote_plus
16
16
 
17
17
  import httpx
18
18
  from fastmcp import Context
19
+ from fastmcp.tools import tool
19
20
  from pydantic import Field
20
21
 
21
22
  from ha_mcp import __version__
@@ -28,7 +29,7 @@ from ..utils.usage_logger import (
28
29
  get_recent_logs,
29
30
  get_startup_logs,
30
31
  )
31
- from .helpers import log_tool_usage
32
+ from .helpers import log_tool_usage, register_tool_methods
32
33
  from .util_helpers import ANSI_ESCAPE_RE
33
34
 
34
35
  logger = logging.getLogger(__name__)
@@ -388,10 +389,66 @@ async def _fetch_addon_logs() -> str:
388
389
  return ""
389
390
 
390
391
 
391
- def register_bug_report_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
392
- """Register bug report tools with the MCP server."""
392
+ def _build_formatted_report(
393
+ diagnostic_info: dict[str, Any],
394
+ mcp_transport: str,
395
+ client_info: dict[str, str],
396
+ platform_info: dict[str, str],
397
+ config_toggles: dict[str, Any],
398
+ startup_logs: list[dict[str, Any]],
399
+ startup_log_summary: str,
400
+ recent_logs: list[dict[str, Any]],
401
+ log_summary: str,
402
+ addon_logs: str,
403
+ ) -> str:
404
+ report_lines = [
405
+ "=== ha-mcp Bug Report Info ===",
406
+ "",
407
+ f"ha-mcp Version: {diagnostic_info['ha_mcp_version']}",
408
+ f"Installation Method: {diagnostic_info['installation_method']}",
409
+ f"MCP Transport: {mcp_transport}",
410
+ f"MCP Client: {_format_client_info_for_template(client_info)}",
411
+ f"Operating System: {platform_info['os']} {platform_info['os_release']} ({platform_info['architecture']})",
412
+ f"Python Version: {platform_info['python_version']}",
413
+ f"Home Assistant Version: {diagnostic_info['home_assistant_version']}",
414
+ f"Connection Status: {diagnostic_info['connection_status']}",
415
+ f"Entity Count: {diagnostic_info['entity_count']}",
416
+ ]
417
+ if "location_name" in diagnostic_info:
418
+ report_lines.append(f"Location Name: {diagnostic_info['location_name']}")
419
+ if "time_zone" in diagnostic_info:
420
+ report_lines.append(f"Time Zone: {diagnostic_info['time_zone']}")
421
+ if config_toggles:
422
+ report_lines.extend(["", "=== ha-mcp Config Toggles ==="])
423
+ for key, value in config_toggles.items():
424
+ report_lines.append(f" {key}: {value}")
425
+ if startup_logs:
426
+ report_lines.extend(
427
+ [
428
+ "",
429
+ f"=== Startup Logs ({len(startup_logs)} entries) ===",
430
+ startup_log_summary,
431
+ ]
432
+ )
433
+ if recent_logs:
434
+ report_lines.extend(
435
+ [
436
+ "",
437
+ f"=== Recent Tool Calls ({len(recent_logs)} entries) ===",
438
+ log_summary,
439
+ ]
440
+ )
441
+ if addon_logs:
442
+ report_lines.extend(["", "=== Add-on Container Logs ===", addon_logs])
443
+ return "\n".join(report_lines)
444
+
393
445
 
394
- @mcp.tool(
446
+ class BugReportTools:
447
+ def __init__(self, client: Any) -> None:
448
+ self._client = client
449
+
450
+ @tool(
451
+ name="ha_report_issue",
395
452
  tags={"Utilities"},
396
453
  annotations={
397
454
  "idempotentHint": True,
@@ -401,6 +458,7 @@ def register_bug_report_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
401
458
  )
402
459
  @log_tool_usage
403
460
  async def ha_report_issue(
461
+ self,
404
462
  tool_call_count: Annotated[
405
463
  int,
406
464
  Field(
@@ -476,7 +534,7 @@ def register_bug_report_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
476
534
 
477
535
  # Try to get Home Assistant config and connection status
478
536
  try:
479
- config = await client.get_config()
537
+ config = await self._client.get_config()
480
538
  diagnostic_info["connection_status"] = "Connected"
481
539
  diagnostic_info["home_assistant_version"] = config.get("version", "Unknown")
482
540
  diagnostic_info["location_name"] = config.get("location_name", "Unknown")
@@ -487,7 +545,7 @@ def register_bug_report_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
487
545
 
488
546
  # Try to get entity count
489
547
  try:
490
- states = await client.get_states()
548
+ states = await self._client.get_states()
491
549
  if states:
492
550
  diagnostic_info["entity_count"] = len(states)
493
551
  except Exception as e:
@@ -511,59 +569,18 @@ def register_bug_report_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
511
569
  startup_log_summary = _format_startup_logs(startup_logs)
512
570
 
513
571
  # Build the formatted report
514
- report_lines = [
515
- "=== ha-mcp Bug Report Info ===",
516
- "",
517
- f"ha-mcp Version: {diagnostic_info['ha_mcp_version']}",
518
- f"Installation Method: {diagnostic_info['installation_method']}",
519
- f"MCP Transport: {mcp_transport}",
520
- f"MCP Client: {_format_client_info_for_template(client_info)}",
521
- f"Operating System: {platform_info['os']} {platform_info['os_release']} ({platform_info['architecture']})",
522
- f"Python Version: {platform_info['python_version']}",
523
- f"Home Assistant Version: {diagnostic_info['home_assistant_version']}",
524
- f"Connection Status: {diagnostic_info['connection_status']}",
525
- f"Entity Count: {diagnostic_info['entity_count']}",
526
- ]
527
-
528
- # Add optional fields if available
529
- if "location_name" in diagnostic_info:
530
- report_lines.append(f"Location Name: {diagnostic_info['location_name']}")
531
- if "time_zone" in diagnostic_info:
532
- report_lines.append(f"Time Zone: {diagnostic_info['time_zone']}")
533
-
534
- if config_toggles:
535
- report_lines.extend(["", "=== ha-mcp Config Toggles ==="])
536
- for key, value in config_toggles.items():
537
- report_lines.append(f" {key}: {value}")
538
-
539
- if startup_logs:
540
- report_lines.extend(
541
- [
542
- "",
543
- f"=== Startup Logs ({len(startup_logs)} entries) ===",
544
- startup_log_summary,
545
- ]
546
- )
547
-
548
- if recent_logs:
549
- report_lines.extend(
550
- [
551
- "",
552
- f"=== Recent Tool Calls ({len(recent_logs)} entries) ===",
553
- log_summary,
554
- ]
555
- )
556
-
557
- if addon_logs:
558
- report_lines.extend(
559
- [
560
- "",
561
- "=== Add-on Container Logs ===",
562
- addon_logs,
563
- ]
564
- )
565
-
566
- formatted_report = "\n".join(report_lines)
572
+ formatted_report = _build_formatted_report(
573
+ diagnostic_info,
574
+ mcp_transport,
575
+ client_info,
576
+ platform_info,
577
+ config_toggles,
578
+ startup_logs,
579
+ startup_log_summary,
580
+ recent_logs,
581
+ log_summary,
582
+ addon_logs,
583
+ )
567
584
 
568
585
  # Generate suggested title up-front so it can be folded into the
569
586
  # submission URLs as a `&title=` query param. This auto-fills the
@@ -673,6 +690,11 @@ def register_bug_report_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
673
690
  }
674
691
 
675
692
 
693
+ def register_bug_report_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
694
+ """Register bug report tools with the MCP server."""
695
+ register_tool_methods(mcp, BugReportTools(client))
696
+
697
+
676
698
  def _format_config_toggles_for_template(toggles: dict[str, Any]) -> str:
677
699
  """Render config toggle snapshot as a markdown bullet list.
678
700