ha-mcp-dev 7.5.0.dev540__tar.gz → 7.5.0.dev541__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 (111) hide show
  1. {ha_mcp_dev-7.5.0.dev540/src/ha_mcp_dev.egg-info → ha_mcp_dev-7.5.0.dev541}/PKG-INFO +1 -1
  2. {ha_mcp_dev-7.5.0.dev540 → ha_mcp_dev-7.5.0.dev541}/pyproject.toml +1 -1
  3. {ha_mcp_dev-7.5.0.dev540 → ha_mcp_dev-7.5.0.dev541}/src/ha_mcp/tools/tools_integrations.py +187 -2
  4. {ha_mcp_dev-7.5.0.dev540 → ha_mcp_dev-7.5.0.dev541}/src/ha_mcp/tools/tools_system.py +144 -6
  5. {ha_mcp_dev-7.5.0.dev540 → ha_mcp_dev-7.5.0.dev541}/src/ha_mcp/tools/util_helpers.py +378 -0
  6. {ha_mcp_dev-7.5.0.dev540 → ha_mcp_dev-7.5.0.dev541/src/ha_mcp_dev.egg-info}/PKG-INFO +1 -1
  7. {ha_mcp_dev-7.5.0.dev540 → ha_mcp_dev-7.5.0.dev541}/LICENSE +0 -0
  8. {ha_mcp_dev-7.5.0.dev540 → ha_mcp_dev-7.5.0.dev541}/MANIFEST.in +0 -0
  9. {ha_mcp_dev-7.5.0.dev540 → ha_mcp_dev-7.5.0.dev541}/README.md +0 -0
  10. {ha_mcp_dev-7.5.0.dev540 → ha_mcp_dev-7.5.0.dev541}/setup.cfg +0 -0
  11. {ha_mcp_dev-7.5.0.dev540 → ha_mcp_dev-7.5.0.dev541}/src/ha_mcp/__init__.py +0 -0
  12. {ha_mcp_dev-7.5.0.dev540 → ha_mcp_dev-7.5.0.dev541}/src/ha_mcp/__main__.py +0 -0
  13. {ha_mcp_dev-7.5.0.dev540 → ha_mcp_dev-7.5.0.dev541}/src/ha_mcp/_pypi_marker +0 -0
  14. {ha_mcp_dev-7.5.0.dev540 → ha_mcp_dev-7.5.0.dev541}/src/ha_mcp/_version.py +0 -0
  15. {ha_mcp_dev-7.5.0.dev540 → ha_mcp_dev-7.5.0.dev541}/src/ha_mcp/auth/__init__.py +0 -0
  16. {ha_mcp_dev-7.5.0.dev540 → ha_mcp_dev-7.5.0.dev541}/src/ha_mcp/auth/consent_form.py +0 -0
  17. {ha_mcp_dev-7.5.0.dev540 → ha_mcp_dev-7.5.0.dev541}/src/ha_mcp/auth/provider.py +0 -0
  18. {ha_mcp_dev-7.5.0.dev540 → ha_mcp_dev-7.5.0.dev541}/src/ha_mcp/client/__init__.py +0 -0
  19. {ha_mcp_dev-7.5.0.dev540 → ha_mcp_dev-7.5.0.dev541}/src/ha_mcp/client/rest_client.py +0 -0
  20. {ha_mcp_dev-7.5.0.dev540 → ha_mcp_dev-7.5.0.dev541}/src/ha_mcp/client/supervisor_client.py +0 -0
  21. {ha_mcp_dev-7.5.0.dev540 → ha_mcp_dev-7.5.0.dev541}/src/ha_mcp/client/websocket_client.py +0 -0
  22. {ha_mcp_dev-7.5.0.dev540 → ha_mcp_dev-7.5.0.dev541}/src/ha_mcp/client/websocket_listener.py +0 -0
  23. {ha_mcp_dev-7.5.0.dev540 → ha_mcp_dev-7.5.0.dev541}/src/ha_mcp/config.py +0 -0
  24. {ha_mcp_dev-7.5.0.dev540 → ha_mcp_dev-7.5.0.dev541}/src/ha_mcp/errors.py +0 -0
  25. {ha_mcp_dev-7.5.0.dev540 → ha_mcp_dev-7.5.0.dev541}/src/ha_mcp/py.typed +0 -0
  26. {ha_mcp_dev-7.5.0.dev540 → ha_mcp_dev-7.5.0.dev541}/src/ha_mcp/resources/skills-vendor/.claude/settings.json +0 -0
  27. {ha_mcp_dev-7.5.0.dev540 → ha_mcp_dev-7.5.0.dev541}/src/ha_mcp/resources/skills-vendor/.claude-plugin/marketplace.json +0 -0
  28. {ha_mcp_dev-7.5.0.dev540 → ha_mcp_dev-7.5.0.dev541}/src/ha_mcp/resources/skills-vendor/.claude-plugin/plugin.json +0 -0
  29. {ha_mcp_dev-7.5.0.dev540 → ha_mcp_dev-7.5.0.dev541}/src/ha_mcp/resources/skills-vendor/.github/ISSUE_TEMPLATE/skill-rca.md +0 -0
  30. {ha_mcp_dev-7.5.0.dev540 → ha_mcp_dev-7.5.0.dev541}/src/ha_mcp/resources/skills-vendor/AGENTS.md +0 -0
  31. {ha_mcp_dev-7.5.0.dev540 → ha_mcp_dev-7.5.0.dev541}/src/ha_mcp/resources/skills-vendor/CLAUDE.md +0 -0
  32. {ha_mcp_dev-7.5.0.dev540 → ha_mcp_dev-7.5.0.dev541}/src/ha_mcp/resources/skills-vendor/CONTRIBUTING.md +0 -0
  33. {ha_mcp_dev-7.5.0.dev540 → ha_mcp_dev-7.5.0.dev541}/src/ha_mcp/resources/skills-vendor/LICENSE +0 -0
  34. {ha_mcp_dev-7.5.0.dev540 → ha_mcp_dev-7.5.0.dev541}/src/ha_mcp/resources/skills-vendor/README.md +0 -0
  35. {ha_mcp_dev-7.5.0.dev540 → ha_mcp_dev-7.5.0.dev541}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/SKILL.md +0 -0
  36. {ha_mcp_dev-7.5.0.dev540 → ha_mcp_dev-7.5.0.dev541}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/evals/evals.json +0 -0
  37. {ha_mcp_dev-7.5.0.dev540 → ha_mcp_dev-7.5.0.dev541}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/automation-patterns.md +0 -0
  38. {ha_mcp_dev-7.5.0.dev540 → ha_mcp_dev-7.5.0.dev541}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-cards.md +0 -0
  39. {ha_mcp_dev-7.5.0.dev540 → ha_mcp_dev-7.5.0.dev541}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-guide.md +0 -0
  40. {ha_mcp_dev-7.5.0.dev540 → ha_mcp_dev-7.5.0.dev541}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/device-control.md +0 -0
  41. {ha_mcp_dev-7.5.0.dev540 → ha_mcp_dev-7.5.0.dev541}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/domain-docs.md +0 -0
  42. {ha_mcp_dev-7.5.0.dev540 → ha_mcp_dev-7.5.0.dev541}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/examples.yaml +0 -0
  43. {ha_mcp_dev-7.5.0.dev540 → ha_mcp_dev-7.5.0.dev541}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/helper-selection.md +0 -0
  44. {ha_mcp_dev-7.5.0.dev540 → ha_mcp_dev-7.5.0.dev541}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/safe-refactoring.md +0 -0
  45. {ha_mcp_dev-7.5.0.dev540 → ha_mcp_dev-7.5.0.dev541}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/template-guidelines.md +0 -0
  46. {ha_mcp_dev-7.5.0.dev540 → ha_mcp_dev-7.5.0.dev541}/src/ha_mcp/server.py +0 -0
  47. {ha_mcp_dev-7.5.0.dev540 → ha_mcp_dev-7.5.0.dev541}/src/ha_mcp/settings_ui.py +0 -0
  48. {ha_mcp_dev-7.5.0.dev540 → ha_mcp_dev-7.5.0.dev541}/src/ha_mcp/smoke_test.py +0 -0
  49. {ha_mcp_dev-7.5.0.dev540 → ha_mcp_dev-7.5.0.dev541}/src/ha_mcp/tools/__init__.py +0 -0
  50. {ha_mcp_dev-7.5.0.dev540 → ha_mcp_dev-7.5.0.dev541}/src/ha_mcp/tools/backup.py +0 -0
  51. {ha_mcp_dev-7.5.0.dev540 → ha_mcp_dev-7.5.0.dev541}/src/ha_mcp/tools/best_practice_checker.py +0 -0
  52. {ha_mcp_dev-7.5.0.dev540 → ha_mcp_dev-7.5.0.dev541}/src/ha_mcp/tools/device_control.py +0 -0
  53. {ha_mcp_dev-7.5.0.dev540 → ha_mcp_dev-7.5.0.dev541}/src/ha_mcp/tools/enhanced.py +0 -0
  54. {ha_mcp_dev-7.5.0.dev540 → ha_mcp_dev-7.5.0.dev541}/src/ha_mcp/tools/helpers.py +0 -0
  55. {ha_mcp_dev-7.5.0.dev540 → ha_mcp_dev-7.5.0.dev541}/src/ha_mcp/tools/reference_validator.py +0 -0
  56. {ha_mcp_dev-7.5.0.dev540 → ha_mcp_dev-7.5.0.dev541}/src/ha_mcp/tools/registry.py +0 -0
  57. {ha_mcp_dev-7.5.0.dev540 → ha_mcp_dev-7.5.0.dev541}/src/ha_mcp/tools/smart_search.py +0 -0
  58. {ha_mcp_dev-7.5.0.dev540 → ha_mcp_dev-7.5.0.dev541}/src/ha_mcp/tools/tools_addons.py +0 -0
  59. {ha_mcp_dev-7.5.0.dev540 → ha_mcp_dev-7.5.0.dev541}/src/ha_mcp/tools/tools_areas.py +0 -0
  60. {ha_mcp_dev-7.5.0.dev540 → ha_mcp_dev-7.5.0.dev541}/src/ha_mcp/tools/tools_blueprints.py +0 -0
  61. {ha_mcp_dev-7.5.0.dev540 → ha_mcp_dev-7.5.0.dev541}/src/ha_mcp/tools/tools_bug_report.py +0 -0
  62. {ha_mcp_dev-7.5.0.dev540 → ha_mcp_dev-7.5.0.dev541}/src/ha_mcp/tools/tools_calendar.py +0 -0
  63. {ha_mcp_dev-7.5.0.dev540 → ha_mcp_dev-7.5.0.dev541}/src/ha_mcp/tools/tools_camera.py +0 -0
  64. {ha_mcp_dev-7.5.0.dev540 → ha_mcp_dev-7.5.0.dev541}/src/ha_mcp/tools/tools_categories.py +0 -0
  65. {ha_mcp_dev-7.5.0.dev540 → ha_mcp_dev-7.5.0.dev541}/src/ha_mcp/tools/tools_code.py +0 -0
  66. {ha_mcp_dev-7.5.0.dev540 → ha_mcp_dev-7.5.0.dev541}/src/ha_mcp/tools/tools_config_automations.py +0 -0
  67. {ha_mcp_dev-7.5.0.dev540 → ha_mcp_dev-7.5.0.dev541}/src/ha_mcp/tools/tools_config_dashboards.py +0 -0
  68. {ha_mcp_dev-7.5.0.dev540 → ha_mcp_dev-7.5.0.dev541}/src/ha_mcp/tools/tools_config_entry_flow.py +0 -0
  69. {ha_mcp_dev-7.5.0.dev540 → ha_mcp_dev-7.5.0.dev541}/src/ha_mcp/tools/tools_config_helpers.py +0 -0
  70. {ha_mcp_dev-7.5.0.dev540 → ha_mcp_dev-7.5.0.dev541}/src/ha_mcp/tools/tools_config_scenes.py +0 -0
  71. {ha_mcp_dev-7.5.0.dev540 → ha_mcp_dev-7.5.0.dev541}/src/ha_mcp/tools/tools_config_scripts.py +0 -0
  72. {ha_mcp_dev-7.5.0.dev540 → ha_mcp_dev-7.5.0.dev541}/src/ha_mcp/tools/tools_energy.py +0 -0
  73. {ha_mcp_dev-7.5.0.dev540 → ha_mcp_dev-7.5.0.dev541}/src/ha_mcp/tools/tools_entities.py +0 -0
  74. {ha_mcp_dev-7.5.0.dev540 → ha_mcp_dev-7.5.0.dev541}/src/ha_mcp/tools/tools_filesystem.py +0 -0
  75. {ha_mcp_dev-7.5.0.dev540 → ha_mcp_dev-7.5.0.dev541}/src/ha_mcp/tools/tools_groups.py +0 -0
  76. {ha_mcp_dev-7.5.0.dev540 → ha_mcp_dev-7.5.0.dev541}/src/ha_mcp/tools/tools_hacs.py +0 -0
  77. {ha_mcp_dev-7.5.0.dev540 → ha_mcp_dev-7.5.0.dev541}/src/ha_mcp/tools/tools_history.py +0 -0
  78. {ha_mcp_dev-7.5.0.dev540 → ha_mcp_dev-7.5.0.dev541}/src/ha_mcp/tools/tools_labels.py +0 -0
  79. {ha_mcp_dev-7.5.0.dev540 → ha_mcp_dev-7.5.0.dev541}/src/ha_mcp/tools/tools_mcp_component.py +0 -0
  80. {ha_mcp_dev-7.5.0.dev540 → ha_mcp_dev-7.5.0.dev541}/src/ha_mcp/tools/tools_registry.py +0 -0
  81. {ha_mcp_dev-7.5.0.dev540 → ha_mcp_dev-7.5.0.dev541}/src/ha_mcp/tools/tools_resources.py +0 -0
  82. {ha_mcp_dev-7.5.0.dev540 → ha_mcp_dev-7.5.0.dev541}/src/ha_mcp/tools/tools_search.py +0 -0
  83. {ha_mcp_dev-7.5.0.dev540 → ha_mcp_dev-7.5.0.dev541}/src/ha_mcp/tools/tools_service.py +0 -0
  84. {ha_mcp_dev-7.5.0.dev540 → ha_mcp_dev-7.5.0.dev541}/src/ha_mcp/tools/tools_services.py +0 -0
  85. {ha_mcp_dev-7.5.0.dev540 → ha_mcp_dev-7.5.0.dev541}/src/ha_mcp/tools/tools_todo.py +0 -0
  86. {ha_mcp_dev-7.5.0.dev540 → ha_mcp_dev-7.5.0.dev541}/src/ha_mcp/tools/tools_traces.py +0 -0
  87. {ha_mcp_dev-7.5.0.dev540 → ha_mcp_dev-7.5.0.dev541}/src/ha_mcp/tools/tools_updates.py +0 -0
  88. {ha_mcp_dev-7.5.0.dev540 → ha_mcp_dev-7.5.0.dev541}/src/ha_mcp/tools/tools_utility.py +0 -0
  89. {ha_mcp_dev-7.5.0.dev540 → ha_mcp_dev-7.5.0.dev541}/src/ha_mcp/tools/tools_voice_assistant.py +0 -0
  90. {ha_mcp_dev-7.5.0.dev540 → ha_mcp_dev-7.5.0.dev541}/src/ha_mcp/tools/tools_yaml_config.py +0 -0
  91. {ha_mcp_dev-7.5.0.dev540 → ha_mcp_dev-7.5.0.dev541}/src/ha_mcp/tools/tools_zones.py +0 -0
  92. {ha_mcp_dev-7.5.0.dev540 → ha_mcp_dev-7.5.0.dev541}/src/ha_mcp/transforms/__init__.py +0 -0
  93. {ha_mcp_dev-7.5.0.dev540 → ha_mcp_dev-7.5.0.dev541}/src/ha_mcp/transforms/categorized_search.py +0 -0
  94. {ha_mcp_dev-7.5.0.dev540 → ha_mcp_dev-7.5.0.dev541}/src/ha_mcp/transforms/lite_docstrings.py +0 -0
  95. {ha_mcp_dev-7.5.0.dev540 → ha_mcp_dev-7.5.0.dev541}/src/ha_mcp/utils/__init__.py +0 -0
  96. {ha_mcp_dev-7.5.0.dev540 → ha_mcp_dev-7.5.0.dev541}/src/ha_mcp/utils/config_hash.py +0 -0
  97. {ha_mcp_dev-7.5.0.dev540 → ha_mcp_dev-7.5.0.dev541}/src/ha_mcp/utils/data_paths.py +0 -0
  98. {ha_mcp_dev-7.5.0.dev540 → ha_mcp_dev-7.5.0.dev541}/src/ha_mcp/utils/domain_handlers.py +0 -0
  99. {ha_mcp_dev-7.5.0.dev540 → ha_mcp_dev-7.5.0.dev541}/src/ha_mcp/utils/fuzzy_search.py +0 -0
  100. {ha_mcp_dev-7.5.0.dev540 → ha_mcp_dev-7.5.0.dev541}/src/ha_mcp/utils/kill_signal_diagnostics.py +0 -0
  101. {ha_mcp_dev-7.5.0.dev540 → ha_mcp_dev-7.5.0.dev541}/src/ha_mcp/utils/operation_manager.py +0 -0
  102. {ha_mcp_dev-7.5.0.dev540 → ha_mcp_dev-7.5.0.dev541}/src/ha_mcp/utils/python_sandbox.py +0 -0
  103. {ha_mcp_dev-7.5.0.dev540 → ha_mcp_dev-7.5.0.dev541}/src/ha_mcp/utils/usage_logger.py +0 -0
  104. {ha_mcp_dev-7.5.0.dev540 → ha_mcp_dev-7.5.0.dev541}/src/ha_mcp_dev.egg-info/SOURCES.txt +0 -0
  105. {ha_mcp_dev-7.5.0.dev540 → ha_mcp_dev-7.5.0.dev541}/src/ha_mcp_dev.egg-info/dependency_links.txt +0 -0
  106. {ha_mcp_dev-7.5.0.dev540 → ha_mcp_dev-7.5.0.dev541}/src/ha_mcp_dev.egg-info/entry_points.txt +0 -0
  107. {ha_mcp_dev-7.5.0.dev540 → ha_mcp_dev-7.5.0.dev541}/src/ha_mcp_dev.egg-info/requires.txt +0 -0
  108. {ha_mcp_dev-7.5.0.dev540 → ha_mcp_dev-7.5.0.dev541}/src/ha_mcp_dev.egg-info/top_level.txt +0 -0
  109. {ha_mcp_dev-7.5.0.dev540 → ha_mcp_dev-7.5.0.dev541}/tests/__init__.py +0 -0
  110. {ha_mcp_dev-7.5.0.dev540 → ha_mcp_dev-7.5.0.dev541}/tests/test_constants.py +0 -0
  111. {ha_mcp_dev-7.5.0.dev540 → ha_mcp_dev-7.5.0.dev541}/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.dev540
3
+ Version: 7.5.0.dev541
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.dev540"
7
+ version = "7.5.0.dev541"
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"
@@ -35,7 +35,9 @@ from .util_helpers import (
35
35
  build_pagination_metadata,
36
36
  coerce_bool_param,
37
37
  coerce_int_param,
38
+ fetch_integration_diagnostics,
38
39
  get_logger_levels,
40
+ parse_diagnostics_fields,
39
41
  wait_for_entity_removed,
40
42
  )
41
43
 
@@ -219,6 +221,114 @@ class IntegrationTools:
219
221
  description="Number of entries to skip for pagination (default: 0)",
220
222
  ),
221
223
  ] = 0,
224
+ include_diagnostics: Annotated[
225
+ bool | str,
226
+ Field(
227
+ description=(
228
+ "When entry_id is set, also fetch the integration's diagnostics "
229
+ "dump — integration-defined JSON (commonly includes redacted "
230
+ "config, device list, state snapshots; exact top-level keys "
231
+ "vary by integration). The canonical artifact users grab via "
232
+ "Settings → Devices & Services → [integration] → ⋯ → Download "
233
+ "diagnostics. Use when triaging integration bugs or filing "
234
+ "ha_report_issue for a specific integration. Payloads can be "
235
+ "large (Hue ~290 KB, ZHA/MQTT/ESPHome several MB) — pair with "
236
+ "diagnostics_fields or diagnostics_truncate_at_bytes to fit "
237
+ "the LLM context budget."
238
+ ),
239
+ default=False,
240
+ ),
241
+ ] = False,
242
+ device_id: Annotated[
243
+ str | None,
244
+ Field(
245
+ description=(
246
+ "Optional. When set with include_diagnostics=True, returns the "
247
+ "device-scoped diagnostics dump for that specific device under "
248
+ "the integration (rather than the full integration dump). Some "
249
+ "integrations only expose config-entry-level dumps; others "
250
+ "expose both."
251
+ ),
252
+ default=None,
253
+ ),
254
+ ] = None,
255
+ diagnostics_fields: Annotated[
256
+ list[str] | str | None,
257
+ Field(
258
+ description=(
259
+ "Optional list of top-level keys to keep from the diagnostics "
260
+ "data payload (e.g. ['home_assistant', 'issues']). Trims the "
261
+ "payload before it hits the LLM context budget. Accepts a JSON "
262
+ "list or comma-separated string. Only applies when "
263
+ "include_diagnostics=True and the data payload is a dict. "
264
+ "Unknown keys are silently dropped and surfaced via the "
265
+ "omitted_fields sub-key."
266
+ ),
267
+ default=None,
268
+ ),
269
+ ] = None,
270
+ diagnostics_truncate_at_bytes: Annotated[
271
+ int | str | None,
272
+ Field(
273
+ description=(
274
+ "Optional byte cap on the serialized diagnostics payload "
275
+ "(after diagnostics_fields and diagnostics_data_path have "
276
+ "been applied). On hit, drops data and emits truncated=true, "
277
+ "bytes_total, byte_cap, plus available_fields (when the "
278
+ "capped value is a dict). Recommended starting point: "
279
+ "20000 bytes. Only applies when include_diagnostics=True."
280
+ ),
281
+ default=None,
282
+ ),
283
+ ] = None,
284
+ diagnostics_data_path: Annotated[
285
+ str | None,
286
+ Field(
287
+ description=(
288
+ "Optional dotted path into the diagnostics data sub-tree "
289
+ "(e.g. '<list-valued path>' for per-device records, "
290
+ "'home_assistant.version' for HA core version; the exact "
291
+ "key path varies by integration version). Walks into the "
292
+ "post-fields payload. Resolution failures replace data "
293
+ "with null and surface data_path_error. Use this when the "
294
+ "interesting payload lives several levels deep — top-level "
295
+ "diagnostics_fields can't address sub-trees on integrations "
296
+ "where the bulk lives under one key (ZHA, MQTT, ESPHome). "
297
+ "Only applies when include_diagnostics=True."
298
+ ),
299
+ default=None,
300
+ ),
301
+ ] = None,
302
+ diagnostics_data_offset: Annotated[
303
+ int | str | None,
304
+ Field(
305
+ description=(
306
+ "Pagination start index (default 0) for list-valued "
307
+ "diagnostics_data_path results. Ignored when "
308
+ "diagnostics_data_path is unset, diagnostics_data_limit is "
309
+ "unset, or the resolved value is not a list. Only applies "
310
+ "when include_diagnostics=True."
311
+ ),
312
+ default=0,
313
+ ),
314
+ ] = 0,
315
+ diagnostics_data_limit: Annotated[
316
+ int | str | None,
317
+ Field(
318
+ description=(
319
+ "Pagination window size for list-valued "
320
+ "diagnostics_data_path results. When set with a "
321
+ "list-resolved path, swaps data for a pagination envelope "
322
+ "{path, items, offset, limit, total, has_more}. Default "
323
+ "None returns the full resolved value. Workflow: probe "
324
+ "with a list-valued diagnostics_data_path and "
325
+ "diagnostics_data_limit=10 to walk a large list one page "
326
+ "at a time (the exact path varies by integration version). "
327
+ "Only applies when include_diagnostics=True."
328
+ ),
329
+ default=None,
330
+ ),
331
+ ] = None,
222
332
  ) -> dict[str, Any]:
223
333
  """Get integration (config entry) information with pagination.
224
334
 
@@ -231,6 +341,10 @@ class IntegrationTools:
231
341
  - Search: ha_get_integration(query="zigbee")
232
342
  - Get specific entry: ha_get_integration(entry_id="abc123")
233
343
  - Get entry with editable fields: ha_get_integration(entry_id="abc123", include_schema=True)
344
+ - Get entry with diagnostics dump: ha_get_integration(entry_id="abc123", include_diagnostics=True)
345
+ - Get device-scoped diagnostics: ha_get_integration(entry_id="abc123", include_diagnostics=True, device_id="dev123")
346
+ - Walk a sub-tree: ha_get_integration(entry_id="abc123", include_diagnostics=True, diagnostics_data_path="<dotted-path>")
347
+ - Paginate a large list: ha_get_integration(entry_id="abc123", include_diagnostics=True, diagnostics_data_path="<list-valued path>", diagnostics_data_limit=10, diagnostics_data_offset=20)
234
348
  - List template entries: ha_get_integration(domain="template")
235
349
 
236
350
  STATES: 'loaded', 'setup_error', 'setup_retry', 'not_loaded',
@@ -256,6 +370,9 @@ class IntegrationTools:
256
370
  include_schema_bool = coerce_bool_param(
257
371
  include_schema, "include_schema", default=False
258
372
  )
373
+ include_diagnostics_bool = coerce_bool_param(
374
+ include_diagnostics, "include_diagnostics", default=False
375
+ )
259
376
  exact_match_bool = coerce_bool_param(
260
377
  exact_match, "exact_match", default=True
261
378
  )
@@ -263,18 +380,86 @@ class IntegrationTools:
263
380
  limit, "limit", default=50, min_value=1, max_value=200
264
381
  )
265
382
  offset_int = coerce_int_param(offset, "offset", default=0, min_value=0)
383
+ fields_list = parse_diagnostics_fields(diagnostics_fields)
384
+ truncate_bytes = coerce_int_param(
385
+ diagnostics_truncate_at_bytes,
386
+ "diagnostics_truncate_at_bytes",
387
+ default=None,
388
+ min_value=1,
389
+ )
390
+ data_offset_int = coerce_int_param(
391
+ diagnostics_data_offset,
392
+ "diagnostics_data_offset",
393
+ default=0,
394
+ min_value=0,
395
+ )
396
+ data_limit_int = coerce_int_param(
397
+ diagnostics_data_limit,
398
+ "diagnostics_data_limit",
399
+ default=None,
400
+ min_value=1,
401
+ )
402
+ # Type-guard ``diagnostics_data_path`` here so a bad caller (dict /
403
+ # list) surfaces as ``VALIDATION_INVALID_PARAMETER`` instead of
404
+ # leaking as ``INTERNAL_ERROR`` from the resolver's ``.strip()``
405
+ # downstream. Mirrors the coerce_int_param guards above.
406
+ if diagnostics_data_path is not None and not isinstance(
407
+ diagnostics_data_path, str
408
+ ):
409
+ raise_tool_error(
410
+ create_error_response(
411
+ ErrorCode.VALIDATION_INVALID_PARAMETER,
412
+ "diagnostics_data_path must be a string, got "
413
+ f"{type(diagnostics_data_path).__name__}",
414
+ context={"parameter": "diagnostics_data_path"},
415
+ )
416
+ )
266
417
  # Auto-enable options when domain filter is set
267
418
  if domain is not None:
268
419
  include_opts = True
269
420
 
270
421
  # If entry_id provided, get specific config entry
271
422
  if entry_id is not None:
272
- return await self._get_single_entry(entry_id, include_schema_bool)
423
+ resp = await self._get_single_entry(entry_id, include_schema_bool)
424
+ if include_diagnostics_bool:
425
+ resp["diagnostics"] = await fetch_integration_diagnostics(
426
+ self._client,
427
+ entry_id,
428
+ device_id,
429
+ fields=fields_list,
430
+ truncate_at_bytes=truncate_bytes,
431
+ data_path=diagnostics_data_path,
432
+ data_offset=data_offset_int,
433
+ data_limit=data_limit_int,
434
+ )
435
+ elif device_id is not None:
436
+ resp.setdefault("warnings", []).append(
437
+ "device_id was provided but ignored because "
438
+ "include_diagnostics=False"
439
+ )
440
+ return resp
273
441
 
274
442
  # List mode - get all config entries
275
- return await self._list_entries(
443
+ result = await self._list_entries(
276
444
  domain, query, include_opts, exact_match_bool, limit_int, offset_int
277
445
  )
446
+ if (
447
+ include_diagnostics_bool
448
+ or device_id is not None
449
+ or fields_list is not None
450
+ or truncate_bytes is not None
451
+ or diagnostics_data_path is not None
452
+ or data_limit_int is not None
453
+ or data_offset_int > 0
454
+ ):
455
+ result.setdefault("warnings", []).append(
456
+ "include_diagnostics, device_id, diagnostics_fields, "
457
+ "diagnostics_truncate_at_bytes, diagnostics_data_path, "
458
+ "diagnostics_data_offset, and/or diagnostics_data_limit "
459
+ "were provided but ignored because entry_id was not set "
460
+ "(list mode)"
461
+ )
462
+ return result
278
463
 
279
464
  except ToolError:
280
465
  raise
@@ -23,7 +23,13 @@ from .helpers import (
23
23
  raise_tool_error,
24
24
  register_tool_methods,
25
25
  )
26
- from .util_helpers import coerce_bool_param, filter_active_repairs
26
+ from .util_helpers import (
27
+ coerce_bool_param,
28
+ coerce_int_param,
29
+ fetch_integration_diagnostics,
30
+ filter_active_repairs,
31
+ parse_diagnostics_fields,
32
+ )
27
33
 
28
34
  logger = logging.getLogger(__name__)
29
35
 
@@ -334,17 +340,24 @@ class SystemTools:
334
340
 
335
341
  @tool(
336
342
  name="ha_get_system_health",
337
- tags={"System", "Zigbee", "Z-Wave"},
338
- annotations={"idempotentHint": True, "readOnlyHint": True, "title": "Get System Health (incl. ZHA/Z-Wave diagnostics)"},
343
+ tags={"System", "Zigbee", "Z-Wave", "Integrations"},
344
+ annotations={"idempotentHint": True, "readOnlyHint": True, "title": "Get System Health (incl. ZHA/Z-Wave/integration diagnostics)"},
339
345
  )
340
346
  @log_tool_usage
341
347
  async def ha_get_system_health(
342
348
  self,
343
349
  include: str | None = None,
344
350
  include_dismissed_repairs: bool | str | None = False,
351
+ config_entry_id: str | None = None,
352
+ device_id: str | None = None,
353
+ diagnostics_fields: list[str] | str | None = None,
354
+ diagnostics_truncate_at_bytes: int | str | None = None,
355
+ diagnostics_data_path: str | None = None,
356
+ diagnostics_data_offset: int | str | None = 0,
357
+ diagnostics_data_limit: int | str | None = None,
345
358
  ) -> dict[str, Any]:
346
359
  """
347
- Get Home Assistant system health, including Zigbee (ZHA) and Z-Wave JS network diagnostics.
360
+ Get Home Assistant system health, including Zigbee (ZHA), Z-Wave JS, and per-integration diagnostics dumps.
348
361
 
349
362
  Returns health check results from integrations, system resources, and connectivity.
350
363
  Available information varies by installation type and loaded integrations.
@@ -355,8 +368,50 @@ class SystemTools:
355
368
  - "zha_network": ZHA Zigbee devices with radio signal summary (name, LQI, RSSI)
356
369
  - "zha_network_full": ZHA Zigbee devices with all device details (can be large on 100+ device networks; prefer "zha_network" for summary)
357
370
  - "zwave_network": Z-Wave JS network status and node summary (status, security, routing)
371
+ - "diagnostics": Per-integration diagnostics dump — integration-defined JSON
372
+ (commonly includes redacted config, device list, state snapshots; exact
373
+ top-level keys vary by integration). REQUIRES ``config_entry_id``. The
374
+ canonical artifact users grab via Settings → Devices & Services →
375
+ [integration] → ⋯ → Download diagnostics. Use this when triaging integration
376
+ bugs or filing ``ha_report_issue`` for a specific integration. Payloads can
377
+ be large (Hue ~290 KB, ZHA/MQTT/ESPHome several MB) — pair with
378
+ ``diagnostics_fields`` or ``diagnostics_truncate_at_bytes`` to fit the LLM
379
+ context budget.
358
380
  - Example: include="repairs,zha_network,zwave_network"
381
+ - Example: include="diagnostics", config_entry_id="abc123..."
359
382
  - include_dismissed_repairs: Include user-dismissed/ignored repairs (default: False). Only meaningful when "repairs" is in `include`.
383
+ - config_entry_id: Required when ``include`` contains ``diagnostics``. The config
384
+ entry ID of the integration (find via ``ha_get_integration``).
385
+ - device_id: Optional. When set with ``include=diagnostics``, returns the
386
+ device-scoped diagnostics dump for that specific device under the integration
387
+ (rather than the full integration dump). Some integrations only expose
388
+ config-entry-level dumps; others expose both.
389
+ - diagnostics_fields: Optional list of top-level keys to keep from the
390
+ diagnostics ``data`` payload (e.g. ``["home_assistant", "issues"]``). Accepts
391
+ a JSON list or comma-separated string. Only applies with ``include=diagnostics``.
392
+ - diagnostics_truncate_at_bytes: Optional byte cap on the serialized
393
+ diagnostics payload (post-projection / post-data_path). On hit,
394
+ drops ``data`` and emits ``truncated=true``, ``bytes_total``,
395
+ ``byte_cap``, plus ``available_fields`` (when the capped value
396
+ is a dict). Only applies when ``include`` contains ``diagnostics``.
397
+ Recommended starting point: 20000 bytes.
398
+ - diagnostics_data_path: Optional dotted path into the diagnostics
399
+ ``data`` sub-tree (e.g. ``"data.devices"`` for ZHA per-device records).
400
+ Walks into the post-fields payload. Resolution failures replace
401
+ ``data`` with ``null`` and surface ``data_path_error``. Only applies
402
+ when ``include`` contains ``diagnostics``.
403
+ - diagnostics_data_offset / diagnostics_data_limit: Pagination on
404
+ list-valued ``diagnostics_data_path`` results. When ``data_limit``
405
+ is set and the resolved path is a list, ``data`` becomes
406
+ ``{"path", "items", "offset", "limit", "total", "has_more"}``. Only
407
+ applies when ``include`` contains ``diagnostics``.
408
+
409
+ Example workflow (walk a list-valued sub-tree one page at a time;
410
+ the exact ``data_path`` varies by integration version):
411
+ ``ha_get_system_health(include="diagnostics", config_entry_id="abc",
412
+ diagnostics_data_path="<list-valued path>", diagnostics_data_limit=10)``
413
+ → inspect the page envelope's ``total`` / ``has_more`` → repeat
414
+ with ``diagnostics_data_offset=10`` for the next slice.
360
415
  """
361
416
  includes = self._parse_includes(include)
362
417
  include_dismissed_repairs_bool = bool(
@@ -373,10 +428,18 @@ class SystemTools:
373
428
  ws_client, result = await self._fetch_health_info()
374
429
 
375
430
  # Warn about unrecognized include values
376
- VALID_INCLUDES = {"repairs", "zha_network", "zha_network_full", "zwave_network"}
431
+ VALID_INCLUDES = {
432
+ "repairs",
433
+ "zha_network",
434
+ "zha_network_full",
435
+ "zwave_network",
436
+ "diagnostics",
437
+ }
377
438
  unknown = includes - VALID_INCLUDES
378
439
  if unknown:
379
- result["warning"] = f"Unknown include sections ignored: {', '.join(sorted(unknown))}"
440
+ result.setdefault("warnings", []).append(
441
+ f"Unknown include sections ignored: {', '.join(sorted(unknown))}"
442
+ )
380
443
 
381
444
  # Fetch optional sections concurrently. The ws_client serialises
382
445
  # outgoing writes via its internal `_send_lock`, but per-message
@@ -462,6 +525,81 @@ class SystemTools:
462
525
  else:
463
526
  result[section_name] = section_result
464
527
 
528
+ # Diagnostics-related coercions live outside the includes branch
529
+ # so the orphan-args warning at the ``elif`` after the
530
+ # ``if "diagnostics" in includes`` block (see below) can see
531
+ # canonicalised values — passing ``diagnostics_data_offset=20``
532
+ # without ``include=diagnostics`` would otherwise slip past the gate.
533
+ fields_list = parse_diagnostics_fields(diagnostics_fields)
534
+ truncate_bytes = coerce_int_param(
535
+ diagnostics_truncate_at_bytes,
536
+ "diagnostics_truncate_at_bytes",
537
+ default=None,
538
+ min_value=1,
539
+ )
540
+ data_offset_int = coerce_int_param(
541
+ diagnostics_data_offset,
542
+ "diagnostics_data_offset",
543
+ default=0,
544
+ min_value=0,
545
+ )
546
+ data_limit_int = coerce_int_param(
547
+ diagnostics_data_limit,
548
+ "diagnostics_data_limit",
549
+ default=None,
550
+ min_value=1,
551
+ )
552
+ # Type-guard ``diagnostics_data_path`` here so a bad caller (dict /
553
+ # list) surfaces as ``VALIDATION_INVALID_PARAMETER`` instead of
554
+ # leaking as ``INTERNAL_ERROR`` from the resolver's ``.strip()``
555
+ # downstream. Mirrors the coerce_int_param guards above.
556
+ if diagnostics_data_path is not None and not isinstance(
557
+ diagnostics_data_path, str
558
+ ):
559
+ raise_tool_error(
560
+ create_error_response(
561
+ ErrorCode.VALIDATION_INVALID_PARAMETER,
562
+ "diagnostics_data_path must be a string, got "
563
+ f"{type(diagnostics_data_path).__name__}",
564
+ context={"parameter": "diagnostics_data_path"},
565
+ )
566
+ )
567
+
568
+ if "diagnostics" in includes:
569
+ # ``fetch_integration_diagnostics`` carries the empty-id guard
570
+ # (returns {"config_entry_id": ..., "error": ...}); calling it
571
+ # unconditionally keeps the missing-id error shape consistent
572
+ # with the populated path instead of returning a bare
573
+ # ``{"error": ...}`` sub-dict on the inline branch. Forward
574
+ # ``config_entry_id`` as-is (None / "") so the helper's echo
575
+ # field reflects what the caller actually passed.
576
+ result["diagnostics"] = await fetch_integration_diagnostics(
577
+ self._client,
578
+ config_entry_id,
579
+ device_id,
580
+ fields=fields_list,
581
+ truncate_at_bytes=truncate_bytes,
582
+ data_path=diagnostics_data_path,
583
+ data_offset=data_offset_int,
584
+ data_limit=data_limit_int,
585
+ )
586
+ elif (
587
+ config_entry_id
588
+ or device_id
589
+ or diagnostics_fields is not None
590
+ or diagnostics_truncate_at_bytes is not None
591
+ or diagnostics_data_path is not None
592
+ or diagnostics_data_limit is not None
593
+ or data_offset_int > 0
594
+ ):
595
+ result.setdefault("warnings", []).append(
596
+ "config_entry_id, device_id, diagnostics_fields, "
597
+ "diagnostics_truncate_at_bytes, diagnostics_data_path, "
598
+ "diagnostics_data_offset, and/or diagnostics_data_limit "
599
+ "were provided but ignored because 'diagnostics' is not "
600
+ "in include"
601
+ )
602
+
465
603
  return result
466
604
 
467
605
  except ToolError: