ha-mcp-dev 7.9.0.dev764__tar.gz → 7.9.0.dev766__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.
- {ha_mcp_dev-7.9.0.dev764/src/ha_mcp_dev.egg-info → ha_mcp_dev-7.9.0.dev766}/PKG-INFO +2 -2
- {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev766}/README.md +1 -1
- {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev766}/pyproject.toml +1 -1
- {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev766}/src/ha_mcp/settings_ui/__init__.py +60 -0
- {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev766}/src/ha_mcp/settings_ui/settings.css +15 -0
- {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev766}/src/ha_mcp/settings_ui/settings.html +65 -0
- {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev766}/src/ha_mcp/settings_ui/settings.js +113 -0
- {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev766}/src/ha_mcp/tools/smart_search/_deep.py +5 -1
- {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev766}/src/ha_mcp/tools/smart_search/_entities.py +72 -24
- {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev766}/src/ha_mcp/tools/smart_search/_overview.py +44 -7
- {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev766}/src/ha_mcp/tools/tools_search.py +81 -23
- {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev766}/src/ha_mcp/tools/util_helpers.py +10 -0
- ha_mcp_dev-7.9.0.dev766/src/ha_mcp/visibility/__init__.py +0 -0
- ha_mcp_dev-7.9.0.dev766/src/ha_mcp/visibility/model.py +35 -0
- ha_mcp_dev-7.9.0.dev766/src/ha_mcp/visibility/persistence.py +42 -0
- ha_mcp_dev-7.9.0.dev766/src/ha_mcp/visibility/resolver.py +509 -0
- {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev766/src/ha_mcp_dev.egg-info}/PKG-INFO +2 -2
- {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev766}/src/ha_mcp_dev.egg-info/SOURCES.txt +4 -0
- {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev766}/LICENSE +0 -0
- {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev766}/MANIFEST.in +0 -0
- {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev766}/setup.cfg +0 -0
- {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev766}/src/ha_mcp/__init__.py +0 -0
- {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev766}/src/ha_mcp/__main__.py +0 -0
- {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev766}/src/ha_mcp/_pypi_marker +0 -0
- {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev766}/src/ha_mcp/_version.py +0 -0
- {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev766}/src/ha_mcp/auth/__init__.py +0 -0
- {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev766}/src/ha_mcp/auth/consent_form.py +0 -0
- {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev766}/src/ha_mcp/auth/provider.py +0 -0
- {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev766}/src/ha_mcp/backup_manager.py +0 -0
- {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev766}/src/ha_mcp/client/__init__.py +0 -0
- {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev766}/src/ha_mcp/client/rest_client.py +0 -0
- {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev766}/src/ha_mcp/client/supervisor_client.py +0 -0
- {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev766}/src/ha_mcp/client/websocket_client.py +0 -0
- {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev766}/src/ha_mcp/client/websocket_listener.py +0 -0
- {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev766}/src/ha_mcp/config.py +0 -0
- {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev766}/src/ha_mcp/dashboard_screenshot/__init__.py +0 -0
- {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev766}/src/ha_mcp/dashboard_screenshot/capture.py +0 -0
- {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev766}/src/ha_mcp/dashboard_screenshot/provision.py +0 -0
- {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev766}/src/ha_mcp/errors.py +0 -0
- {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev766}/src/ha_mcp/policy/__init__.py +0 -0
- {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev766}/src/ha_mcp/policy/approval_queue.py +0 -0
- {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev766}/src/ha_mcp/policy/evaluator.py +0 -0
- {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev766}/src/ha_mcp/policy/handlers.py +0 -0
- {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev766}/src/ha_mcp/policy/middleware.py +0 -0
- {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev766}/src/ha_mcp/policy/model.py +0 -0
- {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev766}/src/ha_mcp/policy/persistence.py +0 -0
- {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev766}/src/ha_mcp/policy/value_sources.py +0 -0
- {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev766}/src/ha_mcp/py.typed +0 -0
- {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev766}/src/ha_mcp/read_only.py +0 -0
- {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev766}/src/ha_mcp/resources/skills-vendor/.claude/settings.json +0 -0
- {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev766}/src/ha_mcp/resources/skills-vendor/.claude-plugin/marketplace.json +0 -0
- {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev766}/src/ha_mcp/resources/skills-vendor/.claude-plugin/plugin.json +0 -0
- {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev766}/src/ha_mcp/resources/skills-vendor/.github/ISSUE_TEMPLATE/skill-rca.md +0 -0
- {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev766}/src/ha_mcp/resources/skills-vendor/.github/pull_request_template.md +0 -0
- {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev766}/src/ha_mcp/resources/skills-vendor/AGENTS.md +0 -0
- {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev766}/src/ha_mcp/resources/skills-vendor/CLAUDE.md +0 -0
- {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev766}/src/ha_mcp/resources/skills-vendor/CONTRIBUTING.md +0 -0
- {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev766}/src/ha_mcp/resources/skills-vendor/LICENSE +0 -0
- {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev766}/src/ha_mcp/resources/skills-vendor/README.md +0 -0
- {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev766}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/SKILL.md +0 -0
- {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev766}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/evals/evals.json +0 -0
- {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev766}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/appdaemon.md +0 -0
- {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev766}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/automation-patterns.md +0 -0
- {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev766}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-cards.md +0 -0
- {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev766}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-guide.md +0 -0
- {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev766}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/device-control.md +0 -0
- {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev766}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/domain-docs.md +0 -0
- {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev766}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/examples.yaml +0 -0
- {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev766}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/helper-selection.md +0 -0
- {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev766}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/safe-refactoring.md +0 -0
- {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev766}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/scenes.md +0 -0
- {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev766}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/template-guidelines.md +0 -0
- {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev766}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/yaml-only-integrations.md +0 -0
- {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev766}/src/ha_mcp/server.py +0 -0
- {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev766}/src/ha_mcp/smoke_test.py +0 -0
- {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev766}/src/ha_mcp/stdio_settings_sidecar.py +0 -0
- {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev766}/src/ha_mcp/tools/__init__.py +0 -0
- {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev766}/src/ha_mcp/tools/auto_backup.py +0 -0
- {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev766}/src/ha_mcp/tools/backup.py +0 -0
- {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev766}/src/ha_mcp/tools/best_practice_checker.py +0 -0
- {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev766}/src/ha_mcp/tools/config_entry_flow.py +0 -0
- {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev766}/src/ha_mcp/tools/device_control.py +0 -0
- {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev766}/src/ha_mcp/tools/enhanced.py +0 -0
- {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev766}/src/ha_mcp/tools/helpers.py +0 -0
- {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev766}/src/ha_mcp/tools/radio/__init__.py +0 -0
- {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev766}/src/ha_mcp/tools/radio/base.py +0 -0
- {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev766}/src/ha_mcp/tools/radio/matter.py +0 -0
- {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev766}/src/ha_mcp/tools/radio/thread.py +0 -0
- {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev766}/src/ha_mcp/tools/radio/zigbee.py +0 -0
- {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev766}/src/ha_mcp/tools/radio/zwave.py +0 -0
- {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev766}/src/ha_mcp/tools/reference_validator.py +0 -0
- {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev766}/src/ha_mcp/tools/registry.py +0 -0
- {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev766}/src/ha_mcp/tools/smart_search/__init__.py +0 -0
- {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev766}/src/ha_mcp/tools/smart_search/_base.py +0 -0
- {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev766}/src/ha_mcp/tools/smart_search/_config.py +0 -0
- {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev766}/src/ha_mcp/tools/smart_search/_fetch.py +0 -0
- {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev766}/src/ha_mcp/tools/smart_search/_scenes.py +0 -0
- {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev766}/src/ha_mcp/tools/smart_search/_scoring.py +0 -0
- {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev766}/src/ha_mcp/tools/tool_search_hint_middleware.py +0 -0
- {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev766}/src/ha_mcp/tools/tools_addons.py +0 -0
- {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev766}/src/ha_mcp/tools/tools_areas.py +0 -0
- {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev766}/src/ha_mcp/tools/tools_blueprints.py +0 -0
- {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev766}/src/ha_mcp/tools/tools_bug_report.py +0 -0
- {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev766}/src/ha_mcp/tools/tools_calendar.py +0 -0
- {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev766}/src/ha_mcp/tools/tools_camera.py +0 -0
- {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev766}/src/ha_mcp/tools/tools_categories.py +0 -0
- {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev766}/src/ha_mcp/tools/tools_code.py +0 -0
- {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev766}/src/ha_mcp/tools/tools_config_automations.py +0 -0
- {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev766}/src/ha_mcp/tools/tools_config_dashboards.py +0 -0
- {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev766}/src/ha_mcp/tools/tools_config_helpers.py +0 -0
- {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev766}/src/ha_mcp/tools/tools_config_scenes.py +0 -0
- {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev766}/src/ha_mcp/tools/tools_config_scripts.py +0 -0
- {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev766}/src/ha_mcp/tools/tools_dashboard_screenshot.py +0 -0
- {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev766}/src/ha_mcp/tools/tools_energy.py +0 -0
- {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev766}/src/ha_mcp/tools/tools_entities.py +0 -0
- {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev766}/src/ha_mcp/tools/tools_filesystem.py +0 -0
- {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev766}/src/ha_mcp/tools/tools_groups.py +0 -0
- {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev766}/src/ha_mcp/tools/tools_hacs.py +0 -0
- {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev766}/src/ha_mcp/tools/tools_history.py +0 -0
- {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev766}/src/ha_mcp/tools/tools_integrations.py +0 -0
- {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev766}/src/ha_mcp/tools/tools_labels.py +0 -0
- {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev766}/src/ha_mcp/tools/tools_mcp_component.py +0 -0
- {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev766}/src/ha_mcp/tools/tools_radio.py +0 -0
- {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev766}/src/ha_mcp/tools/tools_registry.py +0 -0
- {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev766}/src/ha_mcp/tools/tools_resources.py +0 -0
- {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev766}/src/ha_mcp/tools/tools_service.py +0 -0
- {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev766}/src/ha_mcp/tools/tools_services.py +0 -0
- {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev766}/src/ha_mcp/tools/tools_system.py +0 -0
- {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev766}/src/ha_mcp/tools/tools_themes.py +0 -0
- {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev766}/src/ha_mcp/tools/tools_todo.py +0 -0
- {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev766}/src/ha_mcp/tools/tools_traces.py +0 -0
- {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev766}/src/ha_mcp/tools/tools_updates.py +0 -0
- {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev766}/src/ha_mcp/tools/tools_utility.py +0 -0
- {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev766}/src/ha_mcp/tools/tools_voice_assistant.py +0 -0
- {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev766}/src/ha_mcp/tools/tools_yaml_config.py +0 -0
- {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev766}/src/ha_mcp/tools/tools_zones.py +0 -0
- {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev766}/src/ha_mcp/tools/validation_middleware.py +0 -0
- {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev766}/src/ha_mcp/transforms/__init__.py +0 -0
- {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev766}/src/ha_mcp/transforms/categorized_search.py +0 -0
- {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev766}/src/ha_mcp/transforms/lite_docstrings.py +0 -0
- {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev766}/src/ha_mcp/update_check.py +0 -0
- {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev766}/src/ha_mcp/utils/__init__.py +0 -0
- {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev766}/src/ha_mcp/utils/config_hash.py +0 -0
- {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev766}/src/ha_mcp/utils/data_paths.py +0 -0
- {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev766}/src/ha_mcp/utils/domain_handlers.py +0 -0
- {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev766}/src/ha_mcp/utils/fuzzy_search.py +0 -0
- {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev766}/src/ha_mcp/utils/kill_signal_diagnostics.py +0 -0
- {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev766}/src/ha_mcp/utils/operation_manager.py +0 -0
- {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev766}/src/ha_mcp/utils/python_sandbox.py +0 -0
- {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev766}/src/ha_mcp/utils/skill_loader.py +0 -0
- {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev766}/src/ha_mcp/utils/usage_logger.py +0 -0
- {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev766}/src/ha_mcp_dev.egg-info/dependency_links.txt +0 -0
- {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev766}/src/ha_mcp_dev.egg-info/entry_points.txt +0 -0
- {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev766}/src/ha_mcp_dev.egg-info/requires.txt +0 -0
- {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev766}/src/ha_mcp_dev.egg-info/top_level.txt +0 -0
- {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev766}/tests/__init__.py +0 -0
- {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev766}/tests/test_constants.py +0 -0
- {ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev766}/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.
|
|
3
|
+
Version: 7.9.0.dev766
|
|
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
|
|
@@ -316,7 +316,7 @@ To add manually: open **HACS** > **Integrations** > three-dot menu > **Custom re
|
|
|
316
316
|
|
|
317
317
|
After installing, restart Home Assistant. Then open **Settings** > **Devices & Services** > **Add Integration** and search for **HA-MCP Custom Component**.
|
|
318
318
|
|
|
319
|
-
|
|
319
|
+
Want a server too? Add the **HA-MCP Server** entry (see the in-process server section below) — it runs the full server inside Home Assistant on every install type. The add-on remains available from the add-on store for Home Assistant OS / Supervised users who prefer it.
|
|
320
320
|
|
|
321
321
|
### Install manually
|
|
322
322
|
|
|
@@ -283,7 +283,7 @@ To add manually: open **HACS** > **Integrations** > three-dot menu > **Custom re
|
|
|
283
283
|
|
|
284
284
|
After installing, restart Home Assistant. Then open **Settings** > **Devices & Services** > **Add Integration** and search for **HA-MCP Custom Component**.
|
|
285
285
|
|
|
286
|
-
|
|
286
|
+
Want a server too? Add the **HA-MCP Server** entry (see the in-process server section below) — it runs the full server inside Home Assistant on every install type. The add-on remains available from the add-on store for Home Assistant OS / Supervised users who prefer it.
|
|
287
287
|
|
|
288
288
|
### Install manually
|
|
289
289
|
|
|
@@ -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.
|
|
7
|
+
version = "7.9.0.dev766"
|
|
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 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 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 (“conversation”) voice assistant, mirroring the Settings > Voice assistants > 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
|
-
|
|
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
|
{ha_mcp_dev-7.9.0.dev764 → ha_mcp_dev-7.9.0.dev766}/src/ha_mcp/tools/smart_search/_entities.py
RENAMED
|
@@ -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(
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
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
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
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
|