ha-mcp-dev 7.9.0.dev739__tar.gz → 7.9.0.dev741__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 (152) hide show
  1. {ha_mcp_dev-7.9.0.dev739/src/ha_mcp_dev.egg-info → ha_mcp_dev-7.9.0.dev741}/PKG-INFO +4 -3
  2. {ha_mcp_dev-7.9.0.dev739 → ha_mcp_dev-7.9.0.dev741}/README.md +3 -2
  3. {ha_mcp_dev-7.9.0.dev739 → ha_mcp_dev-7.9.0.dev741}/pyproject.toml +1 -1
  4. {ha_mcp_dev-7.9.0.dev739 → ha_mcp_dev-7.9.0.dev741}/src/ha_mcp/read_only.py +34 -2
  5. ha_mcp_dev-7.9.0.dev741/src/ha_mcp/tools/radio/__init__.py +6 -0
  6. ha_mcp_dev-7.9.0.dev741/src/ha_mcp/tools/radio/base.py +183 -0
  7. ha_mcp_dev-7.9.0.dev741/src/ha_mcp/tools/radio/matter.py +223 -0
  8. ha_mcp_dev-7.9.0.dev741/src/ha_mcp/tools/radio/thread.py +161 -0
  9. ha_mcp_dev-7.9.0.dev741/src/ha_mcp/tools/radio/zigbee.py +381 -0
  10. ha_mcp_dev-7.9.0.dev741/src/ha_mcp/tools/radio/zwave.py +374 -0
  11. {ha_mcp_dev-7.9.0.dev739 → ha_mcp_dev-7.9.0.dev741}/src/ha_mcp/tools/tools_mcp_component.py +17 -2
  12. ha_mcp_dev-7.9.0.dev741/src/ha_mcp/tools/tools_radio.py +199 -0
  13. {ha_mcp_dev-7.9.0.dev739 → ha_mcp_dev-7.9.0.dev741}/src/ha_mcp/tools/tools_registry.py +45 -2
  14. {ha_mcp_dev-7.9.0.dev739 → ha_mcp_dev-7.9.0.dev741}/src/ha_mcp/tools/tools_system.py +114 -2
  15. {ha_mcp_dev-7.9.0.dev739 → ha_mcp_dev-7.9.0.dev741/src/ha_mcp_dev.egg-info}/PKG-INFO +4 -3
  16. {ha_mcp_dev-7.9.0.dev739 → ha_mcp_dev-7.9.0.dev741}/src/ha_mcp_dev.egg-info/SOURCES.txt +7 -0
  17. {ha_mcp_dev-7.9.0.dev739 → ha_mcp_dev-7.9.0.dev741}/LICENSE +0 -0
  18. {ha_mcp_dev-7.9.0.dev739 → ha_mcp_dev-7.9.0.dev741}/MANIFEST.in +0 -0
  19. {ha_mcp_dev-7.9.0.dev739 → ha_mcp_dev-7.9.0.dev741}/setup.cfg +0 -0
  20. {ha_mcp_dev-7.9.0.dev739 → ha_mcp_dev-7.9.0.dev741}/src/ha_mcp/__init__.py +0 -0
  21. {ha_mcp_dev-7.9.0.dev739 → ha_mcp_dev-7.9.0.dev741}/src/ha_mcp/__main__.py +0 -0
  22. {ha_mcp_dev-7.9.0.dev739 → ha_mcp_dev-7.9.0.dev741}/src/ha_mcp/_pypi_marker +0 -0
  23. {ha_mcp_dev-7.9.0.dev739 → ha_mcp_dev-7.9.0.dev741}/src/ha_mcp/_version.py +0 -0
  24. {ha_mcp_dev-7.9.0.dev739 → ha_mcp_dev-7.9.0.dev741}/src/ha_mcp/auth/__init__.py +0 -0
  25. {ha_mcp_dev-7.9.0.dev739 → ha_mcp_dev-7.9.0.dev741}/src/ha_mcp/auth/consent_form.py +0 -0
  26. {ha_mcp_dev-7.9.0.dev739 → ha_mcp_dev-7.9.0.dev741}/src/ha_mcp/auth/provider.py +0 -0
  27. {ha_mcp_dev-7.9.0.dev739 → ha_mcp_dev-7.9.0.dev741}/src/ha_mcp/backup_manager.py +0 -0
  28. {ha_mcp_dev-7.9.0.dev739 → ha_mcp_dev-7.9.0.dev741}/src/ha_mcp/client/__init__.py +0 -0
  29. {ha_mcp_dev-7.9.0.dev739 → ha_mcp_dev-7.9.0.dev741}/src/ha_mcp/client/rest_client.py +0 -0
  30. {ha_mcp_dev-7.9.0.dev739 → ha_mcp_dev-7.9.0.dev741}/src/ha_mcp/client/supervisor_client.py +0 -0
  31. {ha_mcp_dev-7.9.0.dev739 → ha_mcp_dev-7.9.0.dev741}/src/ha_mcp/client/websocket_client.py +0 -0
  32. {ha_mcp_dev-7.9.0.dev739 → ha_mcp_dev-7.9.0.dev741}/src/ha_mcp/client/websocket_listener.py +0 -0
  33. {ha_mcp_dev-7.9.0.dev739 → ha_mcp_dev-7.9.0.dev741}/src/ha_mcp/config.py +0 -0
  34. {ha_mcp_dev-7.9.0.dev739 → ha_mcp_dev-7.9.0.dev741}/src/ha_mcp/dashboard_screenshot/__init__.py +0 -0
  35. {ha_mcp_dev-7.9.0.dev739 → ha_mcp_dev-7.9.0.dev741}/src/ha_mcp/dashboard_screenshot/capture.py +0 -0
  36. {ha_mcp_dev-7.9.0.dev739 → ha_mcp_dev-7.9.0.dev741}/src/ha_mcp/dashboard_screenshot/provision.py +0 -0
  37. {ha_mcp_dev-7.9.0.dev739 → ha_mcp_dev-7.9.0.dev741}/src/ha_mcp/errors.py +0 -0
  38. {ha_mcp_dev-7.9.0.dev739 → ha_mcp_dev-7.9.0.dev741}/src/ha_mcp/policy/__init__.py +0 -0
  39. {ha_mcp_dev-7.9.0.dev739 → ha_mcp_dev-7.9.0.dev741}/src/ha_mcp/policy/approval_queue.py +0 -0
  40. {ha_mcp_dev-7.9.0.dev739 → ha_mcp_dev-7.9.0.dev741}/src/ha_mcp/policy/evaluator.py +0 -0
  41. {ha_mcp_dev-7.9.0.dev739 → ha_mcp_dev-7.9.0.dev741}/src/ha_mcp/policy/handlers.py +0 -0
  42. {ha_mcp_dev-7.9.0.dev739 → ha_mcp_dev-7.9.0.dev741}/src/ha_mcp/policy/middleware.py +0 -0
  43. {ha_mcp_dev-7.9.0.dev739 → ha_mcp_dev-7.9.0.dev741}/src/ha_mcp/policy/model.py +0 -0
  44. {ha_mcp_dev-7.9.0.dev739 → ha_mcp_dev-7.9.0.dev741}/src/ha_mcp/policy/persistence.py +0 -0
  45. {ha_mcp_dev-7.9.0.dev739 → ha_mcp_dev-7.9.0.dev741}/src/ha_mcp/policy/value_sources.py +0 -0
  46. {ha_mcp_dev-7.9.0.dev739 → ha_mcp_dev-7.9.0.dev741}/src/ha_mcp/py.typed +0 -0
  47. {ha_mcp_dev-7.9.0.dev739 → ha_mcp_dev-7.9.0.dev741}/src/ha_mcp/resources/skills-vendor/.claude/settings.json +0 -0
  48. {ha_mcp_dev-7.9.0.dev739 → ha_mcp_dev-7.9.0.dev741}/src/ha_mcp/resources/skills-vendor/.claude-plugin/marketplace.json +0 -0
  49. {ha_mcp_dev-7.9.0.dev739 → ha_mcp_dev-7.9.0.dev741}/src/ha_mcp/resources/skills-vendor/.claude-plugin/plugin.json +0 -0
  50. {ha_mcp_dev-7.9.0.dev739 → ha_mcp_dev-7.9.0.dev741}/src/ha_mcp/resources/skills-vendor/.github/ISSUE_TEMPLATE/skill-rca.md +0 -0
  51. {ha_mcp_dev-7.9.0.dev739 → ha_mcp_dev-7.9.0.dev741}/src/ha_mcp/resources/skills-vendor/.github/pull_request_template.md +0 -0
  52. {ha_mcp_dev-7.9.0.dev739 → ha_mcp_dev-7.9.0.dev741}/src/ha_mcp/resources/skills-vendor/AGENTS.md +0 -0
  53. {ha_mcp_dev-7.9.0.dev739 → ha_mcp_dev-7.9.0.dev741}/src/ha_mcp/resources/skills-vendor/CLAUDE.md +0 -0
  54. {ha_mcp_dev-7.9.0.dev739 → ha_mcp_dev-7.9.0.dev741}/src/ha_mcp/resources/skills-vendor/CONTRIBUTING.md +0 -0
  55. {ha_mcp_dev-7.9.0.dev739 → ha_mcp_dev-7.9.0.dev741}/src/ha_mcp/resources/skills-vendor/LICENSE +0 -0
  56. {ha_mcp_dev-7.9.0.dev739 → ha_mcp_dev-7.9.0.dev741}/src/ha_mcp/resources/skills-vendor/README.md +0 -0
  57. {ha_mcp_dev-7.9.0.dev739 → ha_mcp_dev-7.9.0.dev741}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/SKILL.md +0 -0
  58. {ha_mcp_dev-7.9.0.dev739 → ha_mcp_dev-7.9.0.dev741}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/evals/evals.json +0 -0
  59. {ha_mcp_dev-7.9.0.dev739 → ha_mcp_dev-7.9.0.dev741}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/automation-patterns.md +0 -0
  60. {ha_mcp_dev-7.9.0.dev739 → ha_mcp_dev-7.9.0.dev741}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-cards.md +0 -0
  61. {ha_mcp_dev-7.9.0.dev739 → ha_mcp_dev-7.9.0.dev741}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-guide.md +0 -0
  62. {ha_mcp_dev-7.9.0.dev739 → ha_mcp_dev-7.9.0.dev741}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/device-control.md +0 -0
  63. {ha_mcp_dev-7.9.0.dev739 → ha_mcp_dev-7.9.0.dev741}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/domain-docs.md +0 -0
  64. {ha_mcp_dev-7.9.0.dev739 → ha_mcp_dev-7.9.0.dev741}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/examples.yaml +0 -0
  65. {ha_mcp_dev-7.9.0.dev739 → ha_mcp_dev-7.9.0.dev741}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/helper-selection.md +0 -0
  66. {ha_mcp_dev-7.9.0.dev739 → ha_mcp_dev-7.9.0.dev741}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/safe-refactoring.md +0 -0
  67. {ha_mcp_dev-7.9.0.dev739 → ha_mcp_dev-7.9.0.dev741}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/template-guidelines.md +0 -0
  68. {ha_mcp_dev-7.9.0.dev739 → ha_mcp_dev-7.9.0.dev741}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/yaml-only-integrations.md +0 -0
  69. {ha_mcp_dev-7.9.0.dev739 → ha_mcp_dev-7.9.0.dev741}/src/ha_mcp/server.py +0 -0
  70. {ha_mcp_dev-7.9.0.dev739 → ha_mcp_dev-7.9.0.dev741}/src/ha_mcp/settings_ui/__init__.py +0 -0
  71. {ha_mcp_dev-7.9.0.dev739 → ha_mcp_dev-7.9.0.dev741}/src/ha_mcp/settings_ui/settings.css +0 -0
  72. {ha_mcp_dev-7.9.0.dev739 → ha_mcp_dev-7.9.0.dev741}/src/ha_mcp/settings_ui/settings.html +0 -0
  73. {ha_mcp_dev-7.9.0.dev739 → ha_mcp_dev-7.9.0.dev741}/src/ha_mcp/settings_ui/settings.js +0 -0
  74. {ha_mcp_dev-7.9.0.dev739 → ha_mcp_dev-7.9.0.dev741}/src/ha_mcp/smoke_test.py +0 -0
  75. {ha_mcp_dev-7.9.0.dev739 → ha_mcp_dev-7.9.0.dev741}/src/ha_mcp/stdio_settings_sidecar.py +0 -0
  76. {ha_mcp_dev-7.9.0.dev739 → ha_mcp_dev-7.9.0.dev741}/src/ha_mcp/tools/__init__.py +0 -0
  77. {ha_mcp_dev-7.9.0.dev739 → ha_mcp_dev-7.9.0.dev741}/src/ha_mcp/tools/auto_backup.py +0 -0
  78. {ha_mcp_dev-7.9.0.dev739 → ha_mcp_dev-7.9.0.dev741}/src/ha_mcp/tools/backup.py +0 -0
  79. {ha_mcp_dev-7.9.0.dev739 → ha_mcp_dev-7.9.0.dev741}/src/ha_mcp/tools/best_practice_checker.py +0 -0
  80. {ha_mcp_dev-7.9.0.dev739 → ha_mcp_dev-7.9.0.dev741}/src/ha_mcp/tools/config_entry_flow.py +0 -0
  81. {ha_mcp_dev-7.9.0.dev739 → ha_mcp_dev-7.9.0.dev741}/src/ha_mcp/tools/device_control.py +0 -0
  82. {ha_mcp_dev-7.9.0.dev739 → ha_mcp_dev-7.9.0.dev741}/src/ha_mcp/tools/enhanced.py +0 -0
  83. {ha_mcp_dev-7.9.0.dev739 → ha_mcp_dev-7.9.0.dev741}/src/ha_mcp/tools/helpers.py +0 -0
  84. {ha_mcp_dev-7.9.0.dev739 → ha_mcp_dev-7.9.0.dev741}/src/ha_mcp/tools/reference_validator.py +0 -0
  85. {ha_mcp_dev-7.9.0.dev739 → ha_mcp_dev-7.9.0.dev741}/src/ha_mcp/tools/registry.py +0 -0
  86. {ha_mcp_dev-7.9.0.dev739 → ha_mcp_dev-7.9.0.dev741}/src/ha_mcp/tools/smart_search/__init__.py +0 -0
  87. {ha_mcp_dev-7.9.0.dev739 → ha_mcp_dev-7.9.0.dev741}/src/ha_mcp/tools/smart_search/_base.py +0 -0
  88. {ha_mcp_dev-7.9.0.dev739 → ha_mcp_dev-7.9.0.dev741}/src/ha_mcp/tools/smart_search/_config.py +0 -0
  89. {ha_mcp_dev-7.9.0.dev739 → ha_mcp_dev-7.9.0.dev741}/src/ha_mcp/tools/smart_search/_deep.py +0 -0
  90. {ha_mcp_dev-7.9.0.dev739 → ha_mcp_dev-7.9.0.dev741}/src/ha_mcp/tools/smart_search/_entities.py +0 -0
  91. {ha_mcp_dev-7.9.0.dev739 → ha_mcp_dev-7.9.0.dev741}/src/ha_mcp/tools/smart_search/_fetch.py +0 -0
  92. {ha_mcp_dev-7.9.0.dev739 → ha_mcp_dev-7.9.0.dev741}/src/ha_mcp/tools/smart_search/_overview.py +0 -0
  93. {ha_mcp_dev-7.9.0.dev739 → ha_mcp_dev-7.9.0.dev741}/src/ha_mcp/tools/smart_search/_scenes.py +0 -0
  94. {ha_mcp_dev-7.9.0.dev739 → ha_mcp_dev-7.9.0.dev741}/src/ha_mcp/tools/smart_search/_scoring.py +0 -0
  95. {ha_mcp_dev-7.9.0.dev739 → ha_mcp_dev-7.9.0.dev741}/src/ha_mcp/tools/tool_search_hint_middleware.py +0 -0
  96. {ha_mcp_dev-7.9.0.dev739 → ha_mcp_dev-7.9.0.dev741}/src/ha_mcp/tools/tools_addons.py +0 -0
  97. {ha_mcp_dev-7.9.0.dev739 → ha_mcp_dev-7.9.0.dev741}/src/ha_mcp/tools/tools_areas.py +0 -0
  98. {ha_mcp_dev-7.9.0.dev739 → ha_mcp_dev-7.9.0.dev741}/src/ha_mcp/tools/tools_blueprints.py +0 -0
  99. {ha_mcp_dev-7.9.0.dev739 → ha_mcp_dev-7.9.0.dev741}/src/ha_mcp/tools/tools_bug_report.py +0 -0
  100. {ha_mcp_dev-7.9.0.dev739 → ha_mcp_dev-7.9.0.dev741}/src/ha_mcp/tools/tools_calendar.py +0 -0
  101. {ha_mcp_dev-7.9.0.dev739 → ha_mcp_dev-7.9.0.dev741}/src/ha_mcp/tools/tools_camera.py +0 -0
  102. {ha_mcp_dev-7.9.0.dev739 → ha_mcp_dev-7.9.0.dev741}/src/ha_mcp/tools/tools_categories.py +0 -0
  103. {ha_mcp_dev-7.9.0.dev739 → ha_mcp_dev-7.9.0.dev741}/src/ha_mcp/tools/tools_code.py +0 -0
  104. {ha_mcp_dev-7.9.0.dev739 → ha_mcp_dev-7.9.0.dev741}/src/ha_mcp/tools/tools_config_automations.py +0 -0
  105. {ha_mcp_dev-7.9.0.dev739 → ha_mcp_dev-7.9.0.dev741}/src/ha_mcp/tools/tools_config_dashboards.py +0 -0
  106. {ha_mcp_dev-7.9.0.dev739 → ha_mcp_dev-7.9.0.dev741}/src/ha_mcp/tools/tools_config_helpers.py +0 -0
  107. {ha_mcp_dev-7.9.0.dev739 → ha_mcp_dev-7.9.0.dev741}/src/ha_mcp/tools/tools_config_scenes.py +0 -0
  108. {ha_mcp_dev-7.9.0.dev739 → ha_mcp_dev-7.9.0.dev741}/src/ha_mcp/tools/tools_config_scripts.py +0 -0
  109. {ha_mcp_dev-7.9.0.dev739 → ha_mcp_dev-7.9.0.dev741}/src/ha_mcp/tools/tools_dashboard_screenshot.py +0 -0
  110. {ha_mcp_dev-7.9.0.dev739 → ha_mcp_dev-7.9.0.dev741}/src/ha_mcp/tools/tools_energy.py +0 -0
  111. {ha_mcp_dev-7.9.0.dev739 → ha_mcp_dev-7.9.0.dev741}/src/ha_mcp/tools/tools_entities.py +0 -0
  112. {ha_mcp_dev-7.9.0.dev739 → ha_mcp_dev-7.9.0.dev741}/src/ha_mcp/tools/tools_filesystem.py +0 -0
  113. {ha_mcp_dev-7.9.0.dev739 → ha_mcp_dev-7.9.0.dev741}/src/ha_mcp/tools/tools_groups.py +0 -0
  114. {ha_mcp_dev-7.9.0.dev739 → ha_mcp_dev-7.9.0.dev741}/src/ha_mcp/tools/tools_hacs.py +0 -0
  115. {ha_mcp_dev-7.9.0.dev739 → ha_mcp_dev-7.9.0.dev741}/src/ha_mcp/tools/tools_history.py +0 -0
  116. {ha_mcp_dev-7.9.0.dev739 → ha_mcp_dev-7.9.0.dev741}/src/ha_mcp/tools/tools_integrations.py +0 -0
  117. {ha_mcp_dev-7.9.0.dev739 → ha_mcp_dev-7.9.0.dev741}/src/ha_mcp/tools/tools_labels.py +0 -0
  118. {ha_mcp_dev-7.9.0.dev739 → ha_mcp_dev-7.9.0.dev741}/src/ha_mcp/tools/tools_resources.py +0 -0
  119. {ha_mcp_dev-7.9.0.dev739 → ha_mcp_dev-7.9.0.dev741}/src/ha_mcp/tools/tools_search.py +0 -0
  120. {ha_mcp_dev-7.9.0.dev739 → ha_mcp_dev-7.9.0.dev741}/src/ha_mcp/tools/tools_service.py +0 -0
  121. {ha_mcp_dev-7.9.0.dev739 → ha_mcp_dev-7.9.0.dev741}/src/ha_mcp/tools/tools_services.py +0 -0
  122. {ha_mcp_dev-7.9.0.dev739 → ha_mcp_dev-7.9.0.dev741}/src/ha_mcp/tools/tools_themes.py +0 -0
  123. {ha_mcp_dev-7.9.0.dev739 → ha_mcp_dev-7.9.0.dev741}/src/ha_mcp/tools/tools_todo.py +0 -0
  124. {ha_mcp_dev-7.9.0.dev739 → ha_mcp_dev-7.9.0.dev741}/src/ha_mcp/tools/tools_traces.py +0 -0
  125. {ha_mcp_dev-7.9.0.dev739 → ha_mcp_dev-7.9.0.dev741}/src/ha_mcp/tools/tools_updates.py +0 -0
  126. {ha_mcp_dev-7.9.0.dev739 → ha_mcp_dev-7.9.0.dev741}/src/ha_mcp/tools/tools_utility.py +0 -0
  127. {ha_mcp_dev-7.9.0.dev739 → ha_mcp_dev-7.9.0.dev741}/src/ha_mcp/tools/tools_voice_assistant.py +0 -0
  128. {ha_mcp_dev-7.9.0.dev739 → ha_mcp_dev-7.9.0.dev741}/src/ha_mcp/tools/tools_yaml_config.py +0 -0
  129. {ha_mcp_dev-7.9.0.dev739 → ha_mcp_dev-7.9.0.dev741}/src/ha_mcp/tools/tools_zones.py +0 -0
  130. {ha_mcp_dev-7.9.0.dev739 → ha_mcp_dev-7.9.0.dev741}/src/ha_mcp/tools/util_helpers.py +0 -0
  131. {ha_mcp_dev-7.9.0.dev739 → ha_mcp_dev-7.9.0.dev741}/src/ha_mcp/tools/validation_middleware.py +0 -0
  132. {ha_mcp_dev-7.9.0.dev739 → ha_mcp_dev-7.9.0.dev741}/src/ha_mcp/transforms/__init__.py +0 -0
  133. {ha_mcp_dev-7.9.0.dev739 → ha_mcp_dev-7.9.0.dev741}/src/ha_mcp/transforms/categorized_search.py +0 -0
  134. {ha_mcp_dev-7.9.0.dev739 → ha_mcp_dev-7.9.0.dev741}/src/ha_mcp/transforms/lite_docstrings.py +0 -0
  135. {ha_mcp_dev-7.9.0.dev739 → ha_mcp_dev-7.9.0.dev741}/src/ha_mcp/update_check.py +0 -0
  136. {ha_mcp_dev-7.9.0.dev739 → ha_mcp_dev-7.9.0.dev741}/src/ha_mcp/utils/__init__.py +0 -0
  137. {ha_mcp_dev-7.9.0.dev739 → ha_mcp_dev-7.9.0.dev741}/src/ha_mcp/utils/config_hash.py +0 -0
  138. {ha_mcp_dev-7.9.0.dev739 → ha_mcp_dev-7.9.0.dev741}/src/ha_mcp/utils/data_paths.py +0 -0
  139. {ha_mcp_dev-7.9.0.dev739 → ha_mcp_dev-7.9.0.dev741}/src/ha_mcp/utils/domain_handlers.py +0 -0
  140. {ha_mcp_dev-7.9.0.dev739 → ha_mcp_dev-7.9.0.dev741}/src/ha_mcp/utils/fuzzy_search.py +0 -0
  141. {ha_mcp_dev-7.9.0.dev739 → ha_mcp_dev-7.9.0.dev741}/src/ha_mcp/utils/kill_signal_diagnostics.py +0 -0
  142. {ha_mcp_dev-7.9.0.dev739 → ha_mcp_dev-7.9.0.dev741}/src/ha_mcp/utils/operation_manager.py +0 -0
  143. {ha_mcp_dev-7.9.0.dev739 → ha_mcp_dev-7.9.0.dev741}/src/ha_mcp/utils/python_sandbox.py +0 -0
  144. {ha_mcp_dev-7.9.0.dev739 → ha_mcp_dev-7.9.0.dev741}/src/ha_mcp/utils/skill_loader.py +0 -0
  145. {ha_mcp_dev-7.9.0.dev739 → ha_mcp_dev-7.9.0.dev741}/src/ha_mcp/utils/usage_logger.py +0 -0
  146. {ha_mcp_dev-7.9.0.dev739 → ha_mcp_dev-7.9.0.dev741}/src/ha_mcp_dev.egg-info/dependency_links.txt +0 -0
  147. {ha_mcp_dev-7.9.0.dev739 → ha_mcp_dev-7.9.0.dev741}/src/ha_mcp_dev.egg-info/entry_points.txt +0 -0
  148. {ha_mcp_dev-7.9.0.dev739 → ha_mcp_dev-7.9.0.dev741}/src/ha_mcp_dev.egg-info/requires.txt +0 -0
  149. {ha_mcp_dev-7.9.0.dev739 → ha_mcp_dev-7.9.0.dev741}/src/ha_mcp_dev.egg-info/top_level.txt +0 -0
  150. {ha_mcp_dev-7.9.0.dev739 → ha_mcp_dev-7.9.0.dev741}/tests/__init__.py +0 -0
  151. {ha_mcp_dev-7.9.0.dev739 → ha_mcp_dev-7.9.0.dev741}/tests/test_constants.py +0 -0
  152. {ha_mcp_dev-7.9.0.dev739 → ha_mcp_dev-7.9.0.dev741}/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.9.0.dev739
3
+ Version: 7.9.0.dev741
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
@@ -40,7 +40,7 @@ Dynamic: license-file
40
40
  <!-- mcp-name: io.github.homeassistant-ai/ha-mcp -->
41
41
 
42
42
  <p align="center">
43
- <img src="https://img.shields.io/badge/tools-84-blue" alt="95+ Tools">
43
+ <img src="https://img.shields.io/badge/tools-85-blue" alt="95+ Tools">
44
44
  <a href="https://github.com/homeassistant-ai/ha-mcp/releases"><img src="https://img.shields.io/github/v/release/homeassistant-ai/ha-mcp" alt="Release"></a>
45
45
  <a href="https://github.com/homeassistant-ai/ha-mcp/actions/workflows/e2e-tests.yml"><img src="https://img.shields.io/github/actions/workflow/status/homeassistant-ai/ha-mcp/e2e-tests.yml?branch=master&label=E2E%20Tests" alt="E2E Tests"></a>
46
46
  <a href="LICENSE.md"><img src="https://img.shields.io/github/license/homeassistant-ai/ha-mcp.svg" alt="License"></a>
@@ -211,7 +211,7 @@ Spend less time configuring, more time enjoying your smart home.
211
211
  <details>
212
212
  <!-- TOOLS_TABLE_START -->
213
213
 
214
- <summary><b>Complete Tool List (84 tools)</b></summary>
214
+ <summary><b>Complete Tool List (85 tools)</b></summary>
215
215
 
216
216
  | Category | Tools |
217
217
  |----------|-------|
@@ -234,6 +234,7 @@ Spend less time configuring, more time enjoying your smart home.
234
234
  | **History & Statistics** | `ha_get_automation_traces`, `ha_get_history`, `ha_get_logs` |
235
235
  | **Integrations** | `ha_get_integration`, `ha_get_system_health`, `ha_set_integration_enabled` |
236
236
  | **Labels & Categories** | `ha_config_get_category`, `ha_config_get_label`, `ha_config_remove_category`, `ha_config_remove_label`, `ha_config_set_category`, `ha_config_set_label` |
237
+ | **Matter** | `ha_manage_radio` |
237
238
  | **Scenes** | `ha_config_get_scene`, `ha_config_remove_scene`, `ha_config_set_scene` |
238
239
  | **Scripts** | `ha_config_get_script`, `ha_config_remove_script`, `ha_config_set_script` |
239
240
  | **Search & Discovery** | `ha_get_overview`, `ha_get_state`, `ha_search` |
@@ -8,7 +8,7 @@
8
8
  <!-- mcp-name: io.github.homeassistant-ai/ha-mcp -->
9
9
 
10
10
  <p align="center">
11
- <img src="https://img.shields.io/badge/tools-84-blue" alt="95+ Tools">
11
+ <img src="https://img.shields.io/badge/tools-85-blue" alt="95+ Tools">
12
12
  <a href="https://github.com/homeassistant-ai/ha-mcp/releases"><img src="https://img.shields.io/github/v/release/homeassistant-ai/ha-mcp" alt="Release"></a>
13
13
  <a href="https://github.com/homeassistant-ai/ha-mcp/actions/workflows/e2e-tests.yml"><img src="https://img.shields.io/github/actions/workflow/status/homeassistant-ai/ha-mcp/e2e-tests.yml?branch=master&label=E2E%20Tests" alt="E2E Tests"></a>
14
14
  <a href="LICENSE.md"><img src="https://img.shields.io/github/license/homeassistant-ai/ha-mcp.svg" alt="License"></a>
@@ -179,7 +179,7 @@ Spend less time configuring, more time enjoying your smart home.
179
179
  <details>
180
180
  <!-- TOOLS_TABLE_START -->
181
181
 
182
- <summary><b>Complete Tool List (84 tools)</b></summary>
182
+ <summary><b>Complete Tool List (85 tools)</b></summary>
183
183
 
184
184
  | Category | Tools |
185
185
  |----------|-------|
@@ -202,6 +202,7 @@ Spend less time configuring, more time enjoying your smart home.
202
202
  | **History & Statistics** | `ha_get_automation_traces`, `ha_get_history`, `ha_get_logs` |
203
203
  | **Integrations** | `ha_get_integration`, `ha_get_system_health`, `ha_set_integration_enabled` |
204
204
  | **Labels & Categories** | `ha_config_get_category`, `ha_config_get_label`, `ha_config_remove_category`, `ha_config_remove_label`, `ha_config_set_category`, `ha_config_set_label` |
205
+ | **Matter** | `ha_manage_radio` |
205
206
  | **Scenes** | `ha_config_get_scene`, `ha_config_remove_scene`, `ha_config_set_scene` |
206
207
  | **Scripts** | `ha_config_get_script`, `ha_config_remove_script`, `ha_config_set_script` |
207
208
  | **Search & Discovery** | `ha_get_overview`, `ha_get_state`, `ha_search` |
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "ha-mcp-dev"
7
- version = "7.9.0.dev739"
7
+ version = "7.9.0.dev741"
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"
@@ -131,12 +131,37 @@ def _custom_tool_write(args: dict[str, Any]) -> str | None:
131
131
  return "sandbox code execution"
132
132
 
133
133
 
134
+ def _radio_write(args: dict[str, Any]) -> str | None:
135
+ action = args.get("action")
136
+ # Reads (allowed): per-node diagnostics, the integration/network summary,
137
+ # the active reachability probe, a single Zigbee cluster-attribute read, and
138
+ # the Thread dataset listing. Everything else is a write — commission/add,
139
+ # remove, reinterview, firmware, fabric/credential/channel/network changes,
140
+ # plus the two actions that LOOK read-ish but are not: zigbee network_backup
141
+ # (creates a backup artifact + key material, like ha_manage_backup's blocked
142
+ # snapshot create) and thread discover_routers (kicks off a long-running
143
+ # mDNS scan). A missing action fails closed.
144
+ if action in (
145
+ "diagnostics",
146
+ "network_status",
147
+ "ping",
148
+ "cluster_read",
149
+ "list_datasets",
150
+ ):
151
+ return None
152
+ return f"action={action!r}"
153
+
154
+
134
155
  # Mixed read/write tools whose read surface has no pure-read duplicate
135
156
  # (verified per tool: ha_get_addon cannot proxy-read addon-internal
136
157
  # APIs; energy prefs and assist pipelines are reachable only through
137
158
  # these tools; edit-backup listing exists nowhere else; the saved-tools
138
- # cache is only listable here). Everything NOT in this table and not
139
- # ``readOnlyHint=True`` is hidden and blocked outright.
159
+ # cache is only listable here; ha_manage_radio's 'ping' probe, 'cluster_read'
160
+ # and 'list_datasets' have no pure-read duplicate elsewhere, while its
161
+ # 'diagnostics'/'network_status' reads mirror ha_get_device /
162
+ # ha_get_system_health but stay reachable here mid-management). Everything
163
+ # NOT in this table and not ``readOnlyHint=True`` is hidden and blocked
164
+ # outright.
140
165
  #
141
166
  # ``MANDATORY_TOOLS`` (settings_ui/__init__.py) intentionally needs no special
142
167
  # case here: every mandatory tool is either ``readOnlyHint=True`` or
@@ -168,6 +193,13 @@ READ_ONLY_EXEMPT_TOOLS: dict[str, ReadOnlyExemption] = {
168
193
  _custom_tool_write,
169
194
  "listing saved tools (list_saved=True)",
170
195
  ),
196
+ "ha_manage_radio": ReadOnlyExemption(
197
+ _radio_write,
198
+ "node diagnostics ('diagnostics'), the integration/network summary "
199
+ "('network_status'), the active reachability probe ('ping'), a Zigbee "
200
+ "cluster-attribute read ('cluster_read'), and the Thread dataset "
201
+ "listing ('list_datasets')",
202
+ ),
171
203
  }
172
204
 
173
205
 
@@ -0,0 +1,6 @@
1
+ """Per-radio handler modules for the ``ha_manage_radio`` tool.
2
+
3
+ Each radio (Z-Wave, Zigbee/ZHA, Matter, Thread) gets its own handler module so
4
+ no single file spans every protocol (keeps modules focused per AGENTS.md). The
5
+ ``tools_radio`` module wires them together behind one MCP tool.
6
+ """
@@ -0,0 +1,183 @@
1
+ """Shared contract and helpers for ``ha_manage_radio`` per-radio handlers.
2
+
3
+ A handler module exposes:
4
+
5
+ - ``SUPPORTED``: ``dict[action_name, ActionSpec]`` describing every action it
6
+ implements, used for validation, the destructive-confirm gate, and building
7
+ actionable "unsupported action" errors.
8
+ - ``async def handle(client, action, args) -> dict``: execute one action.
9
+
10
+ Handlers send raw WebSocket commands through the REST client's
11
+ ``send_websocket_message`` bridge (same path the ``ha_get_device`` enrichers
12
+ use), or call services via ``call_service`` for the operations that are
13
+ service-only (most ZHA writes, plus ``zwave_js.ping`` and ``update.install``
14
+ firmware installs).
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import logging
20
+ from dataclasses import dataclass, field
21
+ from typing import Any
22
+
23
+ from ...errors import ErrorCode, create_error_response
24
+ from ..helpers import raise_tool_error
25
+
26
+ logger = logging.getLogger(__name__)
27
+
28
+
29
+ @dataclass(frozen=True)
30
+ class ActionSpec:
31
+ """Metadata for one radio action.
32
+
33
+ summary: one-line description (surfaced in unsupported-action errors).
34
+ destructive: requires ``confirm=True`` before it will run.
35
+ long_running: starts an operation that completes out-of-band (inclusion,
36
+ rebuild routes, firmware) — the result documents how to follow up.
37
+ required: parameter names that must be present in ``args`` (or supplied as
38
+ the top-level ``device_id``).
39
+ """
40
+
41
+ summary: str
42
+ destructive: bool = False
43
+ long_running: bool = False
44
+ required: tuple[str, ...] = field(default_factory=tuple)
45
+
46
+
47
+ def ok(radio: str, action: str, **data: Any) -> dict[str, Any]:
48
+ """Build the standard success envelope for a radio action."""
49
+ return {"success": True, "radio": radio, "action": action, **data}
50
+
51
+
52
+ def require(args: dict[str, Any], spec: ActionSpec, radio: str, action: str) -> None:
53
+ """Raise VALIDATION_INVALID_PARAMETER if any required arg is missing/empty."""
54
+ missing = [k for k in spec.required if args.get(k) in (None, "")]
55
+ if missing:
56
+ raise_tool_error(
57
+ create_error_response(
58
+ ErrorCode.VALIDATION_INVALID_PARAMETER,
59
+ f"{radio}/{action} requires: {', '.join(missing)}",
60
+ context={"radio": radio, "action": action, "missing": missing},
61
+ suggestions=[f"Pass {m} (in params or as device_id)" for m in missing],
62
+ )
63
+ )
64
+
65
+
66
+ async def ws_call(
67
+ client: Any, ws_type: str, *, context: dict[str, Any] | None = None, **fields: Any
68
+ ) -> Any:
69
+ """Send a WebSocket command and return its ``result``; raise on failure.
70
+
71
+ Mirrors the ``send_websocket_message`` usage in the ``ha_get_device``
72
+ enrichers. Raises ToolError (SERVICE_CALL_FAILED) when HA reports the
73
+ command failed, attaching the command type and any caller context.
74
+ """
75
+ message = {"type": ws_type, **{k: v for k, v in fields.items() if v is not None}}
76
+ result = await client.send_websocket_message(message)
77
+ if not result.get("success"):
78
+ raise_tool_error(
79
+ create_error_response(
80
+ ErrorCode.SERVICE_CALL_FAILED,
81
+ result.get("error", f"WebSocket command '{ws_type}' failed"),
82
+ context={"ws_type": ws_type, **(context or {})},
83
+ )
84
+ )
85
+ return result.get("result")
86
+
87
+
88
+ async def resolve_entry_id(client: Any, domain: str) -> str | None:
89
+ """Return the config entry_id for a single-instance integration ``domain``.
90
+
91
+ Uses ``config_entries/get`` (underscore form; the slash form is rejected as
92
+ "Unknown command"). Returns None when the integration is not configured.
93
+ """
94
+ entries = await ws_call(client, "config_entries/get", context={"domain": domain})
95
+ for entry in entries or []:
96
+ if entry.get("domain") == domain:
97
+ entry_id = entry.get("entry_id")
98
+ return str(entry_id) if entry_id is not None else None
99
+ return None
100
+
101
+
102
+ async def resolve_update_entity(
103
+ client: Any, device_id: Any, *, platform: str | None = None
104
+ ) -> str:
105
+ """Return the device's ``update.*`` firmware entity_id from the registry.
106
+
107
+ Filters the ``update.*`` entities tied to ``device_id``, preferring the given
108
+ ``platform`` (e.g. "matter", "zha", "zwave_js") when a device exposes more
109
+ than one. Raises ENTITY_NOT_FOUND when the device exposes no update entity.
110
+ """
111
+ entities = await ws_call(
112
+ client, "config/entity_registry/list", context={"device_id": device_id}
113
+ )
114
+ candidates = [
115
+ e
116
+ for e in (entities or [])
117
+ if e.get("device_id") == device_id
118
+ and str(e.get("entity_id", "")).startswith("update.")
119
+ ]
120
+ if platform is not None:
121
+ for entity in candidates:
122
+ if entity.get("platform") == platform:
123
+ return str(entity["entity_id"])
124
+ if candidates:
125
+ return str(candidates[0]["entity_id"])
126
+ raise_tool_error(
127
+ create_error_response(
128
+ ErrorCode.ENTITY_NOT_FOUND,
129
+ f"No update entity found for device {device_id}; no firmware update is available",
130
+ context={"device_id": device_id},
131
+ suggestions=[
132
+ "Firmware updates appear only when the device exposes an update.* entity",
133
+ "Check ha_get_device for an 'update.' entity on this device",
134
+ ],
135
+ )
136
+ )
137
+ raise AssertionError # py/mixed-returns terminal: raise_tool_error is NoReturn
138
+
139
+
140
+ def integration_not_found(radio: str, domain: str) -> dict[str, Any]:
141
+ """Standard degraded payload when an integration/config-entry is absent.
142
+
143
+ For read-only actions (``network_status``) only — reporting the absent
144
+ integration as a graceful ``success: True`` degradation is correct there.
145
+ Write/management actions must call ``integration_required`` instead so an
146
+ unconfigured integration surfaces as an error rather than a silent no-op.
147
+ """
148
+ return {
149
+ "success": True,
150
+ "radio": radio,
151
+ "available": False,
152
+ "warnings": [f"{domain} integration is not configured on this Home Assistant"],
153
+ }
154
+
155
+
156
+ def integration_required(radio: str, domain: str) -> None:
157
+ """Raise a ToolError when a write action's integration/config-entry is absent.
158
+
159
+ The write-action counterpart to ``integration_not_found``: management actions
160
+ must not report a no-op as ``success: True``, so this raises instead of
161
+ degrading to an ``available: False`` payload.
162
+ """
163
+ raise_tool_error(
164
+ create_error_response(
165
+ ErrorCode.SERVICE_CALL_FAILED,
166
+ f"{domain} integration is not configured on this Home Assistant; "
167
+ f"cannot run this {radio} management action.",
168
+ context={"radio": radio, "domain": domain, "available": False},
169
+ suggestions=[f"Install/configure the {domain} integration first"],
170
+ )
171
+ )
172
+
173
+
174
+ def confirm_required(radio: str, action: str) -> None:
175
+ """Raise the ToolError for a destructive action lacking confirm=True."""
176
+ raise_tool_error(
177
+ create_error_response(
178
+ ErrorCode.VALIDATION_INVALID_PARAMETER,
179
+ f"{radio}/{action} is destructive; pass confirm=True to proceed",
180
+ context={"radio": radio, "action": action, "destructive": True},
181
+ suggestions=["Re-run with confirm=True once you intend the change"],
182
+ )
183
+ )
@@ -0,0 +1,223 @@
1
+ """Matter radio handler for ``ha_manage_radio``.
2
+
3
+ Wraps the Home Assistant ``matter/*`` WebSocket API: per-node diagnostics and
4
+ ping (read), commissioning / multi-admin share-out / fabric removal / interview
5
+ (write), and credential provisioning. All commands are single request/response
6
+ or fire-and-return; Matter has no interactive PIN handshake (unlike Z-Wave S2).
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from typing import Any
12
+
13
+ from ...errors import ErrorCode, create_error_response
14
+ from ..helpers import raise_tool_error
15
+ from .base import (
16
+ ActionSpec,
17
+ integration_not_found,
18
+ ok,
19
+ resolve_entry_id,
20
+ resolve_update_entity,
21
+ ws_call,
22
+ )
23
+
24
+ SUPPORTED: dict[str, ActionSpec] = {
25
+ "diagnostics": ActionSpec(
26
+ "Per-node Matter diagnostics: network type (wifi/thread), availability, "
27
+ "IPs, node type, active fabrics.",
28
+ required=("device_id",),
29
+ ),
30
+ "network_status": ActionSpec("Matter fabric / integration summary."),
31
+ "ping": ActionSpec(
32
+ "Actively ping a Matter node at its known IP addresses.",
33
+ required=("device_id",),
34
+ ),
35
+ "commission": ActionSpec(
36
+ "Commission a Matter device from a setup code (QR or manual pairing code).",
37
+ long_running=True,
38
+ required=("code",),
39
+ ),
40
+ "commission_on_network": ActionSpec(
41
+ "Commission an already-networked Matter device by its PIN.",
42
+ long_running=True,
43
+ required=("pin",),
44
+ ),
45
+ "share_out": ActionSpec(
46
+ "Open a commissioning window and return a pairing code so another "
47
+ "controller can add this device (multi-admin).",
48
+ required=("device_id",),
49
+ ),
50
+ "interview": ActionSpec(
51
+ "Re-interview a Matter node to refresh its endpoints/clusters.",
52
+ long_running=True,
53
+ required=("device_id",),
54
+ ),
55
+ "remove_fabric": ActionSpec(
56
+ "Remove ANOTHER controller's fabric from a Matter node (pass "
57
+ "params.fabric_index; see active_fabrics in diagnostics). To detach the "
58
+ "device from Home Assistant itself, delete it with ha_remove_device.",
59
+ destructive=True,
60
+ required=("device_id",),
61
+ ),
62
+ "set_thread": ActionSpec(
63
+ "Provide Thread operational credentials used for the next commissioning.",
64
+ required=("thread_operation_dataset",),
65
+ ),
66
+ "set_wifi_credentials": ActionSpec(
67
+ "Provide WiFi credentials used for the next commissioning.",
68
+ required=("network_name", "password"),
69
+ ),
70
+ "firmware_update": ActionSpec(
71
+ "Install an available OTA firmware update for this Matter node.",
72
+ long_running=True,
73
+ required=("device_id",),
74
+ ),
75
+ }
76
+
77
+
78
+ async def handle(client: Any, action: str, args: dict[str, Any]) -> dict[str, Any]:
79
+ """Execute one Matter action (validation/confirm already applied by caller)."""
80
+ device_id = args.get("device_id")
81
+
82
+ if action == "diagnostics":
83
+ diag = await ws_call(
84
+ client,
85
+ "matter/node_diagnostics",
86
+ device_id=device_id,
87
+ context={"device_id": device_id},
88
+ )
89
+ # Upstream NodeDiagnostics misspells the IP field "ip_adresses" (one d);
90
+ # normalize so callers see the same "ip_addresses" key ha_get_device
91
+ # surfaces. Copy first so the upstream/result dict is not mutated.
92
+ if isinstance(diag, dict) and "ip_adresses" in diag:
93
+ diag = dict(diag)
94
+ diag["ip_addresses"] = diag.pop("ip_adresses")
95
+ return ok("matter", "diagnostics", diagnostics=diag)
96
+
97
+ if action == "ping":
98
+ reachability = await ws_call(
99
+ client,
100
+ "matter/ping_node",
101
+ device_id=device_id,
102
+ context={"device_id": device_id},
103
+ )
104
+ return ok("matter", "ping", reachability=reachability)
105
+
106
+ if action == "network_status":
107
+ entry_id = await resolve_entry_id(client, "matter")
108
+ if not entry_id:
109
+ return integration_not_found("matter", "matter")
110
+ return ok(
111
+ "matter",
112
+ "network_status",
113
+ config_entry_id=entry_id,
114
+ note=(
115
+ "Matter exposes health per node; call action='diagnostics' with a "
116
+ "device_id for node-level network type, availability and fabrics."
117
+ ),
118
+ )
119
+
120
+ if action == "commission":
121
+ result = await ws_call(
122
+ client,
123
+ "matter/commission",
124
+ code=args.get("code"),
125
+ network_only=bool(args.get("network_only", False)),
126
+ )
127
+ return ok("matter", "commission", result=result, long_running=True)
128
+
129
+ if action == "commission_on_network":
130
+ result = await ws_call(
131
+ client,
132
+ "matter/commission_on_network",
133
+ pin=args.get("pin"),
134
+ ip_addr=args.get("ip_addr"),
135
+ )
136
+ return ok("matter", "commission_on_network", result=result, long_running=True)
137
+
138
+ if action == "share_out":
139
+ commissioning = await ws_call(
140
+ client,
141
+ "matter/open_commissioning_window",
142
+ device_id=device_id,
143
+ context={"device_id": device_id},
144
+ )
145
+ # Returns setup_pin_code, setup_manual_code, setup_qr_code.
146
+ return ok("matter", "share_out", commissioning=commissioning)
147
+
148
+ if action == "interview":
149
+ result = await ws_call(
150
+ client,
151
+ "matter/interview_node",
152
+ device_id=device_id,
153
+ context={"device_id": device_id},
154
+ )
155
+ return ok("matter", "interview", result=result, long_running=True)
156
+
157
+ if action == "remove_fabric":
158
+ diag = await ws_call(
159
+ client,
160
+ "matter/node_diagnostics",
161
+ device_id=device_id,
162
+ context={"device_id": device_id},
163
+ )
164
+ active = (diag or {}).get("active_fabric_index")
165
+ fabric_index = args.get("fabric_index")
166
+ # remove_fabric removes ANOTHER controller's fabric. Removing HA's OWN
167
+ # active fabric over that fabric makes the node drop the session, so the
168
+ # Matter server aborts the confirmation (the removal still happens, but
169
+ # reports as an error). The clean "detach from HA" path is deleting the
170
+ # device, so refuse the own-fabric case with that guidance.
171
+ if fabric_index is None or fabric_index == active:
172
+ raise_tool_error(
173
+ create_error_response(
174
+ ErrorCode.VALIDATION_INVALID_PARAMETER,
175
+ "remove_fabric removes ANOTHER controller's fabric from a "
176
+ "node; pass params.fabric_index (see active_fabrics). To "
177
+ "detach this device from Home Assistant itself, delete the "
178
+ "device with ha_remove_device — removing HA's own active "
179
+ "fabric here is aborted by the Matter server.",
180
+ context={
181
+ "device_id": device_id,
182
+ "active_fabric_index": active,
183
+ "active_fabrics": (diag or {}).get("active_fabrics", []),
184
+ },
185
+ suggestions=[
186
+ "Pass params.fabric_index = the other controller's index",
187
+ "To remove Home Assistant, use ha_remove_device",
188
+ ],
189
+ )
190
+ )
191
+ result = await ws_call(
192
+ client,
193
+ "matter/remove_matter_fabric",
194
+ device_id=device_id,
195
+ fabric_index=fabric_index,
196
+ context={"device_id": device_id, "fabric_index": fabric_index},
197
+ )
198
+ return ok("matter", "remove_fabric", result=result, fabric_index=fabric_index)
199
+
200
+ if action == "set_thread":
201
+ result = await ws_call(
202
+ client,
203
+ "matter/set_thread",
204
+ thread_operation_dataset=args.get("thread_operation_dataset"),
205
+ )
206
+ return ok("matter", "set_thread", result=result)
207
+
208
+ if action == "set_wifi_credentials":
209
+ result = await ws_call(
210
+ client,
211
+ "matter/set_wifi_credentials",
212
+ network_name=args.get("network_name"),
213
+ password=args.get("password"),
214
+ )
215
+ return ok("matter", "set_wifi_credentials", result=result)
216
+
217
+ if action == "firmware_update":
218
+ entity_id = await resolve_update_entity(client, device_id, platform="matter")
219
+ await client.call_service("update", "install", {"entity_id": entity_id})
220
+ return ok("matter", "firmware_update", entity_id=entity_id, long_running=True)
221
+
222
+ # Unreachable: dispatcher validates action against SUPPORTED first.
223
+ raise AssertionError(f"unhandled matter action: {action}")