ha-mcp-dev 7.9.0.dev764__tar.gz → 7.9.0.dev765__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 (158) hide show
  1. {ha_mcp_dev-7.9.0.dev764/src/ha_mcp_dev.egg-info → ha_mcp_dev-7.9.0.dev765}/PKG-INFO +1 -1
  2. {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev765}/pyproject.toml +1 -1
  3. {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev765}/src/ha_mcp/settings_ui/__init__.py +60 -0
  4. {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev765}/src/ha_mcp/settings_ui/settings.css +15 -0
  5. {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev765}/src/ha_mcp/settings_ui/settings.html +65 -0
  6. {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev765}/src/ha_mcp/settings_ui/settings.js +113 -0
  7. {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev765}/src/ha_mcp/tools/smart_search/_deep.py +5 -1
  8. {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev765}/src/ha_mcp/tools/smart_search/_entities.py +72 -24
  9. {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev765}/src/ha_mcp/tools/smart_search/_overview.py +44 -7
  10. {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev765}/src/ha_mcp/tools/tools_search.py +81 -23
  11. {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev765}/src/ha_mcp/tools/util_helpers.py +10 -0
  12. ha_mcp_dev-7.9.0.dev765/src/ha_mcp/visibility/__init__.py +0 -0
  13. ha_mcp_dev-7.9.0.dev765/src/ha_mcp/visibility/model.py +35 -0
  14. ha_mcp_dev-7.9.0.dev765/src/ha_mcp/visibility/persistence.py +42 -0
  15. ha_mcp_dev-7.9.0.dev765/src/ha_mcp/visibility/resolver.py +509 -0
  16. {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev765/src/ha_mcp_dev.egg-info}/PKG-INFO +1 -1
  17. {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev765}/src/ha_mcp_dev.egg-info/SOURCES.txt +4 -0
  18. {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev765}/LICENSE +0 -0
  19. {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev765}/MANIFEST.in +0 -0
  20. {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev765}/README.md +0 -0
  21. {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev765}/setup.cfg +0 -0
  22. {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev765}/src/ha_mcp/__init__.py +0 -0
  23. {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev765}/src/ha_mcp/__main__.py +0 -0
  24. {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev765}/src/ha_mcp/_pypi_marker +0 -0
  25. {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev765}/src/ha_mcp/_version.py +0 -0
  26. {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev765}/src/ha_mcp/auth/__init__.py +0 -0
  27. {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev765}/src/ha_mcp/auth/consent_form.py +0 -0
  28. {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev765}/src/ha_mcp/auth/provider.py +0 -0
  29. {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev765}/src/ha_mcp/backup_manager.py +0 -0
  30. {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev765}/src/ha_mcp/client/__init__.py +0 -0
  31. {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev765}/src/ha_mcp/client/rest_client.py +0 -0
  32. {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev765}/src/ha_mcp/client/supervisor_client.py +0 -0
  33. {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev765}/src/ha_mcp/client/websocket_client.py +0 -0
  34. {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev765}/src/ha_mcp/client/websocket_listener.py +0 -0
  35. {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev765}/src/ha_mcp/config.py +0 -0
  36. {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev765}/src/ha_mcp/dashboard_screenshot/__init__.py +0 -0
  37. {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev765}/src/ha_mcp/dashboard_screenshot/capture.py +0 -0
  38. {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev765}/src/ha_mcp/dashboard_screenshot/provision.py +0 -0
  39. {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev765}/src/ha_mcp/errors.py +0 -0
  40. {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev765}/src/ha_mcp/policy/__init__.py +0 -0
  41. {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev765}/src/ha_mcp/policy/approval_queue.py +0 -0
  42. {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev765}/src/ha_mcp/policy/evaluator.py +0 -0
  43. {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev765}/src/ha_mcp/policy/handlers.py +0 -0
  44. {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev765}/src/ha_mcp/policy/middleware.py +0 -0
  45. {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev765}/src/ha_mcp/policy/model.py +0 -0
  46. {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev765}/src/ha_mcp/policy/persistence.py +0 -0
  47. {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev765}/src/ha_mcp/policy/value_sources.py +0 -0
  48. {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev765}/src/ha_mcp/py.typed +0 -0
  49. {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev765}/src/ha_mcp/read_only.py +0 -0
  50. {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev765}/src/ha_mcp/resources/skills-vendor/.claude/settings.json +0 -0
  51. {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev765}/src/ha_mcp/resources/skills-vendor/.claude-plugin/marketplace.json +0 -0
  52. {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev765}/src/ha_mcp/resources/skills-vendor/.claude-plugin/plugin.json +0 -0
  53. {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev765}/src/ha_mcp/resources/skills-vendor/.github/ISSUE_TEMPLATE/skill-rca.md +0 -0
  54. {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev765}/src/ha_mcp/resources/skills-vendor/.github/pull_request_template.md +0 -0
  55. {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev765}/src/ha_mcp/resources/skills-vendor/AGENTS.md +0 -0
  56. {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev765}/src/ha_mcp/resources/skills-vendor/CLAUDE.md +0 -0
  57. {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev765}/src/ha_mcp/resources/skills-vendor/CONTRIBUTING.md +0 -0
  58. {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev765}/src/ha_mcp/resources/skills-vendor/LICENSE +0 -0
  59. {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev765}/src/ha_mcp/resources/skills-vendor/README.md +0 -0
  60. {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev765}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/SKILL.md +0 -0
  61. {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev765}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/evals/evals.json +0 -0
  62. {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev765}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/appdaemon.md +0 -0
  63. {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev765}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/automation-patterns.md +0 -0
  64. {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev765}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-cards.md +0 -0
  65. {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev765}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-guide.md +0 -0
  66. {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev765}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/device-control.md +0 -0
  67. {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev765}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/domain-docs.md +0 -0
  68. {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev765}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/examples.yaml +0 -0
  69. {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev765}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/helper-selection.md +0 -0
  70. {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev765}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/safe-refactoring.md +0 -0
  71. {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev765}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/scenes.md +0 -0
  72. {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev765}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/template-guidelines.md +0 -0
  73. {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev765}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/yaml-only-integrations.md +0 -0
  74. {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev765}/src/ha_mcp/server.py +0 -0
  75. {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev765}/src/ha_mcp/smoke_test.py +0 -0
  76. {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev765}/src/ha_mcp/stdio_settings_sidecar.py +0 -0
  77. {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev765}/src/ha_mcp/tools/__init__.py +0 -0
  78. {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev765}/src/ha_mcp/tools/auto_backup.py +0 -0
  79. {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev765}/src/ha_mcp/tools/backup.py +0 -0
  80. {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev765}/src/ha_mcp/tools/best_practice_checker.py +0 -0
  81. {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev765}/src/ha_mcp/tools/config_entry_flow.py +0 -0
  82. {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev765}/src/ha_mcp/tools/device_control.py +0 -0
  83. {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev765}/src/ha_mcp/tools/enhanced.py +0 -0
  84. {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev765}/src/ha_mcp/tools/helpers.py +0 -0
  85. {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev765}/src/ha_mcp/tools/radio/__init__.py +0 -0
  86. {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev765}/src/ha_mcp/tools/radio/base.py +0 -0
  87. {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev765}/src/ha_mcp/tools/radio/matter.py +0 -0
  88. {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev765}/src/ha_mcp/tools/radio/thread.py +0 -0
  89. {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev765}/src/ha_mcp/tools/radio/zigbee.py +0 -0
  90. {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev765}/src/ha_mcp/tools/radio/zwave.py +0 -0
  91. {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev765}/src/ha_mcp/tools/reference_validator.py +0 -0
  92. {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev765}/src/ha_mcp/tools/registry.py +0 -0
  93. {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev765}/src/ha_mcp/tools/smart_search/__init__.py +0 -0
  94. {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev765}/src/ha_mcp/tools/smart_search/_base.py +0 -0
  95. {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev765}/src/ha_mcp/tools/smart_search/_config.py +0 -0
  96. {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev765}/src/ha_mcp/tools/smart_search/_fetch.py +0 -0
  97. {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev765}/src/ha_mcp/tools/smart_search/_scenes.py +0 -0
  98. {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev765}/src/ha_mcp/tools/smart_search/_scoring.py +0 -0
  99. {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev765}/src/ha_mcp/tools/tool_search_hint_middleware.py +0 -0
  100. {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev765}/src/ha_mcp/tools/tools_addons.py +0 -0
  101. {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev765}/src/ha_mcp/tools/tools_areas.py +0 -0
  102. {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev765}/src/ha_mcp/tools/tools_blueprints.py +0 -0
  103. {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev765}/src/ha_mcp/tools/tools_bug_report.py +0 -0
  104. {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev765}/src/ha_mcp/tools/tools_calendar.py +0 -0
  105. {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev765}/src/ha_mcp/tools/tools_camera.py +0 -0
  106. {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev765}/src/ha_mcp/tools/tools_categories.py +0 -0
  107. {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev765}/src/ha_mcp/tools/tools_code.py +0 -0
  108. {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev765}/src/ha_mcp/tools/tools_config_automations.py +0 -0
  109. {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev765}/src/ha_mcp/tools/tools_config_dashboards.py +0 -0
  110. {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev765}/src/ha_mcp/tools/tools_config_helpers.py +0 -0
  111. {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev765}/src/ha_mcp/tools/tools_config_scenes.py +0 -0
  112. {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev765}/src/ha_mcp/tools/tools_config_scripts.py +0 -0
  113. {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev765}/src/ha_mcp/tools/tools_dashboard_screenshot.py +0 -0
  114. {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev765}/src/ha_mcp/tools/tools_energy.py +0 -0
  115. {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev765}/src/ha_mcp/tools/tools_entities.py +0 -0
  116. {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev765}/src/ha_mcp/tools/tools_filesystem.py +0 -0
  117. {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev765}/src/ha_mcp/tools/tools_groups.py +0 -0
  118. {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev765}/src/ha_mcp/tools/tools_hacs.py +0 -0
  119. {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev765}/src/ha_mcp/tools/tools_history.py +0 -0
  120. {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev765}/src/ha_mcp/tools/tools_integrations.py +0 -0
  121. {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev765}/src/ha_mcp/tools/tools_labels.py +0 -0
  122. {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev765}/src/ha_mcp/tools/tools_mcp_component.py +0 -0
  123. {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev765}/src/ha_mcp/tools/tools_radio.py +0 -0
  124. {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev765}/src/ha_mcp/tools/tools_registry.py +0 -0
  125. {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev765}/src/ha_mcp/tools/tools_resources.py +0 -0
  126. {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev765}/src/ha_mcp/tools/tools_service.py +0 -0
  127. {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev765}/src/ha_mcp/tools/tools_services.py +0 -0
  128. {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev765}/src/ha_mcp/tools/tools_system.py +0 -0
  129. {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev765}/src/ha_mcp/tools/tools_themes.py +0 -0
  130. {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev765}/src/ha_mcp/tools/tools_todo.py +0 -0
  131. {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev765}/src/ha_mcp/tools/tools_traces.py +0 -0
  132. {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev765}/src/ha_mcp/tools/tools_updates.py +0 -0
  133. {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev765}/src/ha_mcp/tools/tools_utility.py +0 -0
  134. {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev765}/src/ha_mcp/tools/tools_voice_assistant.py +0 -0
  135. {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev765}/src/ha_mcp/tools/tools_yaml_config.py +0 -0
  136. {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev765}/src/ha_mcp/tools/tools_zones.py +0 -0
  137. {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev765}/src/ha_mcp/tools/validation_middleware.py +0 -0
  138. {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev765}/src/ha_mcp/transforms/__init__.py +0 -0
  139. {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev765}/src/ha_mcp/transforms/categorized_search.py +0 -0
  140. {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev765}/src/ha_mcp/transforms/lite_docstrings.py +0 -0
  141. {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev765}/src/ha_mcp/update_check.py +0 -0
  142. {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev765}/src/ha_mcp/utils/__init__.py +0 -0
  143. {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev765}/src/ha_mcp/utils/config_hash.py +0 -0
  144. {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev765}/src/ha_mcp/utils/data_paths.py +0 -0
  145. {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev765}/src/ha_mcp/utils/domain_handlers.py +0 -0
  146. {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev765}/src/ha_mcp/utils/fuzzy_search.py +0 -0
  147. {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev765}/src/ha_mcp/utils/kill_signal_diagnostics.py +0 -0
  148. {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev765}/src/ha_mcp/utils/operation_manager.py +0 -0
  149. {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev765}/src/ha_mcp/utils/python_sandbox.py +0 -0
  150. {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev765}/src/ha_mcp/utils/skill_loader.py +0 -0
  151. {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev765}/src/ha_mcp/utils/usage_logger.py +0 -0
  152. {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev765}/src/ha_mcp_dev.egg-info/dependency_links.txt +0 -0
  153. {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev765}/src/ha_mcp_dev.egg-info/entry_points.txt +0 -0
  154. {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev765}/src/ha_mcp_dev.egg-info/requires.txt +0 -0
  155. {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev765}/src/ha_mcp_dev.egg-info/top_level.txt +0 -0
  156. {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev765}/tests/__init__.py +0 -0
  157. {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev765}/tests/test_constants.py +0 -0
  158. {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev765}/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.dev764
3
+ Version: 7.9.0.dev765
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.dev764"
7
+ version = "7.9.0.dev765"
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
@@ -796,6 +796,61 @@ def _build_stub_policy_handlers(*, data_dir: Path) -> dict[str, Any]:
796
796
  }
797
797
 
798
798
 
799
+ def _build_visibility_handlers(*, data_dir: Path) -> dict[str, Any]:
800
+ """Settings-UI handlers for the entity visibility filter config.
801
+
802
+ Serves the on-disk ``entity_visibility.json`` GET/PUT with the same
803
+ optimistic-concurrency (version) guard as the tool policy handlers. Pure
804
+ file I/O, so it is always available (no approval-queue dependency).
805
+ """
806
+ from pydantic import ValidationError
807
+
808
+ from ..visibility.model import VisibilityConfig
809
+ from ..visibility.persistence import (
810
+ load_visibility_config,
811
+ save_visibility_config,
812
+ )
813
+
814
+ async def get_config(_: Request) -> JSONResponse:
815
+ try:
816
+ return JSONResponse(
817
+ load_visibility_config(data_dir).model_dump(mode="json")
818
+ )
819
+ except ValueError as e:
820
+ # Surface corruption rather than crash the tab on a 500.
821
+ return JSONResponse(
822
+ {"error": str(e), "visibility_file_corrupt": True},
823
+ status_code=500,
824
+ )
825
+
826
+ async def put_config(request: Request) -> JSONResponse:
827
+ try:
828
+ new_config = VisibilityConfig.model_validate(await request.json())
829
+ except (ValidationError, ValueError) as e:
830
+ return JSONResponse({"error": str(e)}, status_code=400)
831
+ # Optimistic concurrency: reject if the on-disk version moved between
832
+ # this caller's GET and PUT.
833
+ current = load_visibility_config(data_dir)
834
+ if new_config.version != current.version:
835
+ return JSONResponse(
836
+ {
837
+ "error": (
838
+ "Visibility config version mismatch. Reload before saving."
839
+ ),
840
+ "current_version": current.version,
841
+ "current_config": current.model_dump(mode="json"),
842
+ },
843
+ status_code=409,
844
+ )
845
+ save_visibility_config(data_dir, new_config)
846
+ return JSONResponse({"saved": True, "version": new_config.version + 1})
847
+
848
+ return {
849
+ "visibility_get_config": get_config,
850
+ "visibility_put_config": put_config,
851
+ }
852
+
853
+
799
854
  class _SupervisorOptionsError(NamedTuple):
800
855
  """Discriminated failure shape for the supervisor options helpers.
801
856
 
@@ -3237,6 +3292,8 @@ def build_settings_handlers(
3237
3292
  else:
3238
3293
  handlers.update(_build_stub_policy_handlers(data_dir=get_data_dir()))
3239
3294
 
3295
+ handlers.update(_build_visibility_handlers(data_dir=get_data_dir()))
3296
+
3240
3297
  return handlers
3241
3298
 
3242
3299
 
@@ -3397,6 +3454,9 @@ def register_settings_routes(
3397
3454
  ("/api/policy/deny", ["POST"], "policy_post_deny"),
3398
3455
  ("/api/policy/tool-schema", ["GET"], "policy_get_tool_schema"),
3399
3456
  ("/api/policy/value-source", ["GET"], "policy_get_value_source"),
3457
+ # Entity visibility filter endpoints (issue #1728)
3458
+ ("/api/visibility/config", ["GET"], "visibility_get_config"),
3459
+ ("/api/visibility/config", ["PUT"], "visibility_put_config"),
3400
3460
  ]
3401
3461
 
3402
3462
  def _mount(prefix: str, *, guard: bool = False) -> None:
@@ -953,3 +953,18 @@
953
953
  /* Modal fills the small viewport instead of floating in a sliver. */
954
954
  .modal { max-height: 94vh; }
955
955
  }
956
+
957
+ /* Entity visibility filter tab (#1728) */
958
+ .visibility-field {
959
+ display: block; margin: 10px 0; color: var(--text-secondary); font-size: 0.85rem;
960
+ }
961
+ .visibility-field input[type="text"], .visibility-textarea {
962
+ display: block; width: 100%; margin-top: 4px; padding: 6px 8px;
963
+ background: var(--surface); color: var(--text); border: 1px solid var(--border);
964
+ border-radius: 6px; font: inherit;
965
+ }
966
+ .visibility-textarea { resize: vertical; }
967
+ .visibility-error {
968
+ background: var(--danger); color: #fff; padding: 8px 12px;
969
+ border-radius: 6px; margin-bottom: 8px; font-size: 0.85rem;
970
+ }
@@ -158,6 +158,7 @@
158
158
  <button class="tab" data-panel="server" role="tab" id="tab-server" aria-controls="panel-server" aria-selected="false" tabindex="-1">Server Settings</button>
159
159
  <button class="tab" data-panel="backups" role="tab" id="tab-backups" aria-controls="panel-backups" aria-selected="false" tabindex="-1">Backups</button>
160
160
  <button class="tab" data-panel="tool-security-policies" role="tab" id="tab-tool-security-policies" aria-controls="panel-tool-security-policies" aria-selected="false" tabindex="-1">Tool Security Policies</button>
161
+ <button class="tab" data-panel="entity-visibility" role="tab" id="tab-entity-visibility" aria-controls="panel-entity-visibility" aria-selected="false" tabindex="-1">Entity Visibility</button>
161
162
  <button class="tab" data-panel="accessibility" role="tab" id="tab-accessibility" aria-controls="panel-accessibility" aria-selected="false" tabindex="-1">Accessibility</button>
162
163
  </div>
163
164
  <div class="restart-notice" id="restartNotice">
@@ -403,6 +404,70 @@
403
404
  <button id="a11y-reset" class="restart-btn" type="button">Reset to defaults</button>
404
405
  </section>
405
406
  </div>
407
+ <div class="panel" id="panel-entity-visibility" role="tabpanel" aria-labelledby="tab-entity-visibility" tabindex="0">
408
+ <p class="tool-desc" style="margin-bottom:12px">
409
+ Hide low-value entities (auto-generated diagnostic or helper entities, or an
410
+ explicit list) from the collection read tools <code>ha_search</code> and
411
+ <code>ha_get_overview</code>, so search and overview results are less
412
+ cluttered. Off by default; changes apply on the next tool call.
413
+ </p>
414
+ <p class="a11y-contrast-warning" id="visibility-boundary-note">
415
+ Noise reduction, not access control: a hidden entity is still returned by a
416
+ direct read of its entity_id and still appears in automation, dashboard, and
417
+ template content. Do not rely on this as a security boundary.
418
+ </p>
419
+ <div id="visibility-load-error" class="visibility-error" role="alert" aria-live="assertive" style="display:none"></div>
420
+
421
+ <section class="a11y-section">
422
+ <label class="a11y-option"><input type="checkbox" id="visibility-enabled"> Enable entity visibility filter</label>
423
+ </section>
424
+
425
+ <section class="a11y-section">
426
+ <h2 class="a11y-section-title">Hide by category</h2>
427
+ <p class="a11y-section-help">Home Assistant tags auto-generated entities as diagnostic or config.</p>
428
+ <fieldset class="a11y-options">
429
+ <legend class="visually-hidden">Entity categories to hide</legend>
430
+ <label class="a11y-option"><input type="checkbox" id="visibility-cat-diagnostic"> Diagnostic</label>
431
+ <label class="a11y-option"><input type="checkbox" id="visibility-cat-config"> Config</label>
432
+ <label class="a11y-option"><input type="checkbox" id="visibility-exclude-hidden"> Also entities already hidden in Home Assistant</label>
433
+ </fieldset>
434
+ </section>
435
+
436
+ <section class="a11y-section">
437
+ <h2 class="a11y-section-title">Hide by area or label</h2>
438
+ <p class="a11y-section-help">Comma-separated area IDs or label IDs; entities in any listed area or label are hidden.</p>
439
+ <label class="visibility-field">Areas <input type="text" id="visibility-areas" placeholder="garage, basement"></label>
440
+ <label class="visibility-field">Labels <input type="text" id="visibility-labels" placeholder="noise, archived"></label>
441
+ </section>
442
+
443
+ <section class="a11y-section">
444
+ <h2 class="a11y-section-title">Hide specific entities</h2>
445
+ <p class="a11y-section-help">One entity_id per line. These stay hidden even without an entity-registry entry.</p>
446
+ <label class="visibility-field visually-hidden" for="visibility-deny">Denied entity IDs</label>
447
+ <textarea id="visibility-deny" class="visibility-textarea" rows="4" placeholder="sensor.example_diagnostic&#10;light.unused"></textarea>
448
+ </section>
449
+
450
+ <section class="a11y-section">
451
+ <h2 class="a11y-section-title">Restrict to an allowlist</h2>
452
+ <p class="a11y-section-help">Advanced: when any allowlist field is set the filter inverts – only entities matching an allowlist stay visible, everything else is hidden (including entities added later). Leave all three empty to disable.</p>
453
+ <label class="visibility-field">Allowed areas <input type="text" id="visibility-allow-areas" placeholder="living_room, kitchen"></label>
454
+ <label class="visibility-field">Allowed labels <input type="text" id="visibility-allow-labels" placeholder="voice, main_floor"></label>
455
+ <p class="a11y-section-help">Allowed entity IDs, one per line.</p>
456
+ <label class="visibility-field visually-hidden" for="visibility-allow-entities">Allowed entity IDs</label>
457
+ <textarea id="visibility-allow-entities" class="visibility-textarea" rows="3" placeholder="light.living_room&#10;switch.desk"></textarea>
458
+ </section>
459
+
460
+ <section class="a11y-section">
461
+ <h2 class="a11y-section-title">Respect Assist exposure</h2>
462
+ <p class="a11y-section-help">Hide entities not exposed to Home Assistant's Assist (&ldquo;conversation&rdquo;) voice assistant, mirroring the Settings &gt; Voice assistants &gt; Expose list. Adds two websocket reads per search while enabled.</p>
463
+ <label class="a11y-option"><input type="checkbox" id="visibility-respect-assist"> Only show entities exposed to Assist</label>
464
+ </section>
465
+
466
+ <section class="a11y-section">
467
+ <button id="visibility-save-btn" class="restart-btn" type="button">Save</button>
468
+ <span id="visibility-save-status" class="status" role="status" aria-live="polite"></span>
469
+ </section>
470
+ </div>
406
471
  </main>
407
472
  <div class="modal-backdrop" id="modalBackdrop">
408
473
  <div class="modal" role="dialog" aria-modal="true" aria-labelledby="modalTitle" tabindex="-1">
@@ -2875,6 +2875,7 @@ function activateTab(target, opts) {
2875
2875
  );
2876
2876
  if (target === 'backups') { loadBackupConfig(); loadBackups(); }
2877
2877
  if (target === 'tool-security-policies') { policyLoadConfig(); policyLoadPending(); }
2878
+ if (target === 'entity-visibility') { visibilityLoadConfig(); }
2878
2879
  if (target === 'tools') {
2879
2880
  // Refresh gated-toggle + read-only state in case the user changed
2880
2881
  // them from another tab while it was active.
@@ -3582,3 +3583,115 @@ loadFsCustomPaths();
3582
3583
  });
3583
3584
  }
3584
3585
  })();
3586
+
3587
+ // --- Entity visibility filter tab (#1728) ---
3588
+ let visibilityVersion = 1;
3589
+
3590
+ function _visibilityShowLoadError(msg) {
3591
+ const el = document.getElementById('visibility-load-error');
3592
+ if (!el) return;
3593
+ el.style.display = msg ? '' : 'none';
3594
+ el.textContent = msg || '';
3595
+ }
3596
+
3597
+ function _visibilityParseList(value, sep) {
3598
+ return value.split(sep).map(s => s.trim()).filter(Boolean);
3599
+ }
3600
+
3601
+ async function visibilityLoadConfig() {
3602
+ _visibilityShowLoadError('');
3603
+ let resp;
3604
+ try {
3605
+ resp = await fetch('./api/visibility/config');
3606
+ } catch (e) {
3607
+ _visibilityShowLoadError('Could not reach the server: ' + e.message);
3608
+ return;
3609
+ }
3610
+ if (!resp.ok) {
3611
+ let detail = 'HTTP ' + resp.status;
3612
+ try {
3613
+ const body = await resp.json();
3614
+ if (body && body.error) detail = body.error;
3615
+ if (body && body.visibility_file_corrupt) {
3616
+ detail += ' (entity_visibility.json appears corrupt; edit or delete it on the App (add-on) /data volume)';
3617
+ }
3618
+ } catch (_e) { /* keep the HTTP-status fallback */ }
3619
+ _visibilityShowLoadError('Failed to load visibility config: ' + detail);
3620
+ return;
3621
+ }
3622
+ const c = await resp.json();
3623
+ visibilityVersion = c.version ?? 1;
3624
+ const cats = c.exclude_categories || [];
3625
+ document.getElementById('visibility-enabled').checked = !!c.enabled;
3626
+ document.getElementById('visibility-cat-diagnostic').checked = cats.includes('diagnostic');
3627
+ document.getElementById('visibility-cat-config').checked = cats.includes('config');
3628
+ document.getElementById('visibility-exclude-hidden').checked = !!c.exclude_hidden;
3629
+ document.getElementById('visibility-areas').value = (c.exclude_areas || []).join(', ');
3630
+ document.getElementById('visibility-labels').value = (c.exclude_labels || []).join(', ');
3631
+ document.getElementById('visibility-deny').value = (c.deny_entity_ids || []).join('\n');
3632
+ document.getElementById('visibility-allow-areas').value = (c.allow_areas || []).join(', ');
3633
+ document.getElementById('visibility-allow-labels').value = (c.allow_labels || []).join(', ');
3634
+ document.getElementById('visibility-allow-entities').value = (c.allow_entity_ids || []).join('\n');
3635
+ document.getElementById('visibility-respect-assist').checked = !!c.respect_assist_exposure;
3636
+ }
3637
+
3638
+ async function visibilitySaveConfig() {
3639
+ const statusEl = document.getElementById('visibility-save-status');
3640
+ const cats = [];
3641
+ if (document.getElementById('visibility-cat-diagnostic').checked) cats.push('diagnostic');
3642
+ if (document.getElementById('visibility-cat-config').checked) cats.push('config');
3643
+ const config = {
3644
+ version: visibilityVersion,
3645
+ enabled: document.getElementById('visibility-enabled').checked,
3646
+ exclude_categories: cats,
3647
+ exclude_hidden: document.getElementById('visibility-exclude-hidden').checked,
3648
+ deny_entity_ids: _visibilityParseList(document.getElementById('visibility-deny').value, '\n'),
3649
+ exclude_areas: _visibilityParseList(document.getElementById('visibility-areas').value, ','),
3650
+ exclude_labels: _visibilityParseList(document.getElementById('visibility-labels').value, ','),
3651
+ allow_areas: _visibilityParseList(document.getElementById('visibility-allow-areas').value, ','),
3652
+ allow_labels: _visibilityParseList(document.getElementById('visibility-allow-labels').value, ','),
3653
+ allow_entity_ids: _visibilityParseList(document.getElementById('visibility-allow-entities').value, '\n'),
3654
+ respect_assist_exposure: document.getElementById('visibility-respect-assist').checked,
3655
+ };
3656
+ setStatusAlert(statusEl, false);
3657
+ statusEl.textContent = 'Saving...';
3658
+ let resp;
3659
+ try {
3660
+ resp = await fetch('./api/visibility/config', {
3661
+ method: 'PUT',
3662
+ headers: { 'Content-Type': 'application/json' },
3663
+ body: JSON.stringify(config),
3664
+ });
3665
+ } catch (e) {
3666
+ setStatusAlert(statusEl, true);
3667
+ statusEl.textContent = 'Save failed: ' + e.message;
3668
+ return;
3669
+ }
3670
+ if (resp.status === 409) {
3671
+ // Do NOT reload the config here: that would overwrite the user's unsaved
3672
+ // edits (there would be nothing left to "re-apply"). Keep the edits in the
3673
+ // form, surface the conflict, and let the user reload deliberately — mirrors
3674
+ // the policy tab's optimistic-lock message.
3675
+ setStatusAlert(statusEl, true);
3676
+ statusEl.textContent =
3677
+ 'Config was changed in another tab or session. Reload the page to see the '
3678
+ + 'latest, then re-apply your changes.';
3679
+ return;
3680
+ }
3681
+ if (!resp.ok) {
3682
+ let detail = 'HTTP ' + resp.status;
3683
+ try { const b = await resp.json(); if (b && b.error) detail = b.error; } catch (_e) { /* fallback */ }
3684
+ setStatusAlert(statusEl, true);
3685
+ statusEl.textContent = 'Save failed: ' + detail;
3686
+ return;
3687
+ }
3688
+ const body = await resp.json();
3689
+ visibilityVersion = body.version ?? (visibilityVersion + 1);
3690
+ setStatusAlert(statusEl, false);
3691
+ statusEl.textContent = 'Saved.';
3692
+ }
3693
+
3694
+ (function wireVisibilitySave() {
3695
+ const btn = document.getElementById('visibility-save-btn');
3696
+ if (btn) btn.addEventListener('click', visibilitySaveConfig);
3697
+ })();
@@ -92,7 +92,11 @@ class DeepSearchMixin(SceneSearchMixin):
92
92
  ctx,
93
93
  progress=phase_done,
94
94
  total=total_phases,
95
- message=f"fetched {len(all_entities)} entity states",
95
+ # Config-reference search scans the full state machine by design
96
+ # (the config body is unfiltered), so this count is the scanned
97
+ # universe, not the visible one - phrase it so an operator with
98
+ # the visibility filter on doesn't read it as "the filter is off".
99
+ message=f"scanned {len(all_entities)} entity states for config references",
96
100
  )
97
101
 
98
102
  # Pre-resolve unique_ids from cached entity states to avoid redundant API calls
@@ -5,7 +5,9 @@ import logging
5
5
  from typing import Any
6
6
 
7
7
  from ...utils.fuzzy_search import calculate_partial_ratio
8
+ from ...visibility.resolver import load_hidden_set
8
9
  from ..helpers import exception_to_structured_error
10
+ from ..util_helpers import merge_visibility_warnings
9
11
  from ._base import _SearchBase
10
12
 
11
13
  logger = logging.getLogger(__name__)
@@ -47,7 +49,9 @@ class EntitySearchMixin(_SearchBase):
47
49
  if domain_filter:
48
50
  domain_filter = domain_filter.strip().lower()
49
51
 
50
- entities = await self._fetch_search_entities(domain_filter, include_hidden)
52
+ entities, visibility_warnings = await self._fetch_search_entities(
53
+ domain_filter, include_hidden
54
+ )
51
55
 
52
56
  # Perform fuzzy search - returns (paginated_results, total_count)
53
57
  matches, total_matches = self.fuzzy_searcher.search_entities(
@@ -73,7 +77,7 @@ class EntitySearchMixin(_SearchBase):
73
77
  entities, query
74
78
  )
75
79
 
76
- return response
80
+ return merge_visibility_warnings(response, visibility_warnings)
77
81
 
78
82
  except Exception as e:
79
83
  logger.error(f"Error in smart_entity_search: {e}")
@@ -115,6 +119,7 @@ class EntitySearchMixin(_SearchBase):
115
119
  entities: list[dict[str, Any]],
116
120
  registry_slim: dict[str, dict[str, Any]],
117
121
  include_hidden: bool,
122
+ visibility_hidden: set[str],
118
123
  ) -> tuple[list[str], list[dict[str, Any]]]:
119
124
  """Drop UI-hidden entities (unless include_hidden); collect survivor ids.
120
125
 
@@ -127,6 +132,8 @@ class EntitySearchMixin(_SearchBase):
127
132
  eid = entity.get("entity_id", "")
128
133
  if not eid:
129
134
  continue
135
+ if eid in visibility_hidden:
136
+ continue
130
137
  slim = registry_slim.get(eid, {})
131
138
  hidden_by = slim.get("hidden_by")
132
139
  if hidden_by is not None and not include_hidden:
@@ -175,7 +182,7 @@ class EntitySearchMixin(_SearchBase):
175
182
 
176
183
  async def _fetch_search_entities(
177
184
  self, domain_filter: str | None, include_hidden: bool
178
- ) -> list[dict[str, Any]]:
185
+ ) -> tuple[list[dict[str, Any]], list[str]]:
179
186
  """Fetch + enrich the entity set fed into the fuzzy search layer.
180
187
 
181
188
  Fetches states + the slim entity-registry list in parallel (the slim
@@ -187,6 +194,7 @@ class EntitySearchMixin(_SearchBase):
187
194
  results = await asyncio.gather(
188
195
  self.client.get_states(),
189
196
  self.client.send_websocket_message({"type": "config/entity_registry/list"}),
197
+ self.client.send_websocket_message({"type": "config/device_registry/list"}),
190
198
  return_exceptions=True,
191
199
  )
192
200
  # States-fetch failure is fatal — auth/connection errors must propagate
@@ -194,15 +202,26 @@ class EntitySearchMixin(_SearchBase):
194
202
  # with success=True.
195
203
  if isinstance(results[0], BaseException):
196
204
  raise results[0]
197
- # CancelledError on the registry task must propagate too; gather captures
205
+ # CancelledError on the registry tasks must propagate too; gather captures
198
206
  # it like any other exception when return_exceptions=True.
199
207
  if isinstance(results[1], asyncio.CancelledError):
200
208
  raise results[1]
209
+ if isinstance(results[2], asyncio.CancelledError):
210
+ raise results[2]
201
211
  entities = results[0]
202
212
 
213
+ # Opt-in visibility filter. results[1] is the unprojected registry, so
214
+ # entity_category/hidden_by/area_id/labels are present (the slim map
215
+ # below drops them); results[2] is the device registry, which lets the
216
+ # area/label dimensions match a device-bound entity by its device's
217
+ # area/labels. Fails open; do NOT wrap in try/except or the failure mode
218
+ # inverts.
219
+ visibility_hidden, visibility_warnings = await load_hidden_set(
220
+ results[1], results[0], self.client, results[2]
221
+ )
203
222
  registry_slim = self._build_registry_slim(results[1])
204
223
  survivor_ids, survivor_states = self._filter_hidden_entities(
205
- entities, registry_slim, include_hidden
224
+ entities, registry_slim, include_hidden, visibility_hidden
206
225
  )
207
226
  aliases_map = await self._fetch_entity_aliases(survivor_ids)
208
227
 
@@ -226,7 +245,7 @@ class EntitySearchMixin(_SearchBase):
226
245
  for e in enriched
227
246
  if e.get("entity_id", "").startswith(f"{domain_filter}.")
228
247
  ]
229
- return enriched
248
+ return enriched, visibility_warnings
230
249
 
231
250
  @staticmethod
232
251
  def _format_entity_matches(
@@ -293,7 +312,27 @@ class EntitySearchMixin(_SearchBase):
293
312
  return_exceptions=True,
294
313
  )
295
314
 
296
- entities = results[0] if not isinstance(results[0], Exception) else []
315
+ # States are mandatory surface connection/auth errors instead of a
316
+ # bogus empty area result with success=True (mirrors
317
+ # _fetch_search_entities). The registry results below still fail open.
318
+ if isinstance(results[0], BaseException):
319
+ raise results[0]
320
+ entities = results[0]
321
+ # A registry sub-task that was individually cancelled must propagate,
322
+ # not be silently degraded to an empty registry by the parsers below
323
+ # (mirrors _fetch_search_entities). Non-cancellation registry errors
324
+ # still fail open.
325
+ for reg_result in results[1:]:
326
+ if isinstance(reg_result, asyncio.CancelledError):
327
+ raise reg_result
328
+ # Opt-in visibility filter. results[2] is the unprojected entity
329
+ # registry, so entity_category/hidden_by/area_id/labels are present;
330
+ # results[3] is the device registry, so the area/label dimensions
331
+ # match a device-bound entity by its device's area/labels.
332
+ # Fails open (empty set on any error / non-dict payload).
333
+ visibility_hidden, visibility_warnings = await load_hidden_set(
334
+ results[2], results[0], self.client, results[3]
335
+ )
297
336
  area_registry = self._parse_area_registry(results[1])
298
337
  entity_reg_map = self._parse_entity_reg_map(results[2])
299
338
  device_area_map = self._parse_device_area_map(results[3])
@@ -302,19 +341,22 @@ class EntitySearchMixin(_SearchBase):
302
341
  matched_area_ids = self._match_area_ids(area_registry, area_query_lower)
303
342
 
304
343
  if not matched_area_ids:
305
- return {
306
- "area_query": area_query,
307
- "total_areas_found": 0,
308
- "total_entities": 0,
309
- "areas": {},
310
- "available_areas": [
311
- {"area_id": aid, "name": ainfo.get("name", aid)}
312
- for aid, ainfo in area_registry.items()
313
- ],
314
- }
344
+ return merge_visibility_warnings(
345
+ {
346
+ "area_query": area_query,
347
+ "total_areas_found": 0,
348
+ "total_entities": 0,
349
+ "areas": {},
350
+ "available_areas": [
351
+ {"area_id": aid, "name": ainfo.get("name", aid)}
352
+ for aid, ainfo in area_registry.items()
353
+ ],
354
+ },
355
+ visibility_warnings,
356
+ )
315
357
 
316
358
  entity_area_resolved, hidden_entity_ids = self._resolve_entity_areas(
317
- entity_reg_map, device_area_map, include_hidden
359
+ entity_reg_map, device_area_map, include_hidden, visibility_hidden
318
360
  )
319
361
  state_map = self._build_state_map(entities)
320
362
  formatted_areas, total_entities = self._format_area_entities(
@@ -326,12 +368,15 @@ class EntitySearchMixin(_SearchBase):
326
368
  group_by_domain,
327
369
  )
328
370
 
329
- return {
330
- "area_query": area_query,
331
- "total_areas_found": len(formatted_areas),
332
- "total_entities": total_entities,
333
- "areas": formatted_areas,
334
- }
371
+ return merge_visibility_warnings(
372
+ {
373
+ "area_query": area_query,
374
+ "total_areas_found": len(formatted_areas),
375
+ "total_entities": total_entities,
376
+ "areas": formatted_areas,
377
+ },
378
+ visibility_warnings,
379
+ )
335
380
 
336
381
  except Exception as e:
337
382
  logger.error(f"Error in get_entities_by_area: {e}")
@@ -438,6 +483,7 @@ class EntitySearchMixin(_SearchBase):
438
483
  entity_reg_map: dict[str, dict[str, str | None]],
439
484
  device_area_map: dict[str, str | None],
440
485
  include_hidden: bool,
486
+ visibility_hidden: set[str],
441
487
  ) -> tuple[dict[str, str], set[str]]:
442
488
  """Map entity_id -> resolved area_id (entity area > device area).
443
489
 
@@ -449,6 +495,8 @@ class EntitySearchMixin(_SearchBase):
449
495
  entity_area_resolved: dict[str, str] = {}
450
496
  hidden_entity_ids: set[str] = set()
451
497
  for entity_id, reg_info in entity_reg_map.items():
498
+ if entity_id in visibility_hidden:
499
+ continue
452
500
  is_hidden = reg_info.get("hidden_by") is not None
453
501
  if is_hidden and not include_hidden:
454
502
  continue
@@ -5,6 +5,7 @@ import logging
5
5
  import random
6
6
  from typing import Any
7
7
 
8
+ from ...visibility.resolver import load_hidden_set
8
9
  from ..helpers import exception_to_structured_error
9
10
  from ._base import _SearchBase
10
11
  from ._config import _simplify_states_summary
@@ -64,11 +65,38 @@ class SystemOverviewMixin(_SearchBase):
64
65
  )
65
66
 
66
67
  # Entities are mandatory — surface connection/auth errors immediately.
67
- if isinstance(results[0], Exception):
68
+ # Use BaseException so a cancelled states fetch propagates instead of
69
+ # being assigned to `entities` and crashing downstream iteration
70
+ # (mirrors get_entities_by_area / _fetch_search_entities).
71
+ if isinstance(results[0], BaseException):
68
72
  raise results[0]
69
73
  entities = results[0]
74
+ # A cancelled services/registry sub-task must propagate too, not be
75
+ # silently degraded by the fail-open handlers below.
76
+ for sub_result in results[1:]:
77
+ if isinstance(sub_result, asyncio.CancelledError):
78
+ raise sub_result
79
+
80
+ # Opt-in visibility filter: drop out-of-scope entities from the whole
81
+ # overview universe before any domain stats/samples are computed, so
82
+ # every count stays coherent. results[3] is the raw entity registry,
83
+ # results[4] the device registry (so the area/label dimensions match a
84
+ # device-bound entity by its device's area/labels).
85
+ # Fails open (empty set on any error); do NOT wrap in try/except.
86
+ visibility_hidden, visibility_warnings = await load_hidden_set(
87
+ results[3], results[0], self.client, results[4]
88
+ )
89
+ if visibility_hidden:
90
+ entities = [
91
+ e for e in entities if e.get("entity_id") not in visibility_hidden
92
+ ]
70
93
 
71
- # Services failure affects total count + catalog; log at warning.
94
+ # Services failure means the total count + catalog are genuinely
95
+ # incomplete, so it sets `partial`. Visibility warnings are kept
96
+ # separate: they annotate an otherwise-complete result (a config typo
97
+ # or a skipped Assist dimension), so they must surface in `warnings`
98
+ # without flipping `partial` — aligning with ha_search, which reports
99
+ # the same visibility warnings and never marks itself partial for them.
72
100
  partial_warnings: list[str] = []
73
101
  if isinstance(results[1], Exception):
74
102
  logger.warning(f"Could not fetch services: {results[1]}")
@@ -125,7 +153,8 @@ class SystemOverviewMixin(_SearchBase):
125
153
  formatted_domain_stats, limit, offset, detail_level
126
154
  )
127
155
 
128
- # totals always reflect the full system, regardless of filtering
156
+ # totals reflect the full (visibility-filtered) system and are not
157
+ # narrowed by the domains_filter display filter below
129
158
  system_summary: dict[str, Any] = {
130
159
  "total_entities": len(entities),
131
160
  "total_domains": len(all_domains),
@@ -143,6 +172,7 @@ class SystemOverviewMixin(_SearchBase):
143
172
  detail_level=detail_level,
144
173
  pagination_metadata=pagination_metadata,
145
174
  partial_warnings=partial_warnings,
175
+ visibility_warnings=visibility_warnings,
146
176
  device_types=device_types,
147
177
  service_stats=service_stats,
148
178
  )
@@ -162,9 +192,10 @@ class SystemOverviewMixin(_SearchBase):
162
192
  "controllable_devices": {},
163
193
  },
164
194
  )
165
- # exception_to_structured_error always raises; unreachable but makes
166
- # every code path return explicitly (py/mixed-returns).
167
- return None
195
+ # ``exception_to_structured_error`` always raises (NoReturn); the
196
+ # explicit re-raise makes the exit unambiguous and matches the
197
+ # sibling handlers in get_entities_by_area / _fetch_search_entities.
198
+ raise
168
199
 
169
200
  @staticmethod
170
201
  def _build_entity_area_map(
@@ -478,6 +509,7 @@ class SystemOverviewMixin(_SearchBase):
478
509
  detail_level: str,
479
510
  pagination_metadata: dict[str, Any] | None,
480
511
  partial_warnings: list[str],
512
+ visibility_warnings: list[str],
481
513
  device_types: dict[str, int],
482
514
  service_stats: dict[str, dict[str, Any]],
483
515
  ) -> dict[str, Any]:
@@ -497,9 +529,14 @@ class SystemOverviewMixin(_SearchBase):
497
529
  if pagination_metadata:
498
530
  base_response["pagination"] = pagination_metadata
499
531
 
532
+ # Only genuinely incomplete data (e.g. a failed services fetch) marks the
533
+ # response partial; visibility warnings annotate a complete result. Both
534
+ # surface in `warnings`.
500
535
  if partial_warnings:
501
536
  base_response["partial"] = True
502
- base_response["warnings"] = partial_warnings
537
+ all_warnings = partial_warnings + visibility_warnings
538
+ if all_warnings:
539
+ base_response["warnings"] = all_warnings
503
540
 
504
541
  # Full: add device types and service catalog
505
542
  if detail_level == "full":