ha-mcp-dev 8.3.0.dev2387__tar.gz → 8.3.0.dev2393__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-8.3.0.dev2387/src/ha_mcp_dev.egg-info → ha_mcp_dev-8.3.0.dev2393}/PKG-INFO +1 -1
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/pyproject.toml +1 -1
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/tools/smart_search/_entities.py +63 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/tools/tools_search.py +225 -41
- ha_mcp_dev-8.3.0.dev2393/src/ha_mcp/utils/entity_membership.py +59 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393/src/ha_mcp_dev.egg-info}/PKG-INFO +1 -1
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp_dev.egg-info/SOURCES.txt +1 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/LICENSE +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/MANIFEST.in +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/README.md +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/setup.cfg +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/__init__.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/__main__.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/_pypi_marker +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/_rich_compat.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/_vendor/__init__.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/_vendor/requirements.txt +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/_vendor/websockets/LICENSE +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/_vendor/websockets/MANIFEST.sha256 +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/_vendor/websockets/VENDORED +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/_vendor/websockets/__init__.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/_vendor/websockets/__main__.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/_vendor/websockets/asyncio/__init__.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/_vendor/websockets/asyncio/client.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/_vendor/websockets/asyncio/connection.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/_vendor/websockets/asyncio/messages.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/_vendor/websockets/asyncio/router.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/_vendor/websockets/asyncio/server.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/_vendor/websockets/auth.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/_vendor/websockets/cli.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/_vendor/websockets/client.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/_vendor/websockets/connection.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/_vendor/websockets/datastructures.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/_vendor/websockets/exceptions.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/_vendor/websockets/extensions/__init__.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/_vendor/websockets/extensions/base.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/_vendor/websockets/extensions/permessage_deflate.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/_vendor/websockets/frames.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/_vendor/websockets/headers.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/_vendor/websockets/http11.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/_vendor/websockets/imports.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/_vendor/websockets/legacy/__init__.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/_vendor/websockets/legacy/auth.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/_vendor/websockets/legacy/client.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/_vendor/websockets/legacy/exceptions.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/_vendor/websockets/legacy/framing.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/_vendor/websockets/legacy/handshake.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/_vendor/websockets/legacy/http.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/_vendor/websockets/legacy/protocol.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/_vendor/websockets/legacy/server.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/_vendor/websockets/protocol.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/_vendor/websockets/proxy.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/_vendor/websockets/py.typed +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/_vendor/websockets/server.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/_vendor/websockets/streams.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/_vendor/websockets/sync/__init__.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/_vendor/websockets/sync/client.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/_vendor/websockets/sync/connection.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/_vendor/websockets/sync/messages.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/_vendor/websockets/sync/router.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/_vendor/websockets/sync/server.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/_vendor/websockets/sync/utils.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/_vendor/websockets/trio/__init__.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/_vendor/websockets/trio/client.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/_vendor/websockets/trio/connection.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/_vendor/websockets/trio/messages.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/_vendor/websockets/trio/router.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/_vendor/websockets/trio/server.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/_vendor/websockets/trio/utils.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/_vendor/websockets/typing.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/_vendor/websockets/uri.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/_vendor/websockets/utils.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/_vendor/websockets/version.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/_version.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/auth/__init__.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/auth/consent_form.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/auth/oidc_compat.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/auth/provider.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/backup_manager.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/browser_landing.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/client/__init__.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/client/rest_client.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/client/supervisor_client.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/client/websocket_client.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/client/websocket_listener.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/config.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/dashboard_screenshot/__init__.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/dashboard_screenshot/capture.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/dashboard_screenshot/content.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/dashboard_screenshot/paths.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/dashboard_screenshot/provision.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/dashboard_screenshot/theme_guard.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/errors.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/hacs_auto_refresh.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/llm_exposure.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/policy/__init__.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/policy/approval_queue.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/policy/editing.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/policy/evaluator.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/policy/handlers.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/policy/middleware.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/policy/model.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/policy/persistence.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/policy/value_sources.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/py.typed +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/read_only.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/redaction.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/renamed_tools.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/resources/skills-vendor/.claude/settings.json +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/resources/skills-vendor/.claude-plugin/marketplace.json +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/resources/skills-vendor/.claude-plugin/plugin.json +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/resources/skills-vendor/.github/ISSUE_TEMPLATE/skill-rca.md +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/resources/skills-vendor/.github/pull_request_template.md +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/resources/skills-vendor/AGENTS.md +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/resources/skills-vendor/CLAUDE.md +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/resources/skills-vendor/CONTRIBUTING.md +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/resources/skills-vendor/LICENSE +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/resources/skills-vendor/README.md +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/SKILL.md +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/evals/evals.json +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/appdaemon.md +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/automation-patterns.md +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/blueprint-guide.md +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-cards.md +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-guide.md +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/device-control.md +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/domain-docs.md +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/examples.yaml +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/helper-selection.md +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/safe-refactoring.md +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/scenes.md +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/template-guidelines.md +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/yaml-only-integrations.md +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/server.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/settings_ui/__init__.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/settings_ui/_handlers_advanced.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/settings_ui/_handlers_backups.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/settings_ui/_handlers_fs.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/settings_ui/_handlers_server.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/settings_ui/_handlers_theme.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/settings_ui/_handlers_tools.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/settings_ui/_i18n.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/settings_ui/_persistence.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/settings_ui/_supervisor.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/settings_ui/_theme.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/settings_ui/_tools_meta.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/settings_ui/locales/README.md +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/settings_ui/locales/cs.json +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/settings_ui/locales/de.json +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/settings_ui/locales/en.json +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/settings_ui/locales/eo.json +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/settings_ui/locales/es.json +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/settings_ui/locales/fr.json +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/settings_ui/locales/it.json +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/settings_ui/locales/nl.json +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/settings_ui/locales/pl.json +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/settings_ui/locales/ru.json +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/settings_ui/locales/sv.json +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/settings_ui/locales/zh-Hans.json +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/settings_ui/settings.css +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/settings_ui/settings.html +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/settings_ui/settings.js +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/smoke_test.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/stdio_settings_sidecar.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/strict_bps.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/tools/__init__.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/tools/auto_backup.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/tools/backup.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/tools/best_practice_checker.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/tools/best_practice_result.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/tools/best_practice_variables.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/tools/component_api.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/tools/component_config_entries.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/tools/component_config_reads.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/tools/component_devices.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/tools/component_registries.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/tools/component_registry_lookup.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/tools/config_entry_flow.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/tools/config_entry_flow_form.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/tools/config_entry_flow_menu.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/tools/config_entry_flow_walker.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/tools/config_entry_identity.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/tools/config_entry_reconfigure.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/tools/config_entry_reload_watch.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/tools/device_control.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/tools/error_log_parsing.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/tools/hacs_registration.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/tools/helpers.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/tools/integration_reconfigure.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/tools/radio/__init__.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/tools/radio/base.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/tools/radio/matter.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/tools/radio/thread.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/tools/radio/zigbee.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/tools/radio/zwave.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/tools/reference_validator.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/tools/registry.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/tools/renamed_tool_middleware.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/tools/smart_search/__init__.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/tools/smart_search/_base.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/tools/smart_search/_config.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/tools/smart_search/_deep.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/tools/smart_search/_fetch.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/tools/smart_search/_overview.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/tools/smart_search/_scenes.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/tools/smart_search/_scoring.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/tools/tool_search_hint_middleware.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/tools/tools_addons.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/tools/tools_areas.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/tools/tools_assist_pipeline.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/tools/tools_blueprints.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/tools/tools_bug_report.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/tools/tools_calendar.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/tools/tools_camera.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/tools/tools_categories.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/tools/tools_code.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/tools/tools_config_automations.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/tools/tools_config_dashboards.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/tools/tools_config_helpers.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/tools/tools_config_scenes.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/tools/tools_config_scripts.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/tools/tools_dashboard_screenshot.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/tools/tools_dev.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/tools/tools_energy.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/tools/tools_entities.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/tools/tools_filesystem.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/tools/tools_groups.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/tools/tools_hacs.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/tools/tools_history.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/tools/tools_integrations.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/tools/tools_labels.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/tools/tools_radio.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/tools/tools_registry.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/tools/tools_resources.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/tools/tools_security_policy.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/tools/tools_service.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/tools/tools_services.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/tools/tools_system.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/tools/tools_themes.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/tools/tools_todo.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/tools/tools_traces.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/tools/tools_updates.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/tools/tools_utility.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/tools/tools_voice_assistant.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/tools/tools_yaml_config.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/tools/tools_yaml_read.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/tools/tools_zones.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/tools/util_helpers.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/tools/validation_middleware.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/transforms/__init__.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/transforms/categorized_search.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/transforms/lite_docstrings.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/transport_security.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/update_check.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/utils/__init__.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/utils/config_hash.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/utils/config_write_lock.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/utils/data_paths.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/utils/domain_handlers.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/utils/fuzzy_search.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/utils/kill_signal_diagnostics.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/utils/operation_manager.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/utils/python_sandbox.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/utils/skill_loader.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/utils/usage_logger.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/visibility/__init__.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/visibility/enforcement.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/visibility/model.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/visibility/persistence.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/visibility/resolver.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp_dev.egg-info/dependency_links.txt +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp_dev.egg-info/entry_points.txt +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp_dev.egg-info/requires.txt +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp_dev.egg-info/top_level.txt +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/tests/__init__.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/tests/test_constants.py +0 -0
- {ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/tests/test_env_manager.py +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "ha-mcp-dev"
|
|
7
|
-
version = "8.3.0.
|
|
7
|
+
version = "8.3.0.dev2393"
|
|
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
|
{ha_mcp_dev-8.3.0.dev2387 → ha_mcp_dev-8.3.0.dev2393}/src/ha_mcp/tools/smart_search/_entities.py
RENAMED
|
@@ -2,8 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
import asyncio
|
|
4
4
|
import logging
|
|
5
|
+
from collections.abc import Mapping
|
|
5
6
|
from typing import Any
|
|
6
7
|
|
|
8
|
+
from ...utils.entity_membership import normalize_member_entity_ids
|
|
7
9
|
from ...utils.fuzzy_search import calculate_partial_ratio, calculate_ratio
|
|
8
10
|
from ...visibility.resolver import (
|
|
9
11
|
device_registry_needed_for_visibility,
|
|
@@ -21,6 +23,35 @@ logger = logging.getLogger(__name__)
|
|
|
21
23
|
_GET_ENTRIES_CHUNK_SIZE = 500
|
|
22
24
|
|
|
23
25
|
|
|
26
|
+
def _redact_hidden_memberships(
|
|
27
|
+
entities: list[dict[str, Any]], visibility_hidden: set[str]
|
|
28
|
+
) -> list[dict[str, Any]]:
|
|
29
|
+
"""Mark membership whose direct IDs intersect the visibility deny-set.
|
|
30
|
+
|
|
31
|
+
tools_search._add_membership_fields consumes this private sentinel after
|
|
32
|
+
smart-search projection, preserving is_group while withholding member IDs.
|
|
33
|
+
"""
|
|
34
|
+
if not visibility_hidden:
|
|
35
|
+
return entities
|
|
36
|
+
redacted: list[dict[str, Any]] = []
|
|
37
|
+
for entity in entities:
|
|
38
|
+
attributes = entity.get("attributes")
|
|
39
|
+
members = normalize_member_entity_ids(attributes)
|
|
40
|
+
if members is not None and visibility_hidden.intersection(members):
|
|
41
|
+
redacted.append(
|
|
42
|
+
{
|
|
43
|
+
**entity,
|
|
44
|
+
"attributes": {
|
|
45
|
+
**(attributes if isinstance(attributes, Mapping) else {}),
|
|
46
|
+
"_ha_mcp_membership_redacted": True,
|
|
47
|
+
},
|
|
48
|
+
}
|
|
49
|
+
)
|
|
50
|
+
else:
|
|
51
|
+
redacted.append(entity)
|
|
52
|
+
return redacted
|
|
53
|
+
|
|
54
|
+
|
|
24
55
|
class EntitySearchMixin(_SearchBase):
|
|
25
56
|
"""``smart_entity_search`` and ``get_entities_by_area`` plus helpers."""
|
|
26
57
|
|
|
@@ -33,6 +64,7 @@ class EntitySearchMixin(_SearchBase):
|
|
|
33
64
|
domain_filter: str | None = None,
|
|
34
65
|
include_hidden: bool = True,
|
|
35
66
|
*,
|
|
67
|
+
include_membership: bool = False,
|
|
36
68
|
prefetched_states: list[dict[str, Any]] | None = None,
|
|
37
69
|
prefetched_registry: Any = None,
|
|
38
70
|
) -> dict[str, Any]:
|
|
@@ -49,6 +81,8 @@ class EntitySearchMixin(_SearchBase):
|
|
|
49
81
|
set in the entity registry are still returned but receive
|
|
50
82
|
a score penalty so they sort below comparable visible
|
|
51
83
|
matches. Pass False to filter them out entirely.
|
|
84
|
+
include_membership: Whether to retain membership attributes and redaction markers
|
|
85
|
+
for opt-in search result fields.
|
|
52
86
|
prefetched_states: Pre-fetched ``get_states()`` list shared by the
|
|
53
87
|
ha_search orchestrator when both search branches run; ``None``
|
|
54
88
|
means fetch here.
|
|
@@ -68,6 +102,7 @@ class EntitySearchMixin(_SearchBase):
|
|
|
68
102
|
entities, visibility_warnings = await self._fetch_search_entities(
|
|
69
103
|
domain_filter,
|
|
70
104
|
include_hidden,
|
|
105
|
+
include_membership=include_membership,
|
|
71
106
|
prefetched_states=prefetched_states,
|
|
72
107
|
prefetched_registry=prefetched_registry,
|
|
73
108
|
)
|
|
@@ -261,6 +296,7 @@ class EntitySearchMixin(_SearchBase):
|
|
|
261
296
|
domain_filter: str | None,
|
|
262
297
|
include_hidden: bool,
|
|
263
298
|
*,
|
|
299
|
+
include_membership: bool = False,
|
|
264
300
|
prefetched_states: list[dict[str, Any]] | None = None,
|
|
265
301
|
prefetched_registry: Any = None,
|
|
266
302
|
) -> tuple[list[dict[str, Any]], list[str]]:
|
|
@@ -334,6 +370,17 @@ class EntitySearchMixin(_SearchBase):
|
|
|
334
370
|
registry_result, states_result, self.client, device_result
|
|
335
371
|
)
|
|
336
372
|
registry_slim = self._build_registry_slim(registry_result)
|
|
373
|
+
if include_membership:
|
|
374
|
+
denied_member_ids = visibility_hidden | (
|
|
375
|
+
{
|
|
376
|
+
entity_id
|
|
377
|
+
for entity_id, entry in registry_slim.items()
|
|
378
|
+
if entry.get("hidden_by") is not None
|
|
379
|
+
}
|
|
380
|
+
if not include_hidden
|
|
381
|
+
else set()
|
|
382
|
+
)
|
|
383
|
+
entities = _redact_hidden_memberships(entities, denied_member_ids)
|
|
337
384
|
survivor_ids, survivor_states = self._filter_hidden_entities(
|
|
338
385
|
entities, registry_slim, include_hidden, visibility_hidden
|
|
339
386
|
)
|
|
@@ -401,6 +448,8 @@ class EntitySearchMixin(_SearchBase):
|
|
|
401
448
|
area_query: str,
|
|
402
449
|
group_by_domain: bool = True,
|
|
403
450
|
include_hidden: bool = True,
|
|
451
|
+
*,
|
|
452
|
+
include_membership: bool = False,
|
|
404
453
|
) -> dict[str, Any]:
|
|
405
454
|
"""
|
|
406
455
|
Get entities grouped by area/room using the HA registries for accurate area resolution.
|
|
@@ -417,6 +466,8 @@ class EntitySearchMixin(_SearchBase):
|
|
|
417
466
|
set in the entity registry are still grouped under their
|
|
418
467
|
area but receive a score penalty when ranked. Pass False
|
|
419
468
|
to filter them out entirely.
|
|
469
|
+
include_membership: Whether to retain membership attributes and redaction markers
|
|
470
|
+
for opt-in search result fields.
|
|
420
471
|
|
|
421
472
|
Returns:
|
|
422
473
|
Dictionary with area-grouped entities
|
|
@@ -509,6 +560,17 @@ class EntitySearchMixin(_SearchBase):
|
|
|
509
560
|
entity_area_resolved, hidden_entity_ids = self._resolve_entity_areas(
|
|
510
561
|
entity_reg_map, device_area_map, include_hidden, visibility_hidden
|
|
511
562
|
)
|
|
563
|
+
if include_membership:
|
|
564
|
+
denied_member_ids = visibility_hidden | (
|
|
565
|
+
{
|
|
566
|
+
entity_id
|
|
567
|
+
for entity_id, entry in entity_reg_map.items()
|
|
568
|
+
if entry.get("hidden_by") is not None
|
|
569
|
+
}
|
|
570
|
+
if not include_hidden
|
|
571
|
+
else set()
|
|
572
|
+
)
|
|
573
|
+
entities = _redact_hidden_memberships(entities, denied_member_ids)
|
|
512
574
|
state_map = self._build_state_map(entities)
|
|
513
575
|
formatted_areas, total_entities = self._format_area_entities(
|
|
514
576
|
matched_area_ids,
|
|
@@ -870,6 +932,7 @@ class EntitySearchMixin(_SearchBase):
|
|
|
870
932
|
),
|
|
871
933
|
"state": state_info.get("state", "unknown"),
|
|
872
934
|
"_hidden_by": "hidden" if entity_id in hidden_entity_ids else None,
|
|
935
|
+
"_attributes": state_info.get("attributes", {}),
|
|
873
936
|
}
|
|
874
937
|
if include_domain:
|
|
875
938
|
record["domain"] = entity_id.split(".", maxsplit=1)[0]
|
|
@@ -6,6 +6,7 @@ This module provides entity search, system overview, deep search, and state retr
|
|
|
6
6
|
|
|
7
7
|
import asyncio
|
|
8
8
|
import logging
|
|
9
|
+
from collections.abc import Mapping
|
|
9
10
|
from dataclasses import dataclass
|
|
10
11
|
from typing import Annotated, Any, Literal, cast
|
|
11
12
|
|
|
@@ -24,6 +25,7 @@ from ..client.websocket_client import get_websocket_client
|
|
|
24
25
|
from ..config import get_global_settings
|
|
25
26
|
from ..errors import create_validation_error
|
|
26
27
|
from ..transforms.categorized_search import DEFAULT_PINNED_TOOLS
|
|
28
|
+
from ..utils.entity_membership import normalize_member_entity_ids
|
|
27
29
|
from ..utils.fuzzy_search import apply_hidden_penalty
|
|
28
30
|
from ..visibility.resolver import (
|
|
29
31
|
device_registry_needed_for_visibility,
|
|
@@ -596,12 +598,22 @@ def _apply_by_domain_grouping(
|
|
|
596
598
|
by_domain = {d: ents[:per_domain_limit_int] for d, ents in by_domain.items()}
|
|
597
599
|
if parsed_result_fields is not None:
|
|
598
600
|
by_domain = {
|
|
599
|
-
d: _project_records(ents, parsed_result_fields)
|
|
601
|
+
d: _project_records(ents, _effective_result_fields(parsed_result_fields))
|
|
600
602
|
for d, ents in by_domain.items()
|
|
601
603
|
}
|
|
602
604
|
data["by_domain"] = by_domain
|
|
603
605
|
|
|
604
606
|
|
|
607
|
+
def _effective_result_fields(parsed_result_fields: list[str]) -> list[str]:
|
|
608
|
+
"""Retain is_group whenever member_entity_ids is projected."""
|
|
609
|
+
if (
|
|
610
|
+
"member_entity_ids" in parsed_result_fields
|
|
611
|
+
and "is_group" not in parsed_result_fields
|
|
612
|
+
):
|
|
613
|
+
return [*parsed_result_fields, "is_group"]
|
|
614
|
+
return parsed_result_fields
|
|
615
|
+
|
|
616
|
+
|
|
605
617
|
def _apply_result_fields_to_response(
|
|
606
618
|
data: dict[str, Any],
|
|
607
619
|
parsed_result_fields: list[str] | None,
|
|
@@ -610,8 +622,17 @@ def _apply_result_fields_to_response(
|
|
|
610
622
|
if parsed_result_fields is None or "results" not in data:
|
|
611
623
|
return
|
|
612
624
|
orig = data["results"]
|
|
613
|
-
data["results"] = _project_records(
|
|
614
|
-
|
|
625
|
+
data["results"] = _project_records(
|
|
626
|
+
orig, _effective_result_fields(parsed_result_fields)
|
|
627
|
+
)
|
|
628
|
+
warning_fields = [
|
|
629
|
+
field for field in parsed_result_fields if field != "member_entity_ids"
|
|
630
|
+
]
|
|
631
|
+
_warn = (
|
|
632
|
+
_result_fields_warning(orig, data["results"], warning_fields)
|
|
633
|
+
if warning_fields
|
|
634
|
+
else None
|
|
635
|
+
)
|
|
615
636
|
if _warn:
|
|
616
637
|
data.setdefault("warnings", []).append(_warn)
|
|
617
638
|
|
|
@@ -738,12 +759,15 @@ _ENTITY_RECORD_KEYS = (
|
|
|
738
759
|
# (SearchTools._fetch_entity_enrichment). Ordered so a projected record lists them
|
|
739
760
|
# consistently regardless of the caller's result_fields order.
|
|
740
761
|
_ENRICHMENT_FIELDS: tuple[str, ...] = ("area", "floor", "labels", "aliases")
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
#
|
|
744
|
-
#
|
|
745
|
-
|
|
746
|
-
|
|
762
|
+
_MEMBERSHIP_FIELDS: tuple[str, ...] = ("is_group", "member_entity_ids")
|
|
763
|
+
|
|
764
|
+
# Every field name result_fields= accepts — base record keys plus opt-in
|
|
765
|
+
# enrichment and membership keys. Unknown names are rejected up front rather
|
|
766
|
+
# than silently projecting to empty records.
|
|
767
|
+
_ALLOWED_RESULT_FIELDS: frozenset[str] = (
|
|
768
|
+
frozenset(_ENTITY_RECORD_KEYS)
|
|
769
|
+
| frozenset(_ENRICHMENT_FIELDS)
|
|
770
|
+
| frozenset(_MEMBERSHIP_FIELDS)
|
|
747
771
|
)
|
|
748
772
|
|
|
749
773
|
|
|
@@ -790,6 +814,50 @@ def _requested_enrichment(parsed_result_fields: list[str] | None) -> tuple[str,
|
|
|
790
814
|
return tuple(f for f in _ENRICHMENT_FIELDS if f in requested)
|
|
791
815
|
|
|
792
816
|
|
|
817
|
+
def _requested_membership(parsed_result_fields: list[str] | None) -> tuple[str, ...]:
|
|
818
|
+
"""Return requested membership fields, retaining the group discriminator.
|
|
819
|
+
|
|
820
|
+
A member-only projection still includes is_group so a redacted group,
|
|
821
|
+
an empty group, and a leaf entity remain distinguishable.
|
|
822
|
+
"""
|
|
823
|
+
if not parsed_result_fields:
|
|
824
|
+
return ()
|
|
825
|
+
requested = set(parsed_result_fields)
|
|
826
|
+
if "member_entity_ids" in requested:
|
|
827
|
+
requested.add("is_group")
|
|
828
|
+
return tuple(field for field in _MEMBERSHIP_FIELDS if field in requested)
|
|
829
|
+
|
|
830
|
+
|
|
831
|
+
def _add_membership_fields(
|
|
832
|
+
record: dict[str, Any],
|
|
833
|
+
attributes: Any,
|
|
834
|
+
requested: tuple[str, ...],
|
|
835
|
+
*,
|
|
836
|
+
denied_member_ids: set[str],
|
|
837
|
+
) -> None:
|
|
838
|
+
"""Add requested generic membership metadata to one search record.
|
|
839
|
+
|
|
840
|
+
Smart-search paths may mark attributes in _redact_hidden_memberships before
|
|
841
|
+
this final projection. Callers that consume that sentinel pass an explicit
|
|
842
|
+
empty denied set.
|
|
843
|
+
"""
|
|
844
|
+
if not requested:
|
|
845
|
+
return
|
|
846
|
+
members = normalize_member_entity_ids(attributes)
|
|
847
|
+
redacted = bool(
|
|
848
|
+
isinstance(attributes, Mapping)
|
|
849
|
+
and attributes.get("_ha_mcp_membership_redacted")
|
|
850
|
+
) or bool(
|
|
851
|
+
members is not None
|
|
852
|
+
and denied_member_ids
|
|
853
|
+
and denied_member_ids.intersection(members)
|
|
854
|
+
)
|
|
855
|
+
if "is_group" in requested:
|
|
856
|
+
record["is_group"] = members is not None
|
|
857
|
+
if "member_entity_ids" in requested and members is not None and not redacted:
|
|
858
|
+
record["member_entity_ids"] = members
|
|
859
|
+
|
|
860
|
+
|
|
793
861
|
def _ws_result_map(resp: Any) -> dict[str, dict[str, Any]]:
|
|
794
862
|
"""The ``{entity_id: entry}`` map from a ``config/entity_registry/get_entries`` reply."""
|
|
795
863
|
if isinstance(resp, dict) and resp.get("success"):
|
|
@@ -974,6 +1042,11 @@ def _build_component_search_request(req: _ResolvedSearch) -> dict[str, Any]:
|
|
|
974
1042
|
"limit": req.limit,
|
|
975
1043
|
"offset": req.offset,
|
|
976
1044
|
}
|
|
1045
|
+
membership_fields = _requested_membership(
|
|
1046
|
+
_parse_component_result_fields(req.result_fields)
|
|
1047
|
+
)
|
|
1048
|
+
if membership_fields:
|
|
1049
|
+
request["result_fields"] = list(membership_fields)
|
|
977
1050
|
if req.query_text:
|
|
978
1051
|
request["query"] = req.query_text
|
|
979
1052
|
domain_filter = _normalized_domain_filter(req.domain_filter)
|
|
@@ -1061,18 +1134,22 @@ def _shape_component_search_response(
|
|
|
1061
1134
|
|
|
1062
1135
|
if req.registry_eligible:
|
|
1063
1136
|
parsed_result_fields = _parse_component_result_fields(req.result_fields)
|
|
1064
|
-
# Base
|
|
1065
|
-
#
|
|
1066
|
-
#
|
|
1067
|
-
#
|
|
1068
|
-
# survives it, while a search that requests none still emits the default
|
|
1069
|
-
# six-key shape (parity with the legacy path).
|
|
1137
|
+
# Base records contain the six documented keys. Requested enrichment
|
|
1138
|
+
# fields retain the component's area/floor/labels/aliases registry join;
|
|
1139
|
+
# requested membership fields append its opt-in state-derived metadata.
|
|
1140
|
+
# With neither class requested, the default six-key shape is unchanged.
|
|
1070
1141
|
record_keys = (
|
|
1071
1142
|
*_ENTITY_RECORD_KEYS,
|
|
1072
1143
|
*_requested_enrichment(parsed_result_fields),
|
|
1144
|
+
*_requested_membership(parsed_result_fields),
|
|
1073
1145
|
)
|
|
1146
|
+
conditional_keys = set(_requested_membership(parsed_result_fields))
|
|
1074
1147
|
entities = [
|
|
1075
|
-
{
|
|
1148
|
+
{
|
|
1149
|
+
key: rec[key] if key in conditional_keys else rec.get(key)
|
|
1150
|
+
for key in record_keys
|
|
1151
|
+
if key not in conditional_keys or key in rec
|
|
1152
|
+
}
|
|
1076
1153
|
for rec in _as_record_list(component_result.get("entities"))
|
|
1077
1154
|
]
|
|
1078
1155
|
entity_has_more = bool(component_result.get("entity_has_more", False))
|
|
@@ -1310,7 +1387,7 @@ def _build_domain_only_by_domain(
|
|
|
1310
1387
|
"""Build the by_domain dict for domain-listing mode (all results are one domain)."""
|
|
1311
1388
|
items = results[:per_domain_limit] if per_domain_limit is not None else results
|
|
1312
1389
|
if parsed_result_fields is not None:
|
|
1313
|
-
items = _project_records(items, parsed_result_fields)
|
|
1390
|
+
items = _project_records(items, _effective_result_fields(parsed_result_fields))
|
|
1314
1391
|
return {domain: items}
|
|
1315
1392
|
|
|
1316
1393
|
|
|
@@ -1491,6 +1568,9 @@ def _match_exact_search_entity(
|
|
|
1491
1568
|
visibility_hidden: set[str],
|
|
1492
1569
|
hidden_ids: set[str],
|
|
1493
1570
|
include_hidden: bool,
|
|
1571
|
+
*,
|
|
1572
|
+
membership_fields: tuple[str, ...] = (),
|
|
1573
|
+
denied_member_ids: set[str],
|
|
1494
1574
|
) -> dict[str, Any] | None:
|
|
1495
1575
|
"""Score a single entity for ``_exact_match_search``, or None if it's excluded/no match."""
|
|
1496
1576
|
entity_id = entity.get("entity_id", "")
|
|
@@ -1518,7 +1598,7 @@ def _match_exact_search_entity(
|
|
|
1518
1598
|
score = 100 if is_exact else 80
|
|
1519
1599
|
if is_hidden:
|
|
1520
1600
|
score = apply_hidden_penalty(score, "_hidden")
|
|
1521
|
-
|
|
1601
|
+
record = {
|
|
1522
1602
|
"entity_id": entity_id,
|
|
1523
1603
|
"friendly_name": friendly_name,
|
|
1524
1604
|
"domain": domain,
|
|
@@ -1526,6 +1606,13 @@ def _match_exact_search_entity(
|
|
|
1526
1606
|
"score": score,
|
|
1527
1607
|
"match_type": "exact_match",
|
|
1528
1608
|
}
|
|
1609
|
+
_add_membership_fields(
|
|
1610
|
+
record,
|
|
1611
|
+
attributes,
|
|
1612
|
+
membership_fields,
|
|
1613
|
+
denied_member_ids=denied_member_ids,
|
|
1614
|
+
)
|
|
1615
|
+
return record
|
|
1529
1616
|
|
|
1530
1617
|
|
|
1531
1618
|
async def _exact_match_search(
|
|
@@ -1539,6 +1626,7 @@ async def _exact_match_search(
|
|
|
1539
1626
|
*,
|
|
1540
1627
|
prefetched_states: list[dict[str, Any]] | None = None,
|
|
1541
1628
|
prefetched_registry: Any = None,
|
|
1629
|
+
membership_fields: tuple[str, ...] = (),
|
|
1542
1630
|
) -> dict[str, Any]:
|
|
1543
1631
|
"""
|
|
1544
1632
|
Search entities by substring on entity_id + friendly_name.
|
|
@@ -1607,6 +1695,9 @@ async def _exact_match_search(
|
|
|
1607
1695
|
)
|
|
1608
1696
|
|
|
1609
1697
|
query_lower = query.lower().strip()
|
|
1698
|
+
denied_member_ids = visibility_hidden | (
|
|
1699
|
+
hidden_ids if not include_hidden else set()
|
|
1700
|
+
)
|
|
1610
1701
|
|
|
1611
1702
|
results = []
|
|
1612
1703
|
for entity in all_entities:
|
|
@@ -1617,6 +1708,8 @@ async def _exact_match_search(
|
|
|
1617
1708
|
visibility_hidden,
|
|
1618
1709
|
hidden_ids,
|
|
1619
1710
|
include_hidden,
|
|
1711
|
+
membership_fields=membership_fields,
|
|
1712
|
+
denied_member_ids=denied_member_ids,
|
|
1620
1713
|
)
|
|
1621
1714
|
if match is not None:
|
|
1622
1715
|
results.append(match)
|
|
@@ -1808,8 +1901,15 @@ class SearchTools:
|
|
|
1808
1901
|
"Project each entity-registry record to only the specified "
|
|
1809
1902
|
'keys (e.g. ["entity_id", "state"]). None = full records. '
|
|
1810
1903
|
"Base keys: entity_id, friendly_name, domain, state, score, "
|
|
1811
|
-
"match_type. Opt-in enrichment keys (
|
|
1812
|
-
"area, floor, labels, aliases
|
|
1904
|
+
"match_type. Opt-in enrichment/membership keys (computed on request): "
|
|
1905
|
+
"area, floor, labels, aliases, is_group, member_entity_ids. "
|
|
1906
|
+
"Membership is recognized only when HA explicitly exposes a "
|
|
1907
|
+
"valid group_entities or legacy entity_id collection; "
|
|
1908
|
+
"member IDs are sorted, direct (not recursively expanded), "
|
|
1909
|
+
"and omitted if visibility/include_hidden excludes a member. "
|
|
1910
|
+
"is_group remains true when member IDs are withheld; requesting "
|
|
1911
|
+
"member_entity_ids also retains is_group. "
|
|
1912
|
+
"An unknown key is rejected."
|
|
1813
1913
|
),
|
|
1814
1914
|
),
|
|
1815
1915
|
] = None,
|
|
@@ -1871,6 +1971,13 @@ class SearchTools:
|
|
|
1871
1971
|
Use this whenever you need to find something in HA without deciding
|
|
1872
1972
|
entity-name vs config-body search up front.
|
|
1873
1973
|
|
|
1974
|
+
For control requests with exclusions such as "except", "excluding", or
|
|
1975
|
+
"but not", include `is_group` and `member_entity_ids` in `result_fields`.
|
|
1976
|
+
Do not control an aggregate whose members include an excluded entity;
|
|
1977
|
+
prefer leaf entities when the exception cannot be verified safely.
|
|
1978
|
+
A withheld member list still returns is_group=true; absence of
|
|
1979
|
+
member_entity_ids must not be interpreted as a leaf entity.
|
|
1980
|
+
|
|
1874
1981
|
When NOT to use:
|
|
1875
1982
|
- To read a known entity_id's state: use `ha_get_state` (cheaper).
|
|
1876
1983
|
- To inspect one automation/script/scene config by id: use the
|
|
@@ -1901,6 +2008,10 @@ class SearchTools:
|
|
|
1901
2008
|
Examples:
|
|
1902
2009
|
- List sensors in an area: ha_search(domain_filter="sensor", area_filter="Living Room")
|
|
1903
2010
|
- Find a light by name: ha_search("kitchen", domain_filter="light")
|
|
2011
|
+
- Find lights safely before an "all except one" control request:
|
|
2012
|
+
ha_search("living room", domain_filter="light",
|
|
2013
|
+
result_fields=["entity_id", "friendly_name", "is_group",
|
|
2014
|
+
"member_entity_ids"])
|
|
1904
2015
|
- Which automations use an entity: ha_search("light.bed_light")
|
|
1905
2016
|
- Scenes touching a light: ha_search("light.kitchen", search_types=["scene"])
|
|
1906
2017
|
- Narrow the response to the entity bucket: ha_search("kitchen", fields=["entities"])
|
|
@@ -2016,7 +2127,10 @@ class SearchTools:
|
|
|
2016
2127
|
and _component_serves_search_types(req)
|
|
2017
2128
|
):
|
|
2018
2129
|
caps = await get_component_caps(self._client)
|
|
2019
|
-
if component_supports(caps, "search")
|
|
2130
|
+
if component_supports(caps, "search") and (
|
|
2131
|
+
not _requested_membership(parsed_result_fields)
|
|
2132
|
+
or component_supports(caps, "search_entity_membership")
|
|
2133
|
+
):
|
|
2020
2134
|
(
|
|
2021
2135
|
route_component,
|
|
2022
2136
|
visibility,
|
|
@@ -2356,7 +2470,7 @@ class SearchTools:
|
|
|
2356
2470
|
'E.g. ["entity_id", "state"] returns slim entity records. '
|
|
2357
2471
|
"None = full records (default). "
|
|
2358
2472
|
"Base keys: entity_id, friendly_name, domain, state, score, match_type. "
|
|
2359
|
-
"Opt-in enrichment keys (
|
|
2473
|
+
"Opt-in enrichment/membership keys (computed on request): area, floor, labels, aliases, is_group, member_entity_ids. "
|
|
2360
2474
|
"An unknown key is rejected."
|
|
2361
2475
|
),
|
|
2362
2476
|
),
|
|
@@ -2402,6 +2516,11 @@ class SearchTools:
|
|
|
2402
2516
|
area_filter,
|
|
2403
2517
|
group_by_domain=True,
|
|
2404
2518
|
include_hidden=include_hidden_bool,
|
|
2519
|
+
**(
|
|
2520
|
+
{"include_membership": True}
|
|
2521
|
+
if _requested_membership(parsed_result_fields)
|
|
2522
|
+
else {}
|
|
2523
|
+
),
|
|
2405
2524
|
)
|
|
2406
2525
|
if query and query.strip():
|
|
2407
2526
|
area_search = await self._search_area_with_query(
|
|
@@ -2754,7 +2873,7 @@ class SearchTools:
|
|
|
2754
2873
|
entities_for_search = [
|
|
2755
2874
|
{
|
|
2756
2875
|
"entity_id": entity.get("entity_id", ""),
|
|
2757
|
-
"attributes":
|
|
2876
|
+
"attributes": entity.get("_attributes", {}),
|
|
2758
2877
|
"state": entity.get("state", "unknown"),
|
|
2759
2878
|
"_aliases": aliases_map.get(entity.get("entity_id", ""), []),
|
|
2760
2879
|
"_hidden_by": entity.get("_hidden_by"),
|
|
@@ -2780,6 +2899,21 @@ class SearchTools:
|
|
|
2780
2899
|
for match in matches
|
|
2781
2900
|
]
|
|
2782
2901
|
|
|
2902
|
+
membership_fields = _requested_membership(parsed_result_fields)
|
|
2903
|
+
attributes_by_id = {
|
|
2904
|
+
entity.get("entity_id"): entity.get("_attributes")
|
|
2905
|
+
for entity in all_area_entities
|
|
2906
|
+
}
|
|
2907
|
+
for record in results:
|
|
2908
|
+
# smart_entity_search already applied visibility redaction and
|
|
2909
|
+
# preserved its sentinel on the private attributes payload.
|
|
2910
|
+
_add_membership_fields(
|
|
2911
|
+
record,
|
|
2912
|
+
attributes_by_id.get(record["entity_id"]),
|
|
2913
|
+
membership_fields,
|
|
2914
|
+
denied_member_ids=set(),
|
|
2915
|
+
)
|
|
2916
|
+
|
|
2783
2917
|
if state_filter:
|
|
2784
2918
|
results = [r for r in results if _state_matches(r, state_filter)]
|
|
2785
2919
|
|
|
@@ -2856,10 +2990,21 @@ class SearchTools:
|
|
|
2856
2990
|
"domain": domain,
|
|
2857
2991
|
"score": apply_hidden_penalty(100, entity.get("_hidden_by")),
|
|
2858
2992
|
"match_type": "area_match",
|
|
2993
|
+
"attributes": entity.get("_attributes"),
|
|
2859
2994
|
}
|
|
2860
2995
|
for entity in entities
|
|
2861
2996
|
)
|
|
2862
2997
|
|
|
2998
|
+
membership_fields = _requested_membership(parsed_result_fields)
|
|
2999
|
+
for record in all_results:
|
|
3000
|
+
# search_entities_by_area already applied visibility redaction and
|
|
3001
|
+
# preserved its sentinel on the private attributes payload.
|
|
3002
|
+
_add_membership_fields(
|
|
3003
|
+
record,
|
|
3004
|
+
record.pop("attributes", None),
|
|
3005
|
+
membership_fields,
|
|
3006
|
+
denied_member_ids=set(),
|
|
3007
|
+
)
|
|
2863
3008
|
all_results.sort(key=lambda x: (-x["score"], x["entity_id"]))
|
|
2864
3009
|
if state_filter:
|
|
2865
3010
|
all_results = [r for r in all_results if _state_matches(r, state_filter)]
|
|
@@ -3033,6 +3178,10 @@ class SearchTools:
|
|
|
3033
3178
|
and eid not in visibility_hidden
|
|
3034
3179
|
and (include_hidden_bool or eid not in hidden_ids)
|
|
3035
3180
|
]
|
|
3181
|
+
membership_fields = _requested_membership(parsed_result_fields)
|
|
3182
|
+
denied_member_ids = visibility_hidden | (
|
|
3183
|
+
hidden_ids if not include_hidden_bool else set()
|
|
3184
|
+
)
|
|
3036
3185
|
|
|
3037
3186
|
# Score: 100 baseline for domain membership (exact, not fuzzy);
|
|
3038
3187
|
# penalised for hidden entries so they sort below visible peers.
|
|
@@ -3043,16 +3192,21 @@ class SearchTools:
|
|
|
3043
3192
|
score = apply_hidden_penalty(
|
|
3044
3193
|
100, "_hidden" if entity_id in hidden_ids else None
|
|
3045
3194
|
)
|
|
3046
|
-
|
|
3047
|
-
|
|
3048
|
-
|
|
3049
|
-
|
|
3050
|
-
|
|
3051
|
-
|
|
3052
|
-
|
|
3053
|
-
|
|
3054
|
-
|
|
3195
|
+
record = {
|
|
3196
|
+
"entity_id": entity_id,
|
|
3197
|
+
"friendly_name": attributes.get("friendly_name", entity_id),
|
|
3198
|
+
"domain": domain_filter,
|
|
3199
|
+
"state": entity.get("state", "unknown"),
|
|
3200
|
+
"score": score,
|
|
3201
|
+
"match_type": "domain_listing",
|
|
3202
|
+
}
|
|
3203
|
+
_add_membership_fields(
|
|
3204
|
+
record,
|
|
3205
|
+
attributes,
|
|
3206
|
+
membership_fields,
|
|
3207
|
+
denied_member_ids=denied_member_ids,
|
|
3055
3208
|
)
|
|
3209
|
+
scored_entities.append(record)
|
|
3056
3210
|
scored_entities.sort(key=lambda x: (-x["score"], x["entity_id"]))
|
|
3057
3211
|
if state_filter:
|
|
3058
3212
|
scored_entities = [
|
|
@@ -3122,6 +3276,10 @@ class SearchTools:
|
|
|
3122
3276
|
and _state_matches(e, state_filter)
|
|
3123
3277
|
and (include_hidden_bool or eid not in hidden_ids)
|
|
3124
3278
|
]
|
|
3279
|
+
membership_fields = _requested_membership(parsed_result_fields)
|
|
3280
|
+
denied_member_ids = visibility_hidden | (
|
|
3281
|
+
hidden_ids if not include_hidden_bool else set()
|
|
3282
|
+
)
|
|
3125
3283
|
|
|
3126
3284
|
# Score: 100 baseline for state membership (exact, not fuzzy); penalised
|
|
3127
3285
|
# for hidden entries so they sort below visible peers. ``domain`` is
|
|
@@ -3133,16 +3291,21 @@ class SearchTools:
|
|
|
3133
3291
|
score = apply_hidden_penalty(
|
|
3134
3292
|
100, "_hidden" if entity_id in hidden_ids else None
|
|
3135
3293
|
)
|
|
3136
|
-
|
|
3137
|
-
|
|
3138
|
-
|
|
3139
|
-
|
|
3140
|
-
|
|
3141
|
-
|
|
3142
|
-
|
|
3143
|
-
|
|
3144
|
-
|
|
3294
|
+
record = {
|
|
3295
|
+
"entity_id": entity_id,
|
|
3296
|
+
"friendly_name": attributes.get("friendly_name", entity_id),
|
|
3297
|
+
"domain": entity_id.split(".")[0],
|
|
3298
|
+
"state": entity.get("state", "unknown"),
|
|
3299
|
+
"score": score,
|
|
3300
|
+
"match_type": "state_listing",
|
|
3301
|
+
}
|
|
3302
|
+
_add_membership_fields(
|
|
3303
|
+
record,
|
|
3304
|
+
attributes,
|
|
3305
|
+
membership_fields,
|
|
3306
|
+
denied_member_ids=denied_member_ids,
|
|
3145
3307
|
)
|
|
3308
|
+
scored_entities.append(record)
|
|
3146
3309
|
scored_entities.sort(key=lambda x: (-x["score"], x["entity_id"]))
|
|
3147
3310
|
results = scored_entities[offset : offset + limit]
|
|
3148
3311
|
|
|
@@ -3216,6 +3379,7 @@ class SearchTools:
|
|
|
3216
3379
|
state_filter=state_filter,
|
|
3217
3380
|
prefetched_states=prefetched_states,
|
|
3218
3381
|
prefetched_registry=prefetched_registry,
|
|
3382
|
+
membership_fields=_requested_membership(parsed_result_fields),
|
|
3219
3383
|
)
|
|
3220
3384
|
else:
|
|
3221
3385
|
# Fuzzy mode: BM25 → substring fallback on exception only.
|
|
@@ -3226,6 +3390,14 @@ class SearchTools:
|
|
|
3226
3390
|
offset=offset,
|
|
3227
3391
|
domain_filter=domain_filter,
|
|
3228
3392
|
include_hidden=include_hidden_bool,
|
|
3393
|
+
include_attributes=bool(
|
|
3394
|
+
_requested_membership(parsed_result_fields)
|
|
3395
|
+
),
|
|
3396
|
+
**(
|
|
3397
|
+
{"include_membership": True}
|
|
3398
|
+
if _requested_membership(parsed_result_fields)
|
|
3399
|
+
else {}
|
|
3400
|
+
),
|
|
3229
3401
|
prefetched_states=prefetched_states,
|
|
3230
3402
|
prefetched_registry=prefetched_registry,
|
|
3231
3403
|
)
|
|
@@ -3251,6 +3423,7 @@ class SearchTools:
|
|
|
3251
3423
|
state_filter=state_filter,
|
|
3252
3424
|
prefetched_states=prefetched_states,
|
|
3253
3425
|
prefetched_registry=prefetched_registry,
|
|
3426
|
+
membership_fields=_requested_membership(parsed_result_fields),
|
|
3254
3427
|
)
|
|
3255
3428
|
warning = "Fuzzy search unavailable, using substring match"
|
|
3256
3429
|
search_type = "exact_match"
|
|
@@ -3258,6 +3431,17 @@ class SearchTools:
|
|
|
3258
3431
|
_normalize_regular_search_result(
|
|
3259
3432
|
result, search_type, domain_filter, offset, limit
|
|
3260
3433
|
)
|
|
3434
|
+
if search_type == "fuzzy_search":
|
|
3435
|
+
membership_fields = _requested_membership(parsed_result_fields)
|
|
3436
|
+
for record in result.get("results", []):
|
|
3437
|
+
# smart_entity_search already applied visibility redaction and
|
|
3438
|
+
# preserved its sentinel on the private attributes payload.
|
|
3439
|
+
_add_membership_fields(
|
|
3440
|
+
record,
|
|
3441
|
+
record.pop("attributes", None),
|
|
3442
|
+
membership_fields,
|
|
3443
|
+
denied_member_ids=set(),
|
|
3444
|
+
)
|
|
3261
3445
|
|
|
3262
3446
|
# Apply state_filter to fuzzy results BEFORE grouping so by_domain
|
|
3263
3447
|
# stays consistent with results[]. For fuzzy_search, state_filter is
|