ha-mcp-dev 7.12.3.dev1885__tar.gz → 7.12.3.dev1891__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 (165) hide show
  1. {ha_mcp_dev-7.12.3.dev1885/src/ha_mcp_dev.egg-info → ha_mcp_dev-7.12.3.dev1891}/PKG-INFO +1 -1
  2. {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/pyproject.toml +1 -1
  3. {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/client/rest_client.py +34 -10
  4. {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/client/websocket_client.py +20 -5
  5. {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/smoke_test.py +2 -2
  6. {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/stdio_settings_sidecar.py +6 -6
  7. {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/tools/backup.py +8 -27
  8. {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/tools/tools_addons.py +12 -40
  9. {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/tools/tools_bug_report.py +15 -4
  10. {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/tools/tools_calendar.py +23 -55
  11. {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/tools/tools_config_helpers.py +276 -5
  12. {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/tools/tools_config_scenes.py +17 -6
  13. {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/tools/tools_entities.py +693 -148
  14. {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/tools/tools_filesystem.py +34 -1
  15. {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/tools/tools_history.py +111 -87
  16. {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/tools/tools_mcp_component.py +14 -2
  17. {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/tools/tools_radio.py +9 -5
  18. {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/tools/tools_search.py +11 -12
  19. {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/tools/tools_system.py +158 -12
  20. {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/tools/tools_zones.py +185 -35
  21. {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891/src/ha_mcp_dev.egg-info}/PKG-INFO +1 -1
  22. {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/LICENSE +0 -0
  23. {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/MANIFEST.in +0 -0
  24. {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/README.md +0 -0
  25. {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/setup.cfg +0 -0
  26. {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/__init__.py +0 -0
  27. {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/__main__.py +0 -0
  28. {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/_pypi_marker +0 -0
  29. {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/_version.py +0 -0
  30. {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/auth/__init__.py +0 -0
  31. {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/auth/consent_form.py +0 -0
  32. {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/auth/provider.py +0 -0
  33. {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/backup_manager.py +0 -0
  34. {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/browser_landing.py +0 -0
  35. {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/client/__init__.py +0 -0
  36. {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/client/supervisor_client.py +0 -0
  37. {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/client/websocket_listener.py +0 -0
  38. {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/config.py +0 -0
  39. {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/dashboard_screenshot/__init__.py +0 -0
  40. {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/dashboard_screenshot/capture.py +0 -0
  41. {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/dashboard_screenshot/provision.py +0 -0
  42. {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/errors.py +0 -0
  43. {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/llm_exposure.py +0 -0
  44. {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/policy/__init__.py +0 -0
  45. {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/policy/approval_queue.py +0 -0
  46. {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/policy/evaluator.py +0 -0
  47. {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/policy/handlers.py +0 -0
  48. {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/policy/middleware.py +0 -0
  49. {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/policy/model.py +0 -0
  50. {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/policy/persistence.py +0 -0
  51. {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/policy/value_sources.py +0 -0
  52. {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/py.typed +0 -0
  53. {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/read_only.py +0 -0
  54. {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/resources/skills-vendor/.claude/settings.json +0 -0
  55. {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/resources/skills-vendor/.claude-plugin/marketplace.json +0 -0
  56. {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/resources/skills-vendor/.claude-plugin/plugin.json +0 -0
  57. {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/resources/skills-vendor/.github/ISSUE_TEMPLATE/skill-rca.md +0 -0
  58. {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/resources/skills-vendor/.github/pull_request_template.md +0 -0
  59. {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/resources/skills-vendor/AGENTS.md +0 -0
  60. {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/resources/skills-vendor/CLAUDE.md +0 -0
  61. {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/resources/skills-vendor/CONTRIBUTING.md +0 -0
  62. {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/resources/skills-vendor/LICENSE +0 -0
  63. {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/resources/skills-vendor/README.md +0 -0
  64. {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/SKILL.md +0 -0
  65. {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/evals/evals.json +0 -0
  66. {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/appdaemon.md +0 -0
  67. {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/automation-patterns.md +0 -0
  68. {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/blueprint-guide.md +0 -0
  69. {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-cards.md +0 -0
  70. {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-guide.md +0 -0
  71. {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/device-control.md +0 -0
  72. {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/domain-docs.md +0 -0
  73. {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/examples.yaml +0 -0
  74. {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/helper-selection.md +0 -0
  75. {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/safe-refactoring.md +0 -0
  76. {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/scenes.md +0 -0
  77. {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/template-guidelines.md +0 -0
  78. {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/yaml-only-integrations.md +0 -0
  79. {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/server.py +0 -0
  80. {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/settings_ui/__init__.py +0 -0
  81. {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/settings_ui/settings.css +0 -0
  82. {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/settings_ui/settings.html +0 -0
  83. {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/settings_ui/settings.js +0 -0
  84. {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/strict_bps.py +0 -0
  85. {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/tools/__init__.py +0 -0
  86. {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/tools/auto_backup.py +0 -0
  87. {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/tools/best_practice_checker.py +0 -0
  88. {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/tools/component_api.py +0 -0
  89. {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/tools/config_entry_flow.py +0 -0
  90. {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/tools/device_control.py +0 -0
  91. {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/tools/enhanced.py +0 -0
  92. {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/tools/helpers.py +0 -0
  93. {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/tools/radio/__init__.py +0 -0
  94. {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/tools/radio/base.py +0 -0
  95. {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/tools/radio/matter.py +0 -0
  96. {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/tools/radio/thread.py +0 -0
  97. {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/tools/radio/zigbee.py +0 -0
  98. {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/tools/radio/zwave.py +0 -0
  99. {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/tools/reference_validator.py +0 -0
  100. {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/tools/registry.py +0 -0
  101. {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/tools/smart_search/__init__.py +0 -0
  102. {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/tools/smart_search/_base.py +0 -0
  103. {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/tools/smart_search/_config.py +0 -0
  104. {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/tools/smart_search/_deep.py +0 -0
  105. {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/tools/smart_search/_entities.py +0 -0
  106. {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/tools/smart_search/_fetch.py +0 -0
  107. {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/tools/smart_search/_overview.py +0 -0
  108. {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/tools/smart_search/_scenes.py +0 -0
  109. {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/tools/smart_search/_scoring.py +0 -0
  110. {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/tools/tool_search_hint_middleware.py +0 -0
  111. {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/tools/tools_areas.py +0 -0
  112. {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/tools/tools_blueprints.py +0 -0
  113. {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/tools/tools_camera.py +0 -0
  114. {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/tools/tools_categories.py +0 -0
  115. {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/tools/tools_code.py +0 -0
  116. {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/tools/tools_config_automations.py +0 -0
  117. {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/tools/tools_config_dashboards.py +0 -0
  118. {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/tools/tools_config_scripts.py +0 -0
  119. {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/tools/tools_dashboard_screenshot.py +0 -0
  120. {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/tools/tools_dev.py +0 -0
  121. {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/tools/tools_energy.py +0 -0
  122. {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/tools/tools_groups.py +0 -0
  123. {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/tools/tools_hacs.py +0 -0
  124. {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/tools/tools_integrations.py +0 -0
  125. {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/tools/tools_labels.py +0 -0
  126. {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/tools/tools_registry.py +0 -0
  127. {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/tools/tools_resources.py +0 -0
  128. {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/tools/tools_service.py +0 -0
  129. {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/tools/tools_services.py +0 -0
  130. {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/tools/tools_themes.py +0 -0
  131. {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/tools/tools_todo.py +0 -0
  132. {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/tools/tools_traces.py +0 -0
  133. {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/tools/tools_updates.py +0 -0
  134. {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/tools/tools_utility.py +0 -0
  135. {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/tools/tools_voice_assistant.py +0 -0
  136. {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/tools/tools_yaml_config.py +0 -0
  137. {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/tools/util_helpers.py +0 -0
  138. {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/tools/validation_middleware.py +0 -0
  139. {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/transforms/__init__.py +0 -0
  140. {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/transforms/categorized_search.py +0 -0
  141. {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/transforms/lite_docstrings.py +0 -0
  142. {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/transport_security.py +0 -0
  143. {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/update_check.py +0 -0
  144. {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/utils/__init__.py +0 -0
  145. {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/utils/config_hash.py +0 -0
  146. {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/utils/data_paths.py +0 -0
  147. {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/utils/domain_handlers.py +0 -0
  148. {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/utils/fuzzy_search.py +0 -0
  149. {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/utils/kill_signal_diagnostics.py +0 -0
  150. {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/utils/operation_manager.py +0 -0
  151. {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/utils/python_sandbox.py +0 -0
  152. {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/utils/skill_loader.py +0 -0
  153. {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/utils/usage_logger.py +0 -0
  154. {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/visibility/__init__.py +0 -0
  155. {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/visibility/model.py +0 -0
  156. {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/visibility/persistence.py +0 -0
  157. {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/visibility/resolver.py +0 -0
  158. {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp_dev.egg-info/SOURCES.txt +0 -0
  159. {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp_dev.egg-info/dependency_links.txt +0 -0
  160. {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp_dev.egg-info/entry_points.txt +0 -0
  161. {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp_dev.egg-info/requires.txt +0 -0
  162. {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp_dev.egg-info/top_level.txt +0 -0
  163. {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/tests/__init__.py +0 -0
  164. {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/tests/test_constants.py +0 -0
  165. {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/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.12.3.dev1885
3
+ Version: 7.12.3.dev1891
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.12.3.dev1885"
7
+ version = "7.12.3.dev1891"
8
8
  description = "Home Assistant MCP Server - Complete control of Home Assistant through MCP"
9
9
  readme = "README.md"
10
10
  # Upper bound widened to <3.15 so the package installs on Home Assistant's
@@ -1388,8 +1388,12 @@ class HomeAssistantClient:
1388
1388
  for attempt in range(max_retries):
1389
1389
  try:
1390
1390
  # Use per-client WebSocket keyed to this client's credentials
1391
+ # (verify_ssl included so a verify_ssl=False client never
1392
+ # shares a default-verification pooled connection).
1391
1393
  ws_client = await get_websocket_client(
1392
- url=self.base_url, token=self.token
1394
+ url=self.base_url,
1395
+ token=self.token,
1396
+ verify_ssl=self.verify_ssl,
1393
1397
  )
1394
1398
 
1395
1399
  # Special handling for render_template which returns an event with the actual result
@@ -1642,9 +1646,17 @@ class HomeAssistantClient:
1642
1646
  )
1643
1647
  return bare_id
1644
1648
 
1645
- async def get_scene_config(self, scene_id: str) -> dict[str, Any]:
1646
- """Get Home Assistant scene configuration by scene_id."""
1647
- resolved_id = await self.resolve_scene_id(scene_id)
1649
+ async def get_scene_config(
1650
+ self, scene_id: str, *, _resolved: bool = False
1651
+ ) -> dict[str, Any]:
1652
+ """Get Home Assistant scene configuration by scene_id.
1653
+
1654
+ ``_resolved=True`` signals ``scene_id`` is already the resolved storage
1655
+ key (the caller ran :meth:`resolve_scene_id`), skipping the redundant
1656
+ registry lookup. ``resolve_scene_id`` is idempotent, so the result is
1657
+ identical either way.
1658
+ """
1659
+ resolved_id = scene_id if _resolved else await self.resolve_scene_id(scene_id)
1648
1660
  try:
1649
1661
  endpoint = f"config/scene/config/{resolved_id}"
1650
1662
  response = await self._request("GET", endpoint)
@@ -1662,10 +1674,15 @@ class HomeAssistantClient:
1662
1674
  raise
1663
1675
 
1664
1676
  async def upsert_scene_config(
1665
- self, config: dict[str, Any], scene_id: str
1677
+ self, config: dict[str, Any], scene_id: str, *, _resolved: bool = False
1666
1678
  ) -> dict[str, Any]:
1667
- """Create or update Home Assistant scene configuration."""
1668
- resolved_id = await self.resolve_scene_id(scene_id)
1679
+ """Create or update Home Assistant scene configuration.
1680
+
1681
+ ``_resolved=True`` signals ``scene_id`` is already the resolved storage
1682
+ key, skipping the redundant registry lookup (``resolve_scene_id`` is
1683
+ idempotent, so the endpoint id is unchanged).
1684
+ """
1685
+ resolved_id = scene_id if _resolved else await self.resolve_scene_id(scene_id)
1669
1686
  try:
1670
1687
  endpoint = f"config/scene/config/{resolved_id}"
1671
1688
 
@@ -1699,9 +1716,16 @@ class HomeAssistantClient:
1699
1716
  logger.error(f"Failed to upsert scene config for {scene_id}: {e}")
1700
1717
  raise
1701
1718
 
1702
- async def delete_scene_config(self, scene_id: str) -> dict[str, Any]:
1703
- """Delete Home Assistant scene configuration."""
1704
- resolved_id = await self.resolve_scene_id(scene_id)
1719
+ async def delete_scene_config(
1720
+ self, scene_id: str, *, _resolved: bool = False
1721
+ ) -> dict[str, Any]:
1722
+ """Delete Home Assistant scene configuration.
1723
+
1724
+ ``_resolved=True`` signals ``scene_id`` is already the resolved storage
1725
+ key, skipping the redundant registry lookup (``resolve_scene_id`` is
1726
+ idempotent, so the endpoint id is unchanged).
1727
+ """
1728
+ resolved_id = scene_id if _resolved else await self.resolve_scene_id(scene_id)
1705
1729
  try:
1706
1730
  endpoint = f"config/scene/config/{resolved_id}"
1707
1731
  response = await self._request("DELETE", endpoint)
@@ -1037,7 +1037,7 @@ class WebSocketManager:
1037
1037
  _current_loop: asyncio.AbstractEventLoop | None = None
1038
1038
  _lock: asyncio.Lock | None = None
1039
1039
  _lock_loop: asyncio.AbstractEventLoop | None = None
1040
- _client_factory: Callable[[str, str], HomeAssistantWebSocketClient] | None = None
1040
+ _client_factory: Callable[..., HomeAssistantWebSocketClient] | None = None
1041
1041
 
1042
1042
  def __new__(cls) -> "WebSocketManager":
1043
1043
  if cls._instance is None:
@@ -1052,8 +1052,7 @@ class WebSocketManager:
1052
1052
  def configure(
1053
1053
  self,
1054
1054
  *,
1055
- client_factory: Callable[[str, str], HomeAssistantWebSocketClient]
1056
- | None = None,
1055
+ client_factory: Callable[..., HomeAssistantWebSocketClient] | None = None,
1057
1056
  ) -> None:
1058
1057
  """Configure the manager with injectable dependencies."""
1059
1058
  if client_factory is not None:
@@ -1088,6 +1087,7 @@ class WebSocketManager:
1088
1087
  self,
1089
1088
  url: str | None = None,
1090
1089
  token: str | None = None,
1090
+ verify_ssl: bool | None = None,
1091
1091
  ) -> HomeAssistantWebSocketClient:
1092
1092
  """Get WebSocket client, creating connection if needed.
1093
1093
 
@@ -1100,6 +1100,10 @@ class WebSocketManager:
1100
1100
  credentials instead of global settings. This is required
1101
1101
  for OAuth mode where each request has its own credentials.
1102
1102
  token: Optional HA token. Must be provided with url.
1103
+ verify_ssl: TLS verification override, keyed into the pool so a
1104
+ ``HomeAssistantClient(verify_ssl=False)`` caller never shares
1105
+ a connection built with default verification. ``None`` keeps
1106
+ the client's own settings-based default.
1103
1107
  """
1104
1108
  current_loop = asyncio.get_event_loop()
1105
1109
 
@@ -1135,6 +1139,8 @@ class WebSocketManager:
1135
1139
  ws_token = settings.homeassistant_token
1136
1140
 
1137
1141
  key = self._client_key(ws_url, ws_token)
1142
+ if verify_ssl is not None:
1143
+ key = f"{key}|verify_ssl={verify_ssl}"
1138
1144
 
1139
1145
  # Return existing connected client for these credentials
1140
1146
  existing = self._clients.get(key)
@@ -1148,7 +1154,11 @@ class WebSocketManager:
1148
1154
  self._last_used.pop(key, None)
1149
1155
 
1150
1156
  factory = self._client_factory or HomeAssistantWebSocketClient
1151
- client = factory(ws_url, ws_token)
1157
+ client = (
1158
+ factory(ws_url, ws_token)
1159
+ if verify_ssl is None
1160
+ else factory(ws_url, ws_token, verify_ssl=verify_ssl)
1161
+ )
1152
1162
 
1153
1163
  connected = await client.connect()
1154
1164
  if not connected:
@@ -1210,11 +1220,16 @@ websocket_manager = WebSocketManager()
1210
1220
  async def get_websocket_client(
1211
1221
  url: str | None = None,
1212
1222
  token: str | None = None,
1223
+ verify_ssl: bool | None = None,
1213
1224
  ) -> HomeAssistantWebSocketClient:
1214
1225
  """Get the global WebSocket client instance.
1215
1226
 
1216
1227
  Args:
1217
1228
  url: Optional HA URL for per-client credentials (OAuth mode).
1218
1229
  token: Optional HA token for per-client credentials (OAuth mode).
1230
+ verify_ssl: Optional TLS-verification override propagated into the
1231
+ pool key and client construction (None = settings default).
1219
1232
  """
1220
- return await websocket_manager.get_client(url=url, token=token)
1233
+ return await websocket_manager.get_client(
1234
+ url=url, token=token, verify_ssl=verify_ssl
1235
+ )
@@ -8,8 +8,8 @@ from typing import Any
8
8
 
9
9
  # Force UTF-8 encoding on Windows for Unicode output
10
10
  if sys.platform == "win32":
11
- sys.stdout.reconfigure(encoding="utf-8", errors="replace")
12
- sys.stderr.reconfigure(encoding="utf-8", errors="replace")
11
+ sys.stdout.reconfigure(encoding="utf-8", errors="replace") # type: ignore[union-attr,unused-ignore]
12
+ sys.stderr.reconfigure(encoding="utf-8", errors="replace") # type: ignore[union-attr,unused-ignore]
13
13
 
14
14
  # Set dummy credentials before any imports try to use them
15
15
  os.environ.setdefault("HOMEASSISTANT_URL", "http://smoke-test:8123")
@@ -125,7 +125,7 @@ def _pid_alive(pid: int) -> bool:
125
125
  # ctypes.windll.kernel32 (no external deps).
126
126
  import ctypes
127
127
 
128
- kernel32 = ctypes.windll.kernel32 # type: ignore[attr-defined]
128
+ kernel32 = ctypes.windll.kernel32 # type: ignore[attr-defined,unused-ignore]
129
129
  SYNCHRONIZE = 0x00100000
130
130
  handle = kernel32.OpenProcess(SYNCHRONIZE, False, pid)
131
131
  if not handle:
@@ -394,13 +394,13 @@ def _do_spawn() -> None:
394
394
  # mode the user saw.
395
395
  pythonw = Path(sys.executable).with_name("pythonw.exe")
396
396
  cmd_python = str(pythonw) if pythonw.exists() else sys.executable
397
- startupinfo = subprocess.STARTUPINFO() # type: ignore[attr-defined]
398
- startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW # type: ignore[attr-defined]
399
- startupinfo.wShowWindow = subprocess.SW_HIDE # type: ignore[attr-defined]
397
+ startupinfo = subprocess.STARTUPINFO() # type: ignore[attr-defined,unused-ignore]
398
+ startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW # type: ignore[attr-defined,unused-ignore]
399
+ startupinfo.wShowWindow = subprocess.SW_HIDE # type: ignore[attr-defined,unused-ignore]
400
400
  popen_kwargs["startupinfo"] = startupinfo
401
401
  popen_kwargs["creationflags"] = (
402
- subprocess.CREATE_NEW_PROCESS_GROUP # type: ignore[attr-defined]
403
- | subprocess.CREATE_NO_WINDOW # type: ignore[attr-defined]
402
+ subprocess.CREATE_NEW_PROCESS_GROUP # type: ignore[attr-defined,unused-ignore]
403
+ | subprocess.CREATE_NO_WINDOW # type: ignore[attr-defined,unused-ignore]
404
404
  )
405
405
  cmd = [cmd_python, "-m", "ha_mcp.stdio_settings_sidecar"]
406
406
  else:
@@ -899,22 +899,15 @@ async def list_backups(client: HomeAssistantClient, limit: int = 200) -> dict[st
899
899
  confirm a specific backup landed; they had to already know the ID. Newest
900
900
  first. Read-only: no safety backup, no restart.
901
901
  """
902
- ws_client = None
903
902
  try:
904
- ws_client, error = await get_connected_ws_client(
905
- client.base_url, client.token, verify_ssl=client.verify_ssl
906
- )
907
- if error:
908
- raise_tool_error(
909
- error
910
- or create_error_response(
911
- ErrorCode.CONNECTION_FAILED,
912
- "Failed to connect to Home Assistant WebSocket to list backups",
913
- )
914
- )
915
- ws_client = cast(HomeAssistantWebSocketClient, ws_client)
916
-
917
- info = await ws_client.send_command("backup/info")
903
+ # Route through the shared pooled WebSocket (issue #1813) rather than a
904
+ # dedicated connect/auth handshake per call. ``list_backups`` issues a
905
+ # single request/response ``backup/info`` command; the pooled client
906
+ # owns the connection lifecycle, so there is no per-call connect or
907
+ # disconnect. A failed command surfaces as ``success=False`` and is
908
+ # handled by the guard below (send_websocket_message never raises for
909
+ # WS command failures).
910
+ info = await client.send_websocket_message({"type": "backup/info"})
918
911
  if not info.get("success"):
919
912
  raise_tool_error(
920
913
  create_error_response(
@@ -955,18 +948,6 @@ async def list_backups(client: HomeAssistantClient, limit: int = 200) -> dict[st
955
948
  suggestions=["Check Home Assistant connection and the backup integration"],
956
949
  )
957
950
  return None # unreachable: exception_to_structured_error always raises
958
- finally:
959
- # Always disconnect WebSocket — narrow to transport errors; a
960
- # programming error during cleanup should still surface.
961
- if ws_client:
962
- try:
963
- await ws_client.disconnect()
964
- except (TimeoutError, OSError, ConnectionError) as err:
965
- logger.debug(
966
- "ws disconnect (cleanup) transport error: %s: %s",
967
- type(err).__name__,
968
- err,
969
- )
970
951
 
971
952
 
972
953
  # Valid (scope, action) combinations. Anything outside this set is
@@ -22,10 +22,9 @@ from pydantic import Field
22
22
  from websockets.asyncio.client import ClientConnection
23
23
 
24
24
  from .._version import is_running_in_addon
25
- from ..client.rest_client import HomeAssistantClient
25
+ from ..client.rest_client import HomeAssistantClient, HomeAssistantCommandError
26
26
  from ..errors import (
27
27
  ErrorCode,
28
- create_connection_error,
29
28
  create_error_response,
30
29
  create_validation_error,
31
30
  )
@@ -36,7 +35,6 @@ from ..utils.python_sandbox import (
36
35
  )
37
36
  from .helpers import (
38
37
  exception_to_structured_error,
39
- get_connected_ws_client,
40
38
  log_tool_usage,
41
39
  raise_tool_error,
42
40
  validate_identifier_not_empty,
@@ -233,16 +231,7 @@ async def _supervisor_api_call(
233
231
  Returns:
234
232
  The "result" field from a successful response, or an error dict.
235
233
  """
236
- ws_client = None
237
234
  try:
238
- ws_client, error = await get_connected_ws_client(
239
- client.base_url, client.token, verify_ssl=client.verify_ssl
240
- )
241
- if error or ws_client is None:
242
- return error or create_connection_error(
243
- "Failed to establish WebSocket connection",
244
- )
245
-
246
235
  kwargs: dict[str, Any] = {"endpoint": endpoint, "method": method}
247
236
  if data is not None:
248
237
  kwargs["data"] = data
@@ -256,29 +245,20 @@ async def _supervisor_api_call(
256
245
  kwargs["timeout"] = timeout
257
246
  wait_timeout = float(timeout) + 15.0
258
247
 
259
- result = await ws_client.send_command(
260
- "supervisor/api", _wait_timeout=wait_timeout, **kwargs
248
+ # Route through the shared pooled WebSocket (issue #1813) rather than
249
+ # opening a dedicated connect/auth handshake per call. ``_wait_timeout``
250
+ # is consumed by send_command (it never reaches Home Assistant). The
251
+ # pooled path collapses a failed WS command into
252
+ # ``{"success": False, "error": ...}``; re-raise it as the same
253
+ # ``HomeAssistantCommandError`` the dedicated send_command used to raise
254
+ # so the classifier below maps schema/not-found/etc. identically.
255
+ result = await client.send_websocket_message(
256
+ {"type": "supervisor/api", "_wait_timeout": wait_timeout, **kwargs}
261
257
  )
262
258
 
263
259
  if not result.get("success"):
264
- error_msg = str(result.get("error", ""))
265
- if "not_found" in error_msg.lower() or "unknown" in error_msg.lower():
266
- raise_tool_error(
267
- create_error_response(
268
- ErrorCode.RESOURCE_NOT_FOUND,
269
- "Supervisor API not available",
270
- details=str(result),
271
- suggestions=[
272
- "This feature requires Home Assistant OS or Supervised installation",
273
- ],
274
- )
275
- )
276
- raise_tool_error(
277
- create_error_response(
278
- ErrorCode.SERVICE_CALL_FAILED,
279
- f"Supervisor API call failed: {endpoint}",
280
- details=str(result),
281
- )
260
+ raise HomeAssistantCommandError(
261
+ str(result.get("error", f"Supervisor API call failed: {endpoint}"))
282
262
  )
283
263
 
284
264
  return {"success": True, "result": result.get("result", {})}
@@ -293,14 +273,6 @@ async def _supervisor_api_call(
293
273
  suggestions=["Check Home Assistant connection and Supervisor availability"],
294
274
  )
295
275
  return None # unreachable: exception_to_structured_error always raises
296
- finally:
297
- if ws_client:
298
- try:
299
- await ws_client.disconnect()
300
- except Exception:
301
- # Best-effort cleanup: the WS connection is being torn down, so a
302
- # disconnect failure is non-fatal and must not mask the real result.
303
- pass
304
276
 
305
277
 
306
278
  def _addon_connection_failure_suggestions(
@@ -32,6 +32,7 @@ from ..utils.usage_logger import (
32
32
  get_recent_logs,
33
33
  get_startup_logs,
34
34
  )
35
+ from .component_api import get_component_caps
35
36
  from .helpers import log_tool_usage, register_tool_methods
36
37
  from .util_helpers import ANSI_ESCAPE_RE
37
38
 
@@ -613,11 +614,21 @@ class BugReportTools:
613
614
  async def _detect_component_version(self) -> str | None:
614
615
  """Read the ha_mcp_tools custom component's version, best-effort.
615
616
 
616
- Uses the component's ``get_caller_token`` bootstrap service, whose
617
- response carries the manifest version (the same field the filesystem
618
- tools' version gate reads). Returns None when the component is not
619
- installed or the call fails — the report path must never break on it.
617
+ Prefers the shared cached capability probe (``get_component_caps`` — one
618
+ ``ha_mcp_tools/info`` round-trip per client): a component new enough to
619
+ answer it (1.1.0+) reports its version there, so the report reuses that
620
+ cached probe instead of a bespoke round-trip. A component in the
621
+ 0.11.0-1.1.0 band has services but no ``info`` command (caps is None),
622
+ so it falls back to the ``get_caller_token`` bootstrap service, whose
623
+ response also carries the manifest version. Returns None when the
624
+ component is not installed or the call fails — the report path must
625
+ never break on it.
620
626
  """
627
+ # get_component_caps never raises (it caches/returns None on every
628
+ # failure mode), so no guard is needed around it.
629
+ caps = await get_component_caps(self._client)
630
+ if caps is not None:
631
+ return caps.component_version or None
621
632
  try:
622
633
  resp = await self._client.call_service(
623
634
  "ha_mcp_tools", "get_caller_token", {}, return_response=True
@@ -15,11 +15,11 @@ from fastmcp.exceptions import ToolError
15
15
  from fastmcp.tools import tool
16
16
  from pydantic import Field
17
17
 
18
+ from ..client.rest_client import HomeAssistantCommandError
18
19
  from ..errors import ErrorCode, create_error_response
19
20
  from .auto_backup import with_auto_backup
20
21
  from .helpers import (
21
22
  exception_to_structured_error,
22
- get_connected_ws_client,
23
23
  log_tool_usage,
24
24
  raise_tool_error,
25
25
  register_tool_methods,
@@ -199,35 +199,20 @@ class CalendarTools:
199
199
  if location is not None:
200
200
  event["location"] = location
201
201
 
202
- ws_client, conn_error = await get_connected_ws_client(
203
- self._client.base_url,
204
- self._client.token,
205
- verify_ssl=self._client.verify_ssl,
202
+ # Route through the shared pooled WebSocket (issue #1813) instead of a
203
+ # dedicated connect/auth handshake per call. The pooled path collapses a
204
+ # failed WS command into ``{"success": False, "error": ...}``; re-raise
205
+ # it as the same ``HomeAssistantCommandError`` the dedicated send_command
206
+ # used to raise so the caller's error handler still attaches the
207
+ # rrule-specific suggestions.
208
+ result = await self._client.send_websocket_message(
209
+ {"type": "calendar/event/create", "entity_id": entity_id, "event": event}
206
210
  )
207
- if conn_error or ws_client is None:
208
- raise_tool_error(
209
- conn_error
210
- or create_error_response(
211
- ErrorCode.CONNECTION_FAILED,
212
- "Failed to connect to Home Assistant WebSocket",
213
- context={"entity_id": entity_id},
214
- )
211
+ if not result.get("success"):
212
+ raise HomeAssistantCommandError(
213
+ str(result.get("error", "calendar/event/create failed"))
215
214
  )
216
-
217
- try:
218
- return await ws_client.send_command(
219
- "calendar/event/create", entity_id=entity_id, event=event
220
- )
221
- finally:
222
- # Guard disconnect: a transport-teardown error here would
223
- # otherwise replace the original send_command exception.
224
- try:
225
- await ws_client.disconnect()
226
- except Exception as disconnect_error:
227
- logger.debug(
228
- f"WebSocket disconnect after create_event for "
229
- f"{entity_id}: {disconnect_error}"
230
- )
215
+ return result
231
216
 
232
217
  async def _create_simple_calendar_event(
233
218
  self,
@@ -541,36 +526,19 @@ class CalendarTools:
541
526
  if recurrence_range:
542
527
  ws_kwargs["recurrence_range"] = recurrence_range
543
528
 
544
- ws_client, conn_error = await get_connected_ws_client(
545
- self._client.base_url,
546
- self._client.token,
547
- verify_ssl=self._client.verify_ssl,
529
+ # Route through the shared pooled WebSocket (issue #1813) instead of a
530
+ # dedicated connect/auth handshake per call. Re-raise a failed WS
531
+ # command as the same ``HomeAssistantCommandError`` the dedicated
532
+ # send_command used to raise so the outer handler builds the
533
+ # delete-specific suggestions (404 / not-supported).
534
+ result = await self._client.send_websocket_message(
535
+ {"type": "calendar/event/delete", **ws_kwargs}
548
536
  )
549
- if conn_error or ws_client is None:
550
- raise_tool_error(
551
- conn_error
552
- or create_error_response(
553
- ErrorCode.CONNECTION_FAILED,
554
- "Failed to connect to Home Assistant WebSocket",
555
- context={"entity_id": entity_id, "uid": uid},
556
- )
537
+ if not result.get("success"):
538
+ raise HomeAssistantCommandError(
539
+ str(result.get("error", "calendar/event/delete failed"))
557
540
  )
558
541
 
559
- try:
560
- result = await ws_client.send_command(
561
- "calendar/event/delete", **ws_kwargs
562
- )
563
- finally:
564
- # Guard disconnect: a transport-teardown error here would
565
- # otherwise replace the original send_command exception.
566
- try:
567
- await ws_client.disconnect()
568
- except Exception as disconnect_error:
569
- logger.debug(
570
- f"WebSocket disconnect after delete_event for "
571
- f"{entity_id} uid={uid}: {disconnect_error}"
572
- )
573
-
574
542
  return {
575
543
  "success": True,
576
544
  "entity_id": entity_id,