ha-mcp-dev 7.9.0.dev759__tar.gz → 7.9.0.dev760__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 (154) hide show
  1. {ha_mcp_dev-7.9.0.dev759/src/ha_mcp_dev.egg-info → ha_mcp_dev-7.9.0.dev760}/PKG-INFO +1 -1
  2. {ha_mcp_dev-7.9.0.dev759 → ha_mcp_dev-7.9.0.dev760}/pyproject.toml +1 -1
  3. {ha_mcp_dev-7.9.0.dev759 → ha_mcp_dev-7.9.0.dev760}/src/ha_mcp/read_only.py +18 -0
  4. {ha_mcp_dev-7.9.0.dev759 → ha_mcp_dev-7.9.0.dev760}/src/ha_mcp/tools/best_practice_checker.py +73 -2
  5. {ha_mcp_dev-7.9.0.dev759 → ha_mcp_dev-7.9.0.dev760}/src/ha_mcp/tools/tools_config_automations.py +2 -0
  6. {ha_mcp_dev-7.9.0.dev759 → ha_mcp_dev-7.9.0.dev760}/src/ha_mcp/tools/tools_traces.py +7 -0
  7. {ha_mcp_dev-7.9.0.dev759 → ha_mcp_dev-7.9.0.dev760}/src/ha_mcp/tools/tools_updates.py +254 -56
  8. {ha_mcp_dev-7.9.0.dev759 → ha_mcp_dev-7.9.0.dev760}/src/ha_mcp/update_check.py +2 -2
  9. {ha_mcp_dev-7.9.0.dev759 → ha_mcp_dev-7.9.0.dev760}/src/ha_mcp/utils/domain_handlers.py +10 -1
  10. {ha_mcp_dev-7.9.0.dev759 → ha_mcp_dev-7.9.0.dev760/src/ha_mcp_dev.egg-info}/PKG-INFO +1 -1
  11. {ha_mcp_dev-7.9.0.dev759 → ha_mcp_dev-7.9.0.dev760}/LICENSE +0 -0
  12. {ha_mcp_dev-7.9.0.dev759 → ha_mcp_dev-7.9.0.dev760}/MANIFEST.in +0 -0
  13. {ha_mcp_dev-7.9.0.dev759 → ha_mcp_dev-7.9.0.dev760}/README.md +0 -0
  14. {ha_mcp_dev-7.9.0.dev759 → ha_mcp_dev-7.9.0.dev760}/setup.cfg +0 -0
  15. {ha_mcp_dev-7.9.0.dev759 → ha_mcp_dev-7.9.0.dev760}/src/ha_mcp/__init__.py +0 -0
  16. {ha_mcp_dev-7.9.0.dev759 → ha_mcp_dev-7.9.0.dev760}/src/ha_mcp/__main__.py +0 -0
  17. {ha_mcp_dev-7.9.0.dev759 → ha_mcp_dev-7.9.0.dev760}/src/ha_mcp/_pypi_marker +0 -0
  18. {ha_mcp_dev-7.9.0.dev759 → ha_mcp_dev-7.9.0.dev760}/src/ha_mcp/_version.py +0 -0
  19. {ha_mcp_dev-7.9.0.dev759 → ha_mcp_dev-7.9.0.dev760}/src/ha_mcp/auth/__init__.py +0 -0
  20. {ha_mcp_dev-7.9.0.dev759 → ha_mcp_dev-7.9.0.dev760}/src/ha_mcp/auth/consent_form.py +0 -0
  21. {ha_mcp_dev-7.9.0.dev759 → ha_mcp_dev-7.9.0.dev760}/src/ha_mcp/auth/provider.py +0 -0
  22. {ha_mcp_dev-7.9.0.dev759 → ha_mcp_dev-7.9.0.dev760}/src/ha_mcp/backup_manager.py +0 -0
  23. {ha_mcp_dev-7.9.0.dev759 → ha_mcp_dev-7.9.0.dev760}/src/ha_mcp/client/__init__.py +0 -0
  24. {ha_mcp_dev-7.9.0.dev759 → ha_mcp_dev-7.9.0.dev760}/src/ha_mcp/client/rest_client.py +0 -0
  25. {ha_mcp_dev-7.9.0.dev759 → ha_mcp_dev-7.9.0.dev760}/src/ha_mcp/client/supervisor_client.py +0 -0
  26. {ha_mcp_dev-7.9.0.dev759 → ha_mcp_dev-7.9.0.dev760}/src/ha_mcp/client/websocket_client.py +0 -0
  27. {ha_mcp_dev-7.9.0.dev759 → ha_mcp_dev-7.9.0.dev760}/src/ha_mcp/client/websocket_listener.py +0 -0
  28. {ha_mcp_dev-7.9.0.dev759 → ha_mcp_dev-7.9.0.dev760}/src/ha_mcp/config.py +0 -0
  29. {ha_mcp_dev-7.9.0.dev759 → ha_mcp_dev-7.9.0.dev760}/src/ha_mcp/dashboard_screenshot/__init__.py +0 -0
  30. {ha_mcp_dev-7.9.0.dev759 → ha_mcp_dev-7.9.0.dev760}/src/ha_mcp/dashboard_screenshot/capture.py +0 -0
  31. {ha_mcp_dev-7.9.0.dev759 → ha_mcp_dev-7.9.0.dev760}/src/ha_mcp/dashboard_screenshot/provision.py +0 -0
  32. {ha_mcp_dev-7.9.0.dev759 → ha_mcp_dev-7.9.0.dev760}/src/ha_mcp/errors.py +0 -0
  33. {ha_mcp_dev-7.9.0.dev759 → ha_mcp_dev-7.9.0.dev760}/src/ha_mcp/policy/__init__.py +0 -0
  34. {ha_mcp_dev-7.9.0.dev759 → ha_mcp_dev-7.9.0.dev760}/src/ha_mcp/policy/approval_queue.py +0 -0
  35. {ha_mcp_dev-7.9.0.dev759 → ha_mcp_dev-7.9.0.dev760}/src/ha_mcp/policy/evaluator.py +0 -0
  36. {ha_mcp_dev-7.9.0.dev759 → ha_mcp_dev-7.9.0.dev760}/src/ha_mcp/policy/handlers.py +0 -0
  37. {ha_mcp_dev-7.9.0.dev759 → ha_mcp_dev-7.9.0.dev760}/src/ha_mcp/policy/middleware.py +0 -0
  38. {ha_mcp_dev-7.9.0.dev759 → ha_mcp_dev-7.9.0.dev760}/src/ha_mcp/policy/model.py +0 -0
  39. {ha_mcp_dev-7.9.0.dev759 → ha_mcp_dev-7.9.0.dev760}/src/ha_mcp/policy/persistence.py +0 -0
  40. {ha_mcp_dev-7.9.0.dev759 → ha_mcp_dev-7.9.0.dev760}/src/ha_mcp/policy/value_sources.py +0 -0
  41. {ha_mcp_dev-7.9.0.dev759 → ha_mcp_dev-7.9.0.dev760}/src/ha_mcp/py.typed +0 -0
  42. {ha_mcp_dev-7.9.0.dev759 → ha_mcp_dev-7.9.0.dev760}/src/ha_mcp/resources/skills-vendor/.claude/settings.json +0 -0
  43. {ha_mcp_dev-7.9.0.dev759 → ha_mcp_dev-7.9.0.dev760}/src/ha_mcp/resources/skills-vendor/.claude-plugin/marketplace.json +0 -0
  44. {ha_mcp_dev-7.9.0.dev759 → ha_mcp_dev-7.9.0.dev760}/src/ha_mcp/resources/skills-vendor/.claude-plugin/plugin.json +0 -0
  45. {ha_mcp_dev-7.9.0.dev759 → ha_mcp_dev-7.9.0.dev760}/src/ha_mcp/resources/skills-vendor/.github/ISSUE_TEMPLATE/skill-rca.md +0 -0
  46. {ha_mcp_dev-7.9.0.dev759 → ha_mcp_dev-7.9.0.dev760}/src/ha_mcp/resources/skills-vendor/.github/pull_request_template.md +0 -0
  47. {ha_mcp_dev-7.9.0.dev759 → ha_mcp_dev-7.9.0.dev760}/src/ha_mcp/resources/skills-vendor/AGENTS.md +0 -0
  48. {ha_mcp_dev-7.9.0.dev759 → ha_mcp_dev-7.9.0.dev760}/src/ha_mcp/resources/skills-vendor/CLAUDE.md +0 -0
  49. {ha_mcp_dev-7.9.0.dev759 → ha_mcp_dev-7.9.0.dev760}/src/ha_mcp/resources/skills-vendor/CONTRIBUTING.md +0 -0
  50. {ha_mcp_dev-7.9.0.dev759 → ha_mcp_dev-7.9.0.dev760}/src/ha_mcp/resources/skills-vendor/LICENSE +0 -0
  51. {ha_mcp_dev-7.9.0.dev759 → ha_mcp_dev-7.9.0.dev760}/src/ha_mcp/resources/skills-vendor/README.md +0 -0
  52. {ha_mcp_dev-7.9.0.dev759 → ha_mcp_dev-7.9.0.dev760}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/SKILL.md +0 -0
  53. {ha_mcp_dev-7.9.0.dev759 → ha_mcp_dev-7.9.0.dev760}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/evals/evals.json +0 -0
  54. {ha_mcp_dev-7.9.0.dev759 → ha_mcp_dev-7.9.0.dev760}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/appdaemon.md +0 -0
  55. {ha_mcp_dev-7.9.0.dev759 → ha_mcp_dev-7.9.0.dev760}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/automation-patterns.md +0 -0
  56. {ha_mcp_dev-7.9.0.dev759 → ha_mcp_dev-7.9.0.dev760}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-cards.md +0 -0
  57. {ha_mcp_dev-7.9.0.dev759 → ha_mcp_dev-7.9.0.dev760}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-guide.md +0 -0
  58. {ha_mcp_dev-7.9.0.dev759 → ha_mcp_dev-7.9.0.dev760}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/device-control.md +0 -0
  59. {ha_mcp_dev-7.9.0.dev759 → ha_mcp_dev-7.9.0.dev760}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/domain-docs.md +0 -0
  60. {ha_mcp_dev-7.9.0.dev759 → ha_mcp_dev-7.9.0.dev760}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/examples.yaml +0 -0
  61. {ha_mcp_dev-7.9.0.dev759 → ha_mcp_dev-7.9.0.dev760}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/helper-selection.md +0 -0
  62. {ha_mcp_dev-7.9.0.dev759 → ha_mcp_dev-7.9.0.dev760}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/safe-refactoring.md +0 -0
  63. {ha_mcp_dev-7.9.0.dev759 → ha_mcp_dev-7.9.0.dev760}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/scenes.md +0 -0
  64. {ha_mcp_dev-7.9.0.dev759 → ha_mcp_dev-7.9.0.dev760}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/template-guidelines.md +0 -0
  65. {ha_mcp_dev-7.9.0.dev759 → ha_mcp_dev-7.9.0.dev760}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/yaml-only-integrations.md +0 -0
  66. {ha_mcp_dev-7.9.0.dev759 → ha_mcp_dev-7.9.0.dev760}/src/ha_mcp/server.py +0 -0
  67. {ha_mcp_dev-7.9.0.dev759 → ha_mcp_dev-7.9.0.dev760}/src/ha_mcp/settings_ui/__init__.py +0 -0
  68. {ha_mcp_dev-7.9.0.dev759 → ha_mcp_dev-7.9.0.dev760}/src/ha_mcp/settings_ui/settings.css +0 -0
  69. {ha_mcp_dev-7.9.0.dev759 → ha_mcp_dev-7.9.0.dev760}/src/ha_mcp/settings_ui/settings.html +0 -0
  70. {ha_mcp_dev-7.9.0.dev759 → ha_mcp_dev-7.9.0.dev760}/src/ha_mcp/settings_ui/settings.js +0 -0
  71. {ha_mcp_dev-7.9.0.dev759 → ha_mcp_dev-7.9.0.dev760}/src/ha_mcp/smoke_test.py +0 -0
  72. {ha_mcp_dev-7.9.0.dev759 → ha_mcp_dev-7.9.0.dev760}/src/ha_mcp/stdio_settings_sidecar.py +0 -0
  73. {ha_mcp_dev-7.9.0.dev759 → ha_mcp_dev-7.9.0.dev760}/src/ha_mcp/tools/__init__.py +0 -0
  74. {ha_mcp_dev-7.9.0.dev759 → ha_mcp_dev-7.9.0.dev760}/src/ha_mcp/tools/auto_backup.py +0 -0
  75. {ha_mcp_dev-7.9.0.dev759 → ha_mcp_dev-7.9.0.dev760}/src/ha_mcp/tools/backup.py +0 -0
  76. {ha_mcp_dev-7.9.0.dev759 → ha_mcp_dev-7.9.0.dev760}/src/ha_mcp/tools/config_entry_flow.py +0 -0
  77. {ha_mcp_dev-7.9.0.dev759 → ha_mcp_dev-7.9.0.dev760}/src/ha_mcp/tools/device_control.py +0 -0
  78. {ha_mcp_dev-7.9.0.dev759 → ha_mcp_dev-7.9.0.dev760}/src/ha_mcp/tools/enhanced.py +0 -0
  79. {ha_mcp_dev-7.9.0.dev759 → ha_mcp_dev-7.9.0.dev760}/src/ha_mcp/tools/helpers.py +0 -0
  80. {ha_mcp_dev-7.9.0.dev759 → ha_mcp_dev-7.9.0.dev760}/src/ha_mcp/tools/radio/__init__.py +0 -0
  81. {ha_mcp_dev-7.9.0.dev759 → ha_mcp_dev-7.9.0.dev760}/src/ha_mcp/tools/radio/base.py +0 -0
  82. {ha_mcp_dev-7.9.0.dev759 → ha_mcp_dev-7.9.0.dev760}/src/ha_mcp/tools/radio/matter.py +0 -0
  83. {ha_mcp_dev-7.9.0.dev759 → ha_mcp_dev-7.9.0.dev760}/src/ha_mcp/tools/radio/thread.py +0 -0
  84. {ha_mcp_dev-7.9.0.dev759 → ha_mcp_dev-7.9.0.dev760}/src/ha_mcp/tools/radio/zigbee.py +0 -0
  85. {ha_mcp_dev-7.9.0.dev759 → ha_mcp_dev-7.9.0.dev760}/src/ha_mcp/tools/radio/zwave.py +0 -0
  86. {ha_mcp_dev-7.9.0.dev759 → ha_mcp_dev-7.9.0.dev760}/src/ha_mcp/tools/reference_validator.py +0 -0
  87. {ha_mcp_dev-7.9.0.dev759 → ha_mcp_dev-7.9.0.dev760}/src/ha_mcp/tools/registry.py +0 -0
  88. {ha_mcp_dev-7.9.0.dev759 → ha_mcp_dev-7.9.0.dev760}/src/ha_mcp/tools/smart_search/__init__.py +0 -0
  89. {ha_mcp_dev-7.9.0.dev759 → ha_mcp_dev-7.9.0.dev760}/src/ha_mcp/tools/smart_search/_base.py +0 -0
  90. {ha_mcp_dev-7.9.0.dev759 → ha_mcp_dev-7.9.0.dev760}/src/ha_mcp/tools/smart_search/_config.py +0 -0
  91. {ha_mcp_dev-7.9.0.dev759 → ha_mcp_dev-7.9.0.dev760}/src/ha_mcp/tools/smart_search/_deep.py +0 -0
  92. {ha_mcp_dev-7.9.0.dev759 → ha_mcp_dev-7.9.0.dev760}/src/ha_mcp/tools/smart_search/_entities.py +0 -0
  93. {ha_mcp_dev-7.9.0.dev759 → ha_mcp_dev-7.9.0.dev760}/src/ha_mcp/tools/smart_search/_fetch.py +0 -0
  94. {ha_mcp_dev-7.9.0.dev759 → ha_mcp_dev-7.9.0.dev760}/src/ha_mcp/tools/smart_search/_overview.py +0 -0
  95. {ha_mcp_dev-7.9.0.dev759 → ha_mcp_dev-7.9.0.dev760}/src/ha_mcp/tools/smart_search/_scenes.py +0 -0
  96. {ha_mcp_dev-7.9.0.dev759 → ha_mcp_dev-7.9.0.dev760}/src/ha_mcp/tools/smart_search/_scoring.py +0 -0
  97. {ha_mcp_dev-7.9.0.dev759 → ha_mcp_dev-7.9.0.dev760}/src/ha_mcp/tools/tool_search_hint_middleware.py +0 -0
  98. {ha_mcp_dev-7.9.0.dev759 → ha_mcp_dev-7.9.0.dev760}/src/ha_mcp/tools/tools_addons.py +0 -0
  99. {ha_mcp_dev-7.9.0.dev759 → ha_mcp_dev-7.9.0.dev760}/src/ha_mcp/tools/tools_areas.py +0 -0
  100. {ha_mcp_dev-7.9.0.dev759 → ha_mcp_dev-7.9.0.dev760}/src/ha_mcp/tools/tools_blueprints.py +0 -0
  101. {ha_mcp_dev-7.9.0.dev759 → ha_mcp_dev-7.9.0.dev760}/src/ha_mcp/tools/tools_bug_report.py +0 -0
  102. {ha_mcp_dev-7.9.0.dev759 → ha_mcp_dev-7.9.0.dev760}/src/ha_mcp/tools/tools_calendar.py +0 -0
  103. {ha_mcp_dev-7.9.0.dev759 → ha_mcp_dev-7.9.0.dev760}/src/ha_mcp/tools/tools_camera.py +0 -0
  104. {ha_mcp_dev-7.9.0.dev759 → ha_mcp_dev-7.9.0.dev760}/src/ha_mcp/tools/tools_categories.py +0 -0
  105. {ha_mcp_dev-7.9.0.dev759 → ha_mcp_dev-7.9.0.dev760}/src/ha_mcp/tools/tools_code.py +0 -0
  106. {ha_mcp_dev-7.9.0.dev759 → ha_mcp_dev-7.9.0.dev760}/src/ha_mcp/tools/tools_config_dashboards.py +0 -0
  107. {ha_mcp_dev-7.9.0.dev759 → ha_mcp_dev-7.9.0.dev760}/src/ha_mcp/tools/tools_config_helpers.py +0 -0
  108. {ha_mcp_dev-7.9.0.dev759 → ha_mcp_dev-7.9.0.dev760}/src/ha_mcp/tools/tools_config_scenes.py +0 -0
  109. {ha_mcp_dev-7.9.0.dev759 → ha_mcp_dev-7.9.0.dev760}/src/ha_mcp/tools/tools_config_scripts.py +0 -0
  110. {ha_mcp_dev-7.9.0.dev759 → ha_mcp_dev-7.9.0.dev760}/src/ha_mcp/tools/tools_dashboard_screenshot.py +0 -0
  111. {ha_mcp_dev-7.9.0.dev759 → ha_mcp_dev-7.9.0.dev760}/src/ha_mcp/tools/tools_energy.py +0 -0
  112. {ha_mcp_dev-7.9.0.dev759 → ha_mcp_dev-7.9.0.dev760}/src/ha_mcp/tools/tools_entities.py +0 -0
  113. {ha_mcp_dev-7.9.0.dev759 → ha_mcp_dev-7.9.0.dev760}/src/ha_mcp/tools/tools_filesystem.py +0 -0
  114. {ha_mcp_dev-7.9.0.dev759 → ha_mcp_dev-7.9.0.dev760}/src/ha_mcp/tools/tools_groups.py +0 -0
  115. {ha_mcp_dev-7.9.0.dev759 → ha_mcp_dev-7.9.0.dev760}/src/ha_mcp/tools/tools_hacs.py +0 -0
  116. {ha_mcp_dev-7.9.0.dev759 → ha_mcp_dev-7.9.0.dev760}/src/ha_mcp/tools/tools_history.py +0 -0
  117. {ha_mcp_dev-7.9.0.dev759 → ha_mcp_dev-7.9.0.dev760}/src/ha_mcp/tools/tools_integrations.py +0 -0
  118. {ha_mcp_dev-7.9.0.dev759 → ha_mcp_dev-7.9.0.dev760}/src/ha_mcp/tools/tools_labels.py +0 -0
  119. {ha_mcp_dev-7.9.0.dev759 → ha_mcp_dev-7.9.0.dev760}/src/ha_mcp/tools/tools_mcp_component.py +0 -0
  120. {ha_mcp_dev-7.9.0.dev759 → ha_mcp_dev-7.9.0.dev760}/src/ha_mcp/tools/tools_radio.py +0 -0
  121. {ha_mcp_dev-7.9.0.dev759 → ha_mcp_dev-7.9.0.dev760}/src/ha_mcp/tools/tools_registry.py +0 -0
  122. {ha_mcp_dev-7.9.0.dev759 → ha_mcp_dev-7.9.0.dev760}/src/ha_mcp/tools/tools_resources.py +0 -0
  123. {ha_mcp_dev-7.9.0.dev759 → ha_mcp_dev-7.9.0.dev760}/src/ha_mcp/tools/tools_search.py +0 -0
  124. {ha_mcp_dev-7.9.0.dev759 → ha_mcp_dev-7.9.0.dev760}/src/ha_mcp/tools/tools_service.py +0 -0
  125. {ha_mcp_dev-7.9.0.dev759 → ha_mcp_dev-7.9.0.dev760}/src/ha_mcp/tools/tools_services.py +0 -0
  126. {ha_mcp_dev-7.9.0.dev759 → ha_mcp_dev-7.9.0.dev760}/src/ha_mcp/tools/tools_system.py +0 -0
  127. {ha_mcp_dev-7.9.0.dev759 → ha_mcp_dev-7.9.0.dev760}/src/ha_mcp/tools/tools_themes.py +0 -0
  128. {ha_mcp_dev-7.9.0.dev759 → ha_mcp_dev-7.9.0.dev760}/src/ha_mcp/tools/tools_todo.py +0 -0
  129. {ha_mcp_dev-7.9.0.dev759 → ha_mcp_dev-7.9.0.dev760}/src/ha_mcp/tools/tools_utility.py +0 -0
  130. {ha_mcp_dev-7.9.0.dev759 → ha_mcp_dev-7.9.0.dev760}/src/ha_mcp/tools/tools_voice_assistant.py +0 -0
  131. {ha_mcp_dev-7.9.0.dev759 → ha_mcp_dev-7.9.0.dev760}/src/ha_mcp/tools/tools_yaml_config.py +0 -0
  132. {ha_mcp_dev-7.9.0.dev759 → ha_mcp_dev-7.9.0.dev760}/src/ha_mcp/tools/tools_zones.py +0 -0
  133. {ha_mcp_dev-7.9.0.dev759 → ha_mcp_dev-7.9.0.dev760}/src/ha_mcp/tools/util_helpers.py +0 -0
  134. {ha_mcp_dev-7.9.0.dev759 → ha_mcp_dev-7.9.0.dev760}/src/ha_mcp/tools/validation_middleware.py +0 -0
  135. {ha_mcp_dev-7.9.0.dev759 → ha_mcp_dev-7.9.0.dev760}/src/ha_mcp/transforms/__init__.py +0 -0
  136. {ha_mcp_dev-7.9.0.dev759 → ha_mcp_dev-7.9.0.dev760}/src/ha_mcp/transforms/categorized_search.py +0 -0
  137. {ha_mcp_dev-7.9.0.dev759 → ha_mcp_dev-7.9.0.dev760}/src/ha_mcp/transforms/lite_docstrings.py +0 -0
  138. {ha_mcp_dev-7.9.0.dev759 → ha_mcp_dev-7.9.0.dev760}/src/ha_mcp/utils/__init__.py +0 -0
  139. {ha_mcp_dev-7.9.0.dev759 → ha_mcp_dev-7.9.0.dev760}/src/ha_mcp/utils/config_hash.py +0 -0
  140. {ha_mcp_dev-7.9.0.dev759 → ha_mcp_dev-7.9.0.dev760}/src/ha_mcp/utils/data_paths.py +0 -0
  141. {ha_mcp_dev-7.9.0.dev759 → ha_mcp_dev-7.9.0.dev760}/src/ha_mcp/utils/fuzzy_search.py +0 -0
  142. {ha_mcp_dev-7.9.0.dev759 → ha_mcp_dev-7.9.0.dev760}/src/ha_mcp/utils/kill_signal_diagnostics.py +0 -0
  143. {ha_mcp_dev-7.9.0.dev759 → ha_mcp_dev-7.9.0.dev760}/src/ha_mcp/utils/operation_manager.py +0 -0
  144. {ha_mcp_dev-7.9.0.dev759 → ha_mcp_dev-7.9.0.dev760}/src/ha_mcp/utils/python_sandbox.py +0 -0
  145. {ha_mcp_dev-7.9.0.dev759 → ha_mcp_dev-7.9.0.dev760}/src/ha_mcp/utils/skill_loader.py +0 -0
  146. {ha_mcp_dev-7.9.0.dev759 → ha_mcp_dev-7.9.0.dev760}/src/ha_mcp/utils/usage_logger.py +0 -0
  147. {ha_mcp_dev-7.9.0.dev759 → ha_mcp_dev-7.9.0.dev760}/src/ha_mcp_dev.egg-info/SOURCES.txt +0 -0
  148. {ha_mcp_dev-7.9.0.dev759 → ha_mcp_dev-7.9.0.dev760}/src/ha_mcp_dev.egg-info/dependency_links.txt +0 -0
  149. {ha_mcp_dev-7.9.0.dev759 → ha_mcp_dev-7.9.0.dev760}/src/ha_mcp_dev.egg-info/entry_points.txt +0 -0
  150. {ha_mcp_dev-7.9.0.dev759 → ha_mcp_dev-7.9.0.dev760}/src/ha_mcp_dev.egg-info/requires.txt +0 -0
  151. {ha_mcp_dev-7.9.0.dev759 → ha_mcp_dev-7.9.0.dev760}/src/ha_mcp_dev.egg-info/top_level.txt +0 -0
  152. {ha_mcp_dev-7.9.0.dev759 → ha_mcp_dev-7.9.0.dev760}/tests/__init__.py +0 -0
  153. {ha_mcp_dev-7.9.0.dev759 → ha_mcp_dev-7.9.0.dev760}/tests/test_constants.py +0 -0
  154. {ha_mcp_dev-7.9.0.dev759 → ha_mcp_dev-7.9.0.dev760}/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.dev759
3
+ Version: 7.9.0.dev760
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.9.0.dev759"
7
+ version = "7.9.0.dev760"
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,6 +131,16 @@ def _custom_tool_write(args: dict[str, Any]) -> str | None:
131
131
  return "sandbox code execution"
132
132
 
133
133
 
134
+ def _updates_write(args: dict[str, Any]) -> str | None:
135
+ # ``action`` defaults to "list" at the schema layer, so a missing action
136
+ # executes as a read (the middleware sees RAW pre-validation arguments —
137
+ # an absent key here means the tool itself will run the list branch).
138
+ action = args.get("action")
139
+ if action in (None, "list", "get"):
140
+ return None
141
+ return f"action={action!r}"
142
+
143
+
134
144
  def _radio_write(args: dict[str, Any]) -> str | None:
135
145
  action = args.get("action")
136
146
  # Reads (allowed): per-node diagnostics, the integration/network summary,
@@ -200,6 +210,14 @@ READ_ONLY_EXEMPT_TOOLS: dict[str, ReadOnlyExemption] = {
200
210
  "cluster-attribute read ('cluster_read'), and the Thread dataset "
201
211
  "listing ('list_datasets')",
202
212
  ),
213
+ # Update listing/details exist only here — ha_get_updates was merged
214
+ # into this tool (issue #1726), so hiding it would remove the read
215
+ # surface entirely.
216
+ "ha_manage_updates": ReadOnlyExemption(
217
+ _updates_write,
218
+ "listing pending updates (action='list', the default) and reading "
219
+ "update details/release notes (action='get')",
220
+ ),
203
221
  }
204
222
 
205
223
 
@@ -162,6 +162,28 @@ _TARGET_FIELDS = ("entity_id", "device_id", "area_id", "floor_id", "label_id")
162
162
  # ``service:`` (legacy) and ``action:`` (modern, 2024+) for the same field.
163
163
  _SERVICE_KEYS = ("service", "action")
164
164
 
165
+ # Purpose-specific trigger/condition keys renamed in HA 2026.7 — the old keys
166
+ # no longer load (home-assistant/core#174463).
167
+ _RENAMED_TRIGGER_KEYS = {
168
+ "battery.low": "battery.became_low",
169
+ "battery.not_low": "battery.no_longer_low",
170
+ "lawn_mower.docked": "lawn_mower.returned_to_dock",
171
+ "schedule.turned_on": "schedule.block_started",
172
+ "schedule.turned_off": "schedule.block_ended",
173
+ "timer.time_remaining": "timer.remaining_time_reached",
174
+ "update.update_became_available": "update.became_available",
175
+ "vacuum.docked": "vacuum.returned_to_dock",
176
+ }
177
+ _RENAMED_CONDITION_KEYS = {
178
+ "climate.target_temperature": "climate.is_target_temperature",
179
+ "climate.target_humidity": "climate.is_target_humidity",
180
+ }
181
+ # Trigger ``options.behavior`` values renamed in HA 2026.7: any→each,
182
+ # last→all (home-assistant/core#173259). The old values still load but raise
183
+ # an HA repair issue and face removal. Condition ``options.behavior`` keeps
184
+ # ``any``/``all`` — conditions are never flagged.
185
+ _DEPRECATED_TRIGGER_BEHAVIOR = {"any": "each", "last": "all"}
186
+
165
187
 
166
188
  # ---------------------------------------------------------------------------
167
189
  # Public API
@@ -318,6 +340,23 @@ def _check_condition_templates(
318
340
  # Shorthand template condition
319
341
  _check_template_string(cond, warnings, skill_prefix, "condition")
320
342
  elif isinstance(cond, dict):
343
+ # 2026.7 renamed purpose-specific condition keys — old keys no
344
+ # longer load.
345
+ condition_key = cond.get("condition")
346
+ renamed_condition = (
347
+ _RENAMED_CONDITION_KEYS.get(condition_key)
348
+ if isinstance(condition_key, str)
349
+ else None
350
+ )
351
+ if renamed_condition:
352
+ _emit(
353
+ warnings,
354
+ f"Condition key `{condition_key}` was renamed to "
355
+ f"`{renamed_condition}` in HA 2026.7 and the old key no "
356
+ f"longer loads — use `condition: {renamed_condition}`.",
357
+ skill_prefix,
358
+ "automation-patterns.md#native-conditions",
359
+ )
321
360
  if cond.get("condition") == "template":
322
361
  vt = cond.get("value_template", "")
323
362
  if isinstance(vt, str):
@@ -655,12 +694,44 @@ def _check_triggers(
655
694
 
656
695
  platform = trigger.get("platform", trigger.get("trigger", ""))
657
696
 
697
+ # 2026.7 renamed purpose-specific trigger keys — old keys no longer load.
698
+ renamed_trigger = (
699
+ _RENAMED_TRIGGER_KEYS.get(platform) if isinstance(platform, str) else None
700
+ )
701
+ if renamed_trigger:
702
+ _emit(
703
+ warnings,
704
+ f"Trigger key `{platform}` was renamed to `{renamed_trigger}` "
705
+ "in HA 2026.7 and the old key no longer loads — use "
706
+ f"`trigger: {renamed_trigger}`.",
707
+ skill_prefix,
708
+ "automation-patterns.md#trigger-types",
709
+ )
710
+
711
+ # 2026.7 renamed trigger `options.behavior` values (any→each, last→all).
712
+ options = trigger.get("options")
713
+ if isinstance(options, dict):
714
+ behavior = options.get("behavior")
715
+ if isinstance(behavior, str) and behavior in _DEPRECATED_TRIGGER_BEHAVIOR:
716
+ _emit(
717
+ warnings,
718
+ f"Trigger `options.behavior: {behavior}` was renamed to "
719
+ f"`{_DEPRECATED_TRIGGER_BEHAVIOR[behavior]}` in HA 2026.7 — "
720
+ "the old value still loads but raises a repair issue and "
721
+ "will be removed. Valid trigger values: `each`, `first`, "
722
+ "`all` (conditions keep `any`/`all`).",
723
+ skill_prefix,
724
+ "automation-patterns.md#trigger-types",
725
+ )
726
+
658
727
  # Device trigger → prefer entity_id-based triggers
659
728
  if platform == "device":
660
729
  _emit(
661
730
  warnings,
662
- "Trigger uses `device` platform with `device_id` — prefer "
663
- "`state` or `event` trigger with `entity_id` when possible "
731
+ "Trigger uses `device` platform with `device_id` — prefer a "
732
+ "purpose-specific trigger (`<domain>.<name>` with a `target:` "
733
+ "of entities/areas/floors/labels, HA 2026.7+) or a `state`/"
734
+ "`event` trigger with `entity_id` when possible "
664
735
  "(device_id breaks on re-add).",
665
736
  skill_prefix,
666
737
  "device-control.md#entity-id-vs-device-id",
@@ -440,6 +440,8 @@ class AutomationConfigTools:
440
440
  Field(
441
441
  description="Complete automation configuration with required fields: 'alias', 'triggers', 'actions'. "
442
442
  "Optional: 'description', 'conditions', 'mode', 'max', 'initial_state', 'variables'. "
443
+ "Purpose-specific triggers/conditions (HA 2026.7+ default: 'trigger': '<domain>.<name>' "
444
+ "with 'target'/'options') are valid config. "
443
445
  "Mutually exclusive with python_transform.",
444
446
  default=None,
445
447
  ),
@@ -712,6 +712,8 @@ def _populate_trigger_info(
712
712
  )
713
713
  if "entity_id" in trigger_vars:
714
714
  result["trigger"]["entity_id"] = trigger_vars["entity_id"]
715
+ if "error" in trigger_step:
716
+ result["trigger"]["error"] = trigger_step["error"]
715
717
 
716
718
  if "trigger" not in result and "trigger" in trace:
717
719
  result["trigger"] = {"description": trace["trigger"]}
@@ -731,6 +733,11 @@ def _populate_condition_results(
731
733
  }
732
734
  if "timestamp" in cond:
733
735
  cond_result["timestamp"] = cond["timestamp"]
736
+ # HA 2026.7+ always records template errors on the failing step
737
+ # (core #172917) — dropping this field would hide the reason a
738
+ # condition evaluated to None.
739
+ if "error" in cond:
740
+ cond_result["error"] = cond["error"]
734
741
  condition_results.append(cond_result)
735
742
  result["condition_results"] = condition_results
736
743
 
@@ -15,6 +15,10 @@ 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 (
19
+ HomeAssistantAuthError,
20
+ HomeAssistantConnectionError,
21
+ )
18
22
  from ..errors import ErrorCode, create_error_response
19
23
  from .helpers import (
20
24
  exception_to_structured_error,
@@ -22,9 +26,16 @@ from .helpers import (
22
26
  raise_tool_error,
23
27
  register_tool_methods,
24
28
  )
29
+ from .util_helpers import JSON_STRING_COERCION
25
30
 
26
31
  logger = logging.getLogger(__name__)
27
32
 
33
+ # Batch-install category split, mirroring the HA 2026.7 Updates page: the
34
+ # "Update all" button never covers core/OS/supervisor — those are applied
35
+ # individually, on purpose.
36
+ _INSTALL_ALL_CATEGORIES = ("addons", "hacs", "devices", "other")
37
+ _PROTECTED_UPDATE_CATEGORIES = ("core", "os", "supervisor")
38
+
28
39
  _GITHUB_CORE_RELEASE_URL = (
29
40
  "https://api.github.com/repos/home-assistant/core/releases/tags/{version}"
30
41
  )
@@ -654,112 +665,299 @@ class UpdateTools:
654
665
 
655
666
  return None, None
656
667
 
668
+ async def _resolve_update_targets(
669
+ self,
670
+ action: str,
671
+ entity_ids: list[str] | str | None,
672
+ categories: list[str] | str | None,
673
+ ) -> list[str]:
674
+ """Validate parameters and resolve the update entity_ids to act on."""
675
+ ids = [entity_ids] if isinstance(entity_ids, str) else list(entity_ids or [])
676
+ cats = [categories] if isinstance(categories, str) else list(categories or [])
677
+
678
+ if action != "install" and (not ids or cats):
679
+ raise_tool_error(
680
+ create_error_response(
681
+ ErrorCode.VALIDATION_INVALID_PARAMETER,
682
+ f"'{action}' requires entity_ids; categories is install-only.",
683
+ )
684
+ )
685
+ if action == "install" and bool(ids) == bool(cats):
686
+ raise_tool_error(
687
+ create_error_response(
688
+ ErrorCode.VALIDATION_INVALID_PARAMETER,
689
+ "'install' requires exactly one of entity_ids or categories.",
690
+ )
691
+ )
692
+
693
+ invalid = [e for e in ids if not e.startswith("update.")]
694
+ if invalid:
695
+ raise_tool_error(
696
+ create_error_response(
697
+ ErrorCode.VALIDATION_INVALID_PARAMETER,
698
+ "entity_ids must be 'update.' entities.",
699
+ context={"invalid_entity_ids": invalid},
700
+ )
701
+ )
702
+
703
+ if not cats:
704
+ return list(dict.fromkeys(ids))
705
+
706
+ protected = sorted(set(cats) & set(_PROTECTED_UPDATE_CATEGORIES))
707
+ unknown = sorted(
708
+ set(cats) - set(_INSTALL_ALL_CATEGORIES) - set(_PROTECTED_UPDATE_CATEGORIES)
709
+ )
710
+ if protected or unknown:
711
+ problems = []
712
+ if protected:
713
+ problems.append(
714
+ f"categories {protected} are excluded from batch install by "
715
+ "design (mirrors the HA Updates page, which never bundles "
716
+ "core/OS/supervisor into 'Update all') — target those "
717
+ "individually via entity_ids"
718
+ )
719
+ if unknown:
720
+ problems.append(f"unknown categories {unknown}")
721
+ raise_tool_error(
722
+ create_error_response(
723
+ ErrorCode.VALIDATION_INVALID_PARAMETER,
724
+ "; ".join(problems),
725
+ context={"allowed_categories": list(_INSTALL_ALL_CATEGORIES)},
726
+ )
727
+ )
728
+
729
+ listing = await self._list_updates(include_skipped=False)
730
+ listed_categories = listing.get("categories", {})
731
+ return [
732
+ update["entity_id"]
733
+ for cat in dict.fromkeys(cats)
734
+ for update in listed_categories.get(cat, [])
735
+ if update.get("can_install")
736
+ ]
737
+
657
738
  @tool(
658
- name="ha_get_updates",
739
+ name="ha_manage_updates",
659
740
  tags={"System"},
660
741
  annotations={
661
- "idempotentHint": True,
742
+ "destructiveHint": True,
662
743
  "openWorldHint": True,
663
- "readOnlyHint": True,
664
- "title": "Get Updates",
744
+ "title": "Manage Updates",
665
745
  },
666
746
  )
667
747
  @log_tool_usage
668
- async def ha_get_updates(
748
+ async def ha_manage_updates(
669
749
  self,
670
- entity_id: Annotated[
671
- str | None,
750
+ action: Annotated[
751
+ str,
752
+ Field(
753
+ description="'list' (all pending updates, default), 'get' "
754
+ "(details/release notes for one update), 'install' (apply "
755
+ "pending updates), 'skip' (hide the offered version), or "
756
+ "'clear_skipped' (re-offer a skipped version).",
757
+ default="list",
758
+ ),
759
+ ] = "list",
760
+ entity_ids: Annotated[
761
+ list[str] | str | None,
762
+ JSON_STRING_COERCION,
672
763
  Field(
673
- description="Update entity ID to get details for (e.g., 'update.home_assistant_core_update'). "
674
- "If omitted, lists all available updates.",
764
+ description="Update entity_id(s) to act on. 'get' takes exactly "
765
+ "one; skip/clear_skipped require at least one; for install, "
766
+ "mutually exclusive with categories.",
767
+ default=None,
768
+ ),
769
+ ] = None,
770
+ categories: Annotated[
771
+ list[str] | str | None,
772
+ JSON_STRING_COERCION,
773
+ Field(
774
+ description="For install: apply every pending update in these "
775
+ "categories ('addons', 'hacs', 'devices', 'other'). Mirrors the "
776
+ "HA 2026.7 'Update all' button: core/os/supervisor are excluded "
777
+ "by design (target those individually via entity_ids) and "
778
+ "skipped updates are never included.",
675
779
  default=None,
676
780
  ),
677
781
  ] = None,
678
782
  include_skipped: Annotated[
679
783
  bool,
680
784
  Field(
681
- description="When listing all updates, include updates that have been skipped (default: False)",
785
+ description="For list: include updates that have been skipped "
786
+ "(default: False).",
682
787
  default=False,
683
788
  ),
684
789
  ] = False,
685
790
  include_release_notes: Annotated[
686
791
  bool,
687
792
  Field(
688
- description="When getting a Core update entity, fetch multi-version release notes "
689
- "and breaking changes for all versions between installed and latest (default: False). "
690
- "Adds breaking_changes, multi_version_release_notes, and installed_integrations to the response.",
793
+ description="For get on a Core update entity: fetch multi-version "
794
+ "release notes and breaking changes for all versions between "
795
+ "installed and latest (default: False). Adds breaking_changes, "
796
+ "multi_version_release_notes, and installed_integrations to the "
797
+ "response.",
798
+ default=False,
799
+ ),
800
+ ] = False,
801
+ backup: Annotated[
802
+ bool,
803
+ Field(
804
+ description="For install: create a backup before installing where "
805
+ "the update entity supports it (apps/add-ons). Default: False.",
691
806
  default=False,
692
807
  ),
693
808
  ] = False,
694
809
  ) -> dict[str, Any]:
695
- """
696
- Get update information -- list all updates or get details for a specific one.
697
-
698
- Without an entity_id: Lists all available updates across the system including
699
- Home Assistant Core, add-ons, device firmware, HACS, and OS updates.
810
+ """Manage Home Assistant updates -- list, read details, batch install, skip, or un-skip.
700
811
 
701
- With an entity_id: Returns detailed information about a specific update including
702
- version info, category, and release notes (if available).
812
+ Covers Core, OS, supervisor, apps (add-ons), device firmware, and HACS
813
+ update entities. In Read Only Mode the read actions ('list', 'get') stay
814
+ available; write actions are blocked.
703
815
 
704
- With include_release_notes=True (Core updates only): Also fetches HA release
705
- blog posts for every monthly version between installed and latest. Returns
706
- structured breaking changes and installed integration domains for cross-referencing.
816
+ Installs run asynchronously in Home Assistant and can take minutes:
817
+ 'install' returns once the service calls are accepted, with per-entity
818
+ results. Poll action='list' to watch in_progress until installed_version
819
+ reaches latest_version.
707
820
 
708
821
  EXAMPLES:
709
- - List all updates: ha_get_updates()
710
- - List including skipped: ha_get_updates(include_skipped=True)
711
- - Get specific update: ha_get_updates(entity_id="update.home_assistant_core_update")
712
- - Pre-update analysis: ha_get_updates(entity_id="update.home_assistant_core_update", include_release_notes=True)
713
-
714
- RETURNS (when listing):
715
- - updates_available: Count of available updates
716
- - updates: List of update entities with version info
717
- - categories: Updates grouped by category (core, addons, devices, hacs, os)
718
- - ha_mcp_update: This MCP server's own update status
719
- {current, latest, update_available} — so you can flag a newer ha-mcp
720
- release (from PyPI for pip/Docker, the Supervisor add-on store for the
721
- add-on). Present on all install types; omitted only for the unknown
722
- version and when HA_MCP_DISABLE_UPDATE_CHECK is set.
723
-
724
- RETURNS (when getting specific update):
725
- - Update details including installed/latest versions
726
- - Release notes (fetched from WebSocket API or GitHub)
727
- - Category and installation status
728
-
729
- RETURNS (with include_release_notes=True, Core only):
730
- - breaking_changes.entries[]: Each has integration, description, version
731
- - multi_version_release_notes[]: Full text per version {version, content, source_url}
732
- - installed_integrations: Your integration domains for cross-referencing
822
+ - List all updates: ha_manage_updates()
823
+ - Pre-update analysis: ha_manage_updates(action="get", entity_ids=["update.home_assistant_core_update"], include_release_notes=True)
824
+ - Update everything pending in a category: ha_manage_updates(action="install", categories=["addons", "hacs"])
825
+
826
+ RETURNS (action='list'): updates_available, updates, categories, and
827
+ ha_mcp_update -- this MCP server's own update status {current, latest,
828
+ update_available}, so a newer ha-mcp release can be flagged.
829
+
830
+ RETURNS (action='get'): update details, release notes; with
831
+ include_release_notes=True on Core also breaking_changes.entries[],
832
+ multi_version_release_notes[], and installed_integrations.
733
833
  """
734
834
  try:
735
- if entity_id is None:
835
+ if action == "list":
736
836
  return await self._list_updates(bool(include_skipped))
737
- else:
837
+
838
+ if action == "get":
839
+ ids = (
840
+ [entity_ids]
841
+ if isinstance(entity_ids, str)
842
+ else list(entity_ids or [])
843
+ )
844
+ if len(ids) != 1:
845
+ raise_tool_error(
846
+ create_error_response(
847
+ ErrorCode.VALIDATION_INVALID_PARAMETER,
848
+ "'get' requires exactly one entity in entity_ids.",
849
+ context={"entity_ids": ids},
850
+ )
851
+ )
738
852
  return await self._get_update_details(
739
- entity_id, bool(include_release_notes)
853
+ ids[0], bool(include_release_notes)
854
+ )
855
+
856
+ if action not in ("install", "skip", "clear_skipped"):
857
+ raise_tool_error(
858
+ create_error_response(
859
+ ErrorCode.VALIDATION_INVALID_PARAMETER,
860
+ f"Invalid action '{action}'. Must be 'list', 'get', "
861
+ "'install', 'skip', or 'clear_skipped'.",
862
+ context={"action": action},
863
+ )
864
+ )
865
+
866
+ targets = await self._resolve_update_targets(action, entity_ids, categories)
867
+
868
+ results: list[dict[str, Any]] = []
869
+ requested = len(targets)
870
+
871
+ # Explicit-id mode: verify the entities exist up front. HA service
872
+ # calls targeting nonexistent entities no-op silently, which would
873
+ # otherwise read as success.
874
+ if entity_ids:
875
+ states = await self._client.get_states()
876
+ known = {s.get("entity_id") for s in states if isinstance(s, dict)}
877
+ results.extend(
878
+ create_error_response(
879
+ ErrorCode.ENTITY_NOT_FOUND,
880
+ f"Update entity not found: {eid}",
881
+ context={"entity_id": eid},
882
+ suggestions=["Use ha_manage_updates() to list update entities"],
883
+ )
884
+ for eid in targets
885
+ if eid not in known
886
+ )
887
+ targets = [e for e in targets if e in known]
888
+
889
+ succeeded = 0
890
+ for eid in targets:
891
+ data: dict[str, Any] = {"entity_id": eid}
892
+ if action == "install" and backup:
893
+ data["backup"] = True
894
+ try:
895
+ await self._client.call_service("update", action, data)
896
+ results.append({"success": True, "entity_id": eid})
897
+ succeeded += 1
898
+ except (HomeAssistantConnectionError, HomeAssistantAuthError):
899
+ # Fatal transport/auth failure — the connection is gone,
900
+ # so remaining items would fail identically. Propagate to
901
+ # surface the root cause instead of N per-item errors.
902
+ raise
903
+ except Exception as e:
904
+ # Batch item failure — collect, don't raise.
905
+ results.append(
906
+ create_error_response(
907
+ ErrorCode.SERVICE_CALL_FAILED,
908
+ str(e),
909
+ context={"entity_id": eid},
910
+ )
911
+ )
912
+
913
+ failed = requested - succeeded
914
+ response: dict[str, Any] = {
915
+ # Aggregate convention (matches ha_bulk-style tools): True only
916
+ # when nothing failed — zero requested counts as success.
917
+ "success": failed == 0,
918
+ "action": action,
919
+ "requested": requested,
920
+ "succeeded": succeeded,
921
+ "failed": failed,
922
+ "results": results,
923
+ }
924
+ if not requested:
925
+ response["note"] = "No matching pending updates to act on."
926
+ elif action == "install" and succeeded:
927
+ response["note"] = (
928
+ "Installs run asynchronously in Home Assistant and can take "
929
+ "minutes; poll ha_manage_updates(action='list') to track "
930
+ "progress."
740
931
  )
932
+ return response
741
933
 
742
934
  except ToolError:
743
935
  raise
744
936
  except Exception as e:
745
937
  error_msg = str(e)
746
- if entity_id and ("404" in error_msg or "not found" in error_msg.lower()):
938
+ if (
939
+ action == "get"
940
+ and entity_ids
941
+ and ("404" in error_msg or "not found" in error_msg.lower())
942
+ ):
943
+ eid = entity_ids if isinstance(entity_ids, str) else entity_ids[0]
747
944
  raise_tool_error(
748
945
  create_error_response(
749
946
  ErrorCode.ENTITY_NOT_FOUND,
750
- f"Update entity not found: {entity_id}",
751
- context={"entity_id": entity_id},
947
+ f"Update entity not found: {eid}",
948
+ context={"entity_id": eid},
752
949
  suggestions=[
753
- "Use ha_get_updates() without entity_id to see all available updates"
950
+ "Use ha_manage_updates() without entity_ids to see "
951
+ "all available updates"
754
952
  ],
755
953
  )
756
954
  )
757
- logger.error(f"Failed to get updates: {e}")
955
+ logger.error(f"Failed to manage updates: {e}")
758
956
  exception_to_structured_error(
759
957
  e,
760
958
  suggestions=[
761
959
  "Check Home Assistant connection",
762
- "Verify API access permissions",
960
+ "Use ha_manage_updates() to inspect available updates",
763
961
  ],
764
962
  )
765
963
  return (
@@ -3,7 +3,7 @@
3
3
  An operator can sit on an old build without ever knowing. This does a
4
4
  fail-silent check, holds the result in memory for the process, and surfaces a
5
5
  newer release via a startup log banner and the ``ha_get_overview`` /
6
- ``ha_get_system_health`` / ``ha_get_updates`` tool fields.
6
+ ``ha_get_system_health`` / ``ha_manage_updates`` tool fields.
7
7
 
8
8
  The comparison reference depends on how ha-mcp is deployed, because the running
9
9
  version only means something against the matching source:
@@ -216,7 +216,7 @@ async def get_update_field() -> UpdateField | None:
216
216
  so it never blocks the event loop, and shapes the result for embedding under
217
217
  an ``ha_mcp_update`` key. Never raises — a hiccup yields None (the tool omits
218
218
  the field) and is logged at debug. Shared by every status tool that surfaces
219
- the notice (``ha_get_overview`` / ``ha_get_system_health`` / ``ha_get_updates``)
219
+ the notice (``ha_get_overview`` / ``ha_get_system_health`` / ``ha_manage_updates``)
220
220
  so the shaping and event-loop offload live in one place.
221
221
  """
222
222
  try:
@@ -176,7 +176,16 @@ DOMAIN_HANDLERS = {
176
176
  "valid_actions": [],
177
177
  "parameters": [],
178
178
  "quick_actions": [],
179
- "state_attributes": ["state", "latitude", "longitude"],
179
+ # in_zones (all containing zones, smallest first) and tracking_type
180
+ # ("connection"/"location") were added in HA 2026.7; latitude/longitude
181
+ # may be absent when a person/tracker is located by a presence scanner.
182
+ "state_attributes": [
183
+ "state",
184
+ "latitude",
185
+ "longitude",
186
+ "in_zones",
187
+ "tracking_type",
188
+ ],
180
189
  "read_only": True,
181
190
  "provides_location": True,
182
191
  },
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ha-mcp-dev
3
- Version: 7.9.0.dev759
3
+ Version: 7.9.0.dev760
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