ha-mcp-dev 7.13.0.dev1959__tar.gz → 7.13.0.dev1965__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.13.0.dev1959/src/ha_mcp_dev.egg-info → ha_mcp_dev-7.13.0.dev1965}/PKG-INFO +1 -1
- {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/pyproject.toml +1 -1
- {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/backup_manager.py +13 -2
- {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/client/rest_client.py +9 -1
- ha_mcp_dev-7.13.0.dev1965/src/ha_mcp/tools/component_devices.py +153 -0
- {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/tools/radio/base.py +12 -3
- {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/tools/radio/zigbee.py +28 -4
- {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/tools/tools_blueprints.py +90 -6
- {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/tools/tools_entities.py +144 -22
- {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/tools/tools_registry.py +216 -57
- {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/tools/tools_search.py +495 -33
- {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/tools/tools_voice_assistant.py +115 -0
- {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965/src/ha_mcp_dev.egg-info}/PKG-INFO +1 -1
- {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp_dev.egg-info/SOURCES.txt +1 -0
- {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/LICENSE +0 -0
- {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/MANIFEST.in +0 -0
- {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/README.md +0 -0
- {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/setup.cfg +0 -0
- {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/__init__.py +0 -0
- {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/__main__.py +0 -0
- {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/_pypi_marker +0 -0
- {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/_version.py +0 -0
- {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/auth/__init__.py +0 -0
- {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/auth/consent_form.py +0 -0
- {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/auth/provider.py +0 -0
- {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/browser_landing.py +0 -0
- {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/client/__init__.py +0 -0
- {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/client/supervisor_client.py +0 -0
- {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/client/websocket_client.py +0 -0
- {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/client/websocket_listener.py +0 -0
- {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/config.py +0 -0
- {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/dashboard_screenshot/__init__.py +0 -0
- {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/dashboard_screenshot/capture.py +0 -0
- {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/dashboard_screenshot/content.py +0 -0
- {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/dashboard_screenshot/paths.py +0 -0
- {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/dashboard_screenshot/provision.py +0 -0
- {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/errors.py +0 -0
- {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/llm_exposure.py +0 -0
- {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/policy/__init__.py +0 -0
- {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/policy/approval_queue.py +0 -0
- {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/policy/evaluator.py +0 -0
- {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/policy/handlers.py +0 -0
- {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/policy/middleware.py +0 -0
- {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/policy/model.py +0 -0
- {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/policy/persistence.py +0 -0
- {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/policy/value_sources.py +0 -0
- {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/py.typed +0 -0
- {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/read_only.py +0 -0
- {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/resources/skills-vendor/.claude/settings.json +0 -0
- {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/resources/skills-vendor/.claude-plugin/marketplace.json +0 -0
- {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/resources/skills-vendor/.claude-plugin/plugin.json +0 -0
- {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/resources/skills-vendor/.github/ISSUE_TEMPLATE/skill-rca.md +0 -0
- {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/resources/skills-vendor/.github/pull_request_template.md +0 -0
- {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/resources/skills-vendor/AGENTS.md +0 -0
- {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/resources/skills-vendor/CLAUDE.md +0 -0
- {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/resources/skills-vendor/CONTRIBUTING.md +0 -0
- {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/resources/skills-vendor/LICENSE +0 -0
- {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/resources/skills-vendor/README.md +0 -0
- {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/SKILL.md +0 -0
- {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/evals/evals.json +0 -0
- {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/appdaemon.md +0 -0
- {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/automation-patterns.md +0 -0
- {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/blueprint-guide.md +0 -0
- {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-cards.md +0 -0
- {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-guide.md +0 -0
- {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/device-control.md +0 -0
- {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/domain-docs.md +0 -0
- {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/examples.yaml +0 -0
- {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/helper-selection.md +0 -0
- {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/safe-refactoring.md +0 -0
- {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/scenes.md +0 -0
- {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/template-guidelines.md +0 -0
- {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/yaml-only-integrations.md +0 -0
- {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/server.py +0 -0
- {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/settings_ui/__init__.py +0 -0
- {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/settings_ui/_handlers_advanced.py +0 -0
- {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/settings_ui/_handlers_backups.py +0 -0
- {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/settings_ui/_handlers_fs.py +0 -0
- {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/settings_ui/_handlers_server.py +0 -0
- {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/settings_ui/_handlers_theme.py +0 -0
- {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/settings_ui/_handlers_tools.py +0 -0
- {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/settings_ui/_persistence.py +0 -0
- {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/settings_ui/_supervisor.py +0 -0
- {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/settings_ui/_theme.py +0 -0
- {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/settings_ui/_tools_meta.py +0 -0
- {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/settings_ui/settings.css +0 -0
- {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/settings_ui/settings.html +0 -0
- {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/settings_ui/settings.js +0 -0
- {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/smoke_test.py +0 -0
- {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/stdio_settings_sidecar.py +0 -0
- {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/strict_bps.py +0 -0
- {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/tools/__init__.py +0 -0
- {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/tools/auto_backup.py +0 -0
- {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/tools/backup.py +0 -0
- {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/tools/best_practice_checker.py +0 -0
- {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/tools/component_api.py +0 -0
- {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/tools/config_entry_flow.py +0 -0
- {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/tools/device_control.py +0 -0
- {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/tools/enhanced.py +0 -0
- {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/tools/helpers.py +0 -0
- {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/tools/radio/__init__.py +0 -0
- {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/tools/radio/matter.py +0 -0
- {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/tools/radio/thread.py +0 -0
- {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/tools/radio/zwave.py +0 -0
- {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/tools/reference_validator.py +0 -0
- {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/tools/registry.py +0 -0
- {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/tools/smart_search/__init__.py +0 -0
- {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/tools/smart_search/_base.py +0 -0
- {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/tools/smart_search/_config.py +0 -0
- {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/tools/smart_search/_deep.py +0 -0
- {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/tools/smart_search/_entities.py +0 -0
- {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/tools/smart_search/_fetch.py +0 -0
- {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/tools/smart_search/_overview.py +0 -0
- {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/tools/smart_search/_scenes.py +0 -0
- {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/tools/smart_search/_scoring.py +0 -0
- {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/tools/tool_search_hint_middleware.py +0 -0
- {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/tools/tools_addons.py +0 -0
- {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/tools/tools_areas.py +0 -0
- {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/tools/tools_bug_report.py +0 -0
- {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/tools/tools_calendar.py +0 -0
- {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/tools/tools_camera.py +0 -0
- {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/tools/tools_categories.py +0 -0
- {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/tools/tools_code.py +0 -0
- {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/tools/tools_config_automations.py +0 -0
- {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/tools/tools_config_dashboards.py +0 -0
- {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/tools/tools_config_helpers.py +0 -0
- {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/tools/tools_config_scenes.py +0 -0
- {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/tools/tools_config_scripts.py +0 -0
- {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/tools/tools_dashboard_screenshot.py +0 -0
- {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/tools/tools_dev.py +0 -0
- {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/tools/tools_energy.py +0 -0
- {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/tools/tools_filesystem.py +0 -0
- {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/tools/tools_groups.py +0 -0
- {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/tools/tools_hacs.py +0 -0
- {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/tools/tools_history.py +0 -0
- {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/tools/tools_integrations.py +0 -0
- {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/tools/tools_labels.py +0 -0
- {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/tools/tools_radio.py +0 -0
- {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/tools/tools_resources.py +0 -0
- {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/tools/tools_service.py +0 -0
- {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/tools/tools_services.py +0 -0
- {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/tools/tools_system.py +0 -0
- {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/tools/tools_themes.py +0 -0
- {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/tools/tools_todo.py +0 -0
- {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/tools/tools_traces.py +0 -0
- {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/tools/tools_updates.py +0 -0
- {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/tools/tools_utility.py +0 -0
- {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/tools/tools_yaml_config.py +0 -0
- {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/tools/tools_yaml_read.py +0 -0
- {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/tools/tools_zones.py +0 -0
- {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/tools/util_helpers.py +0 -0
- {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/tools/validation_middleware.py +0 -0
- {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/transforms/__init__.py +0 -0
- {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/transforms/categorized_search.py +0 -0
- {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/transforms/lite_docstrings.py +0 -0
- {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/transport_security.py +0 -0
- {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/update_check.py +0 -0
- {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/utils/__init__.py +0 -0
- {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/utils/config_hash.py +0 -0
- {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/utils/data_paths.py +0 -0
- {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/utils/domain_handlers.py +0 -0
- {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/utils/fuzzy_search.py +0 -0
- {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/utils/kill_signal_diagnostics.py +0 -0
- {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/utils/operation_manager.py +0 -0
- {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/utils/python_sandbox.py +0 -0
- {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/utils/skill_loader.py +0 -0
- {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/utils/usage_logger.py +0 -0
- {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/visibility/__init__.py +0 -0
- {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/visibility/model.py +0 -0
- {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/visibility/persistence.py +0 -0
- {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/visibility/resolver.py +0 -0
- {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp_dev.egg-info/dependency_links.txt +0 -0
- {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp_dev.egg-info/entry_points.txt +0 -0
- {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp_dev.egg-info/requires.txt +0 -0
- {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp_dev.egg-info/top_level.txt +0 -0
- {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/tests/__init__.py +0 -0
- {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/tests/test_constants.py +0 -0
- {ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/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 = "7.13.0.
|
|
7
|
+
version = "7.13.0.dev1965"
|
|
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
|
|
@@ -1720,12 +1720,23 @@ async def _restore_entity_state(client: Any, entity_id: str, config: Any) -> Any
|
|
|
1720
1720
|
# best-effort.
|
|
1721
1721
|
|
|
1722
1722
|
|
|
1723
|
-
async def _fetch_device(client: Any,
|
|
1723
|
+
async def _fetch_device(client: Any, device_id: str) -> Any:
|
|
1724
|
+
# Route the single-device capture through the component's ``device_get`` when
|
|
1725
|
+
# available (one in-process read of the raw DeviceEntry) instead of dumping the
|
|
1726
|
+
# whole registry — the same pre-write snapshot ``ha_set_device`` /
|
|
1727
|
+
# ``ha_remove_device`` capture. Lazy import to avoid the backup_manager →
|
|
1728
|
+
# tools → backup_manager cycle. ``None`` from the helper means "component
|
|
1729
|
+
# unavailable"; fall back to the full-list scan.
|
|
1730
|
+
from .tools.component_devices import fetch_device_via_component
|
|
1731
|
+
|
|
1732
|
+
result = await fetch_device_via_component(client, device_id)
|
|
1733
|
+
if result is not None:
|
|
1734
|
+
return result.get("device")
|
|
1724
1735
|
items = await _ws_send(client, {"type": "config/device_registry/list"})
|
|
1725
1736
|
if not isinstance(items, list):
|
|
1726
1737
|
return None
|
|
1727
1738
|
for item in items:
|
|
1728
|
-
if item.get("id") ==
|
|
1739
|
+
if item.get("id") == device_id:
|
|
1729
1740
|
return item
|
|
1730
1741
|
return None
|
|
1731
1742
|
|
|
@@ -1445,6 +1445,7 @@ class HomeAssistantClient:
|
|
|
1445
1445
|
return {
|
|
1446
1446
|
"success": False,
|
|
1447
1447
|
"error": f"WebSocket request blocked (403 Forbidden): {error_str}",
|
|
1448
|
+
"error_code": getattr(e, "code", None),
|
|
1448
1449
|
"suggestions": [
|
|
1449
1450
|
"This may be caused by a reverse proxy or security filter",
|
|
1450
1451
|
"Try simplifying the request (e.g., shorter templates, fewer parameters)",
|
|
@@ -1457,7 +1458,14 @@ class HomeAssistantClient:
|
|
|
1457
1458
|
# Unknown command." is undiagnosable from a user's log
|
|
1458
1459
|
# (issue #1889 took a live reproduction to attribute).
|
|
1459
1460
|
logger.error(f"WebSocket message failed ({message.get('type')}): {e}")
|
|
1460
|
-
|
|
1461
|
+
# Preserve HA's structured error code (e.g. ``not_found``) so callers
|
|
1462
|
+
# can distinguish HA's authoritative verdict from a transient failure
|
|
1463
|
+
# instead of only seeing the stringified message.
|
|
1464
|
+
return {
|
|
1465
|
+
"success": False,
|
|
1466
|
+
"error": str(e),
|
|
1467
|
+
"error_code": getattr(e, "code", None),
|
|
1468
|
+
}
|
|
1461
1469
|
|
|
1462
1470
|
return {"success": False, "error": "WebSocket request failed"}
|
|
1463
1471
|
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
"""Shared device-registry reads over the ``ha_mcp_tools`` component gate.
|
|
2
|
+
|
|
3
|
+
Several tools resolve a device the same wasteful way today — pull the ENTIRE
|
|
4
|
+
device registry and filter for one entry (``ha_get_device`` single lookup,
|
|
5
|
+
``ha_remove_device``'s body, the ``@with_auto_backup`` capture read, and
|
|
6
|
+
``ha_manage_radio``'s ``_resolve_ieee``). When the component advertises the
|
|
7
|
+
``device_get`` / ``device_list`` capabilities, that whole-registry dump becomes a
|
|
8
|
+
single in-process read: ``device_get`` returns one ``DeviceEntry.dict_repr`` by
|
|
9
|
+
id, ``device_list`` returns them all — each byte-identical to a
|
|
10
|
+
``config/device_registry/list`` element by construction (see
|
|
11
|
+
``custom_components/ha_mcp_tools/websocket_api.py``). Consumers keep their own
|
|
12
|
+
transforms over that raw shape.
|
|
13
|
+
|
|
14
|
+
This module owns the caps-gated fetch so the routing discipline — probe caps,
|
|
15
|
+
send one frame, invalidate on ``unknown_command``, fall back to the legacy path
|
|
16
|
+
on any component error — lives in one place instead of being duplicated per
|
|
17
|
+
consumer (the pattern ``tools_search._fetch_states_via_component`` established
|
|
18
|
+
for the ``states`` capability). Both helpers return ``None`` to mean "component
|
|
19
|
+
unavailable — use the legacy path"; a component that answers authoritatively
|
|
20
|
+
returns its payload (with ``device`` possibly ``None`` for "no such device").
|
|
21
|
+
|
|
22
|
+
``device_get`` also carries an opt-in per-device entity join
|
|
23
|
+
(:func:`fetch_device_entities_via_component`): with ``include_entities`` the
|
|
24
|
+
component returns the device's ``config/entity_registry/list``-shaped rows as a
|
|
25
|
+
SIBLING ``entities`` key, so a single-device lookup no longer dumps the whole
|
|
26
|
+
entity registry to list one device's entities. The join is additive within
|
|
27
|
+
schema_version 1, so the server tolerates its absence rather than depending on a
|
|
28
|
+
version bump. An older ``device_get`` that predates the param never round-trips
|
|
29
|
+
the entities half — the extra field is rejected by the command's
|
|
30
|
+
``PREVENT_EXTRA`` base schema, surfacing as an error that maps to the ``None``
|
|
31
|
+
miss — and, belt-and-suspenders, a response that carries the device but no
|
|
32
|
+
``entities`` key is treated the same: fall back to the legacy
|
|
33
|
+
``config/entity_registry/list`` for the entity half. Neither breaks the call.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
from __future__ import annotations
|
|
37
|
+
|
|
38
|
+
import logging
|
|
39
|
+
from typing import Any
|
|
40
|
+
|
|
41
|
+
from ..client.rest_client import (
|
|
42
|
+
HomeAssistantCommandError,
|
|
43
|
+
HomeAssistantCommandTimeout,
|
|
44
|
+
)
|
|
45
|
+
from ..client.websocket_client import get_websocket_client
|
|
46
|
+
from .component_api import (
|
|
47
|
+
component_supports,
|
|
48
|
+
get_component_caps,
|
|
49
|
+
invalidate_caps,
|
|
50
|
+
is_unknown_command,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
logger = logging.getLogger(__name__)
|
|
54
|
+
|
|
55
|
+
WS_DEVICE_GET = "ha_mcp_tools/device_get"
|
|
56
|
+
WS_DEVICE_LIST = "ha_mcp_tools/device_list"
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
async def fetch_device_via_component(
|
|
60
|
+
client: Any, device_id: str, *, include_entities: bool = False
|
|
61
|
+
) -> dict[str, Any] | None:
|
|
62
|
+
"""One ``ha_mcp_tools/device_get`` read; ``None`` ⇒ use the legacy path.
|
|
63
|
+
|
|
64
|
+
Returns the component's ``{"device": <raw dict> | None}`` payload (the raw
|
|
65
|
+
``DeviceEntry.dict_repr`` for the id, byte-identical to a
|
|
66
|
+
``config/device_registry/list`` element) or ``None`` when the component lacks
|
|
67
|
+
the ``device_get`` capability, was downgraded (``unknown_command`` →
|
|
68
|
+
invalidate the cached caps), or errored (logged). A component that answers
|
|
69
|
+
with ``{"device": None}`` is authoritative — the device does not exist — so
|
|
70
|
+
the caller must distinguish that from a ``None`` return (which means "component
|
|
71
|
+
unavailable, fall back"). Falls back **silently**, mirroring
|
|
72
|
+
``ha_get_state``: the legacy path returns the byte-identical device either way
|
|
73
|
+
and the ``log.warning`` preserves operator visibility. A
|
|
74
|
+
``HomeAssistantConnectionError`` (WS down) is not caught here, so it
|
|
75
|
+
propagates to the caller's own error handling — the legacy path shares the
|
|
76
|
+
same socket and would fail identically.
|
|
77
|
+
|
|
78
|
+
With ``include_entities`` the payload also carries a sibling ``entities`` list
|
|
79
|
+
(the device's ``config/entity_registry/list``-shaped rows), so a single-device
|
|
80
|
+
lookup that needs the device's entities skips the whole-entity-registry dump.
|
|
81
|
+
``include_entities`` is only sent when true, so the frames of callers that need
|
|
82
|
+
only the device (``_resolve_ieee`` / capture / remove) are unchanged.
|
|
83
|
+
"""
|
|
84
|
+
caps = await get_component_caps(client)
|
|
85
|
+
if not component_supports(caps, "device_get"):
|
|
86
|
+
return None
|
|
87
|
+
kwargs: dict[str, Any] = {"device_id": device_id}
|
|
88
|
+
if include_entities:
|
|
89
|
+
kwargs["include_entities"] = True
|
|
90
|
+
try:
|
|
91
|
+
ws = await get_websocket_client(url=client.base_url, token=client.token)
|
|
92
|
+
raw = await ws.send_command(WS_DEVICE_GET, **kwargs)
|
|
93
|
+
except (HomeAssistantCommandError, HomeAssistantCommandTimeout) as exc:
|
|
94
|
+
if is_unknown_command(exc):
|
|
95
|
+
invalidate_caps(client)
|
|
96
|
+
else:
|
|
97
|
+
logger.warning("%s failed; fell back to legacy: %r", WS_DEVICE_GET, exc)
|
|
98
|
+
return None
|
|
99
|
+
result = raw.get("result")
|
|
100
|
+
if not isinstance(result, dict) or "device" not in result:
|
|
101
|
+
return None
|
|
102
|
+
return result
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
async def fetch_device_entities_via_component(
|
|
106
|
+
client: Any, device_id: str
|
|
107
|
+
) -> list[dict[str, Any]] | None:
|
|
108
|
+
"""The device's entity rows via ``device_get(include_entities=True)``; ``None`` ⇒ legacy.
|
|
109
|
+
|
|
110
|
+
Returns the ``config/entity_registry/list``-shaped rows bound to ``device_id``
|
|
111
|
+
(disabled included) so a per-device entity lookup avoids the whole-registry
|
|
112
|
+
dump. Returns ``None`` — the caller falls back to
|
|
113
|
+
``config/entity_registry/list`` — when the component can't serve ``device_get``
|
|
114
|
+
at all, OR when it served the device but the response carries no ``entities``
|
|
115
|
+
key (an older ``device_get`` predating ``include_entities``, or any component
|
|
116
|
+
that does not round-trip the entities half): the entity join is additive, so
|
|
117
|
+
its absence degrades to legacy rather than silently reporting zero entities.
|
|
118
|
+
An AUTHORITATIVE empty result — the component honored ``include_entities`` and
|
|
119
|
+
the device has no entities, or no such device — comes back as a present empty
|
|
120
|
+
list, kept distinct from that ``None`` miss.
|
|
121
|
+
"""
|
|
122
|
+
result = await fetch_device_via_component(client, device_id, include_entities=True)
|
|
123
|
+
if result is None:
|
|
124
|
+
return None
|
|
125
|
+
entities = result.get("entities")
|
|
126
|
+
return entities if isinstance(entities, list) else None
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
async def fetch_device_list_via_component(client: Any) -> dict[str, Any] | None:
|
|
130
|
+
"""One ``ha_mcp_tools/device_list`` read; ``None`` ⇒ use the legacy path.
|
|
131
|
+
|
|
132
|
+
Returns the component's ``{"devices": [<raw dict>, ...]}`` payload (each a raw
|
|
133
|
+
``DeviceEntry.dict_repr``, the in-process equivalent of
|
|
134
|
+
``config/device_registry/list``) or ``None`` on capability miss, downgrade
|
|
135
|
+
(``unknown_command`` → invalidate caps), or error (logged) — same
|
|
136
|
+
error-taxonomy and silent fallback as :func:`fetch_device_via_component`.
|
|
137
|
+
"""
|
|
138
|
+
caps = await get_component_caps(client)
|
|
139
|
+
if not component_supports(caps, "device_list"):
|
|
140
|
+
return None
|
|
141
|
+
try:
|
|
142
|
+
ws = await get_websocket_client(url=client.base_url, token=client.token)
|
|
143
|
+
raw = await ws.send_command(WS_DEVICE_LIST)
|
|
144
|
+
except (HomeAssistantCommandError, HomeAssistantCommandTimeout) as exc:
|
|
145
|
+
if is_unknown_command(exc):
|
|
146
|
+
invalidate_caps(client)
|
|
147
|
+
else:
|
|
148
|
+
logger.warning("%s failed; fell back to legacy: %r", WS_DEVICE_LIST, exc)
|
|
149
|
+
return None
|
|
150
|
+
result = raw.get("result")
|
|
151
|
+
if not isinstance(result, dict) or not isinstance(result.get("devices"), list):
|
|
152
|
+
return None
|
|
153
|
+
return result
|
|
@@ -21,6 +21,7 @@ from dataclasses import dataclass, field
|
|
|
21
21
|
from typing import Any
|
|
22
22
|
|
|
23
23
|
from ...errors import ErrorCode, create_error_response
|
|
24
|
+
from ..component_devices import fetch_device_entities_via_component
|
|
24
25
|
from ..helpers import raise_tool_error
|
|
25
26
|
|
|
26
27
|
logger = logging.getLogger(__name__)
|
|
@@ -107,10 +108,18 @@ async def resolve_update_entity(
|
|
|
107
108
|
Filters the ``update.*`` entities tied to ``device_id``, preferring the given
|
|
108
109
|
``platform`` (e.g. "matter", "zha", "zwave_js") when a device exposes more
|
|
109
110
|
than one. Raises ENTITY_NOT_FOUND when the device exposes no update entity.
|
|
111
|
+
|
|
112
|
+
Routes the per-device entity read through the component's
|
|
113
|
+
``device_get(include_entities=True)`` when available (the device's rows in one
|
|
114
|
+
in-process frame, ``config/entity_registry/list`` shape) instead of dumping the
|
|
115
|
+
whole entity registry; falls back to ``config/entity_registry/list`` otherwise.
|
|
116
|
+
The ``update.*`` / platform filtering stays client-side exactly as before.
|
|
110
117
|
"""
|
|
111
|
-
entities = await
|
|
112
|
-
|
|
113
|
-
|
|
118
|
+
entities = await fetch_device_entities_via_component(client, device_id)
|
|
119
|
+
if entities is None:
|
|
120
|
+
entities = await ws_call(
|
|
121
|
+
client, "config/entity_registry/list", context={"device_id": device_id}
|
|
122
|
+
)
|
|
114
123
|
candidates = [
|
|
115
124
|
e
|
|
116
125
|
for e in (entities or [])
|
|
@@ -19,6 +19,7 @@ from typing import Any
|
|
|
19
19
|
from fastmcp.exceptions import ToolError
|
|
20
20
|
|
|
21
21
|
from ...errors import ErrorCode, create_error_response
|
|
22
|
+
from ..component_devices import fetch_device_via_component
|
|
22
23
|
from ..helpers import raise_tool_error
|
|
23
24
|
from .base import (
|
|
24
25
|
ActionSpec,
|
|
@@ -123,17 +124,40 @@ SUPPORTED: dict[str, ActionSpec] = {
|
|
|
123
124
|
}
|
|
124
125
|
|
|
125
126
|
|
|
127
|
+
async def _resolve_ieee_devices(client: Any, device_id: Any) -> list[dict[str, Any]]:
|
|
128
|
+
"""The device rows ``_resolve_ieee`` scans — component ``device_get`` or legacy.
|
|
129
|
+
|
|
130
|
+
Returns just the one matching device (``[device]``, or ``[]`` when it does not
|
|
131
|
+
exist) via ``ha_mcp_tools/device_get`` when the component serves it, else the
|
|
132
|
+
whole ``config/device_registry/list`` for the caller to filter. Only a
|
|
133
|
+
non-empty string ``device_id`` is eligible for the component read; anything
|
|
134
|
+
else takes the legacy path (and is rejected by the caller's scan).
|
|
135
|
+
"""
|
|
136
|
+
if isinstance(device_id, str) and device_id:
|
|
137
|
+
result = await fetch_device_via_component(client, device_id)
|
|
138
|
+
if result is not None:
|
|
139
|
+
device = result.get("device")
|
|
140
|
+
return [device] if isinstance(device, dict) else []
|
|
141
|
+
devices = await ws_call(
|
|
142
|
+
client, "config/device_registry/list", context={"device_id": device_id}
|
|
143
|
+
)
|
|
144
|
+
return list(devices or [])
|
|
145
|
+
|
|
146
|
+
|
|
126
147
|
async def _resolve_ieee(client: Any, device_id: Any) -> str:
|
|
127
148
|
"""Resolve a ``device_id`` to its ZHA IEEE address via the device registry.
|
|
128
149
|
|
|
129
150
|
Parses the ``["zha", "<ieee>"]`` registry identifier (falling back to an
|
|
130
151
|
``("ieee", ...)`` connection). Raises VALIDATION_INVALID_PARAMETER when the
|
|
131
152
|
device is not a ZHA device.
|
|
153
|
+
|
|
154
|
+
Routes the single-device lookup through the component's ``device_get`` when
|
|
155
|
+
available (one in-process read of the raw ``DeviceEntry`` shape) instead of
|
|
156
|
+
dumping the whole device registry; falls back to
|
|
157
|
+
``config/device_registry/list`` when the component can't serve it.
|
|
132
158
|
"""
|
|
133
|
-
devices = await
|
|
134
|
-
|
|
135
|
-
)
|
|
136
|
-
for device in devices or []:
|
|
159
|
+
devices = await _resolve_ieee_devices(client, device_id)
|
|
160
|
+
for device in devices:
|
|
137
161
|
if device.get("id") != device_id:
|
|
138
162
|
continue
|
|
139
163
|
for ident in device.get("identifiers", []):
|
{ha_mcp_dev-7.13.0.dev1959 → ha_mcp_dev-7.13.0.dev1965}/src/ha_mcp/tools/tools_blueprints.py
RENAMED
|
@@ -12,7 +12,18 @@ from fastmcp.exceptions import ToolError
|
|
|
12
12
|
from fastmcp.tools import tool
|
|
13
13
|
from pydantic import Field
|
|
14
14
|
|
|
15
|
+
from ..client.rest_client import (
|
|
16
|
+
HomeAssistantCommandError,
|
|
17
|
+
HomeAssistantCommandTimeout,
|
|
18
|
+
)
|
|
19
|
+
from ..client.websocket_client import get_websocket_client
|
|
15
20
|
from ..errors import ErrorCode, create_error_response
|
|
21
|
+
from .component_api import (
|
|
22
|
+
component_supports,
|
|
23
|
+
get_component_caps,
|
|
24
|
+
invalidate_caps,
|
|
25
|
+
is_unknown_command,
|
|
26
|
+
)
|
|
16
27
|
from .helpers import (
|
|
17
28
|
exception_to_structured_error,
|
|
18
29
|
log_tool_usage,
|
|
@@ -105,8 +116,11 @@ class BlueprintTools:
|
|
|
105
116
|
Get blueprint information - list all blueprints or get details for a specific one.
|
|
106
117
|
|
|
107
118
|
Without a path: Lists all installed blueprints for the specified domain.
|
|
108
|
-
With a path:
|
|
109
|
-
conditions,
|
|
119
|
+
With a path: Returns the blueprint's metadata and input definitions. The
|
|
120
|
+
full body (triggers/conditions/actions for automations, sequence for
|
|
121
|
+
scripts) is included under `config` ONLY when the ha_mcp_tools custom
|
|
122
|
+
component is installed — core's blueprint API exposes metadata alone, so
|
|
123
|
+
without the component the body cannot be read.
|
|
110
124
|
|
|
111
125
|
EXAMPLES:
|
|
112
126
|
- List all automation blueprints: ha_get_blueprint(domain="automation")
|
|
@@ -120,7 +134,8 @@ class BlueprintTools:
|
|
|
120
134
|
RETURNS (when getting specific blueprint):
|
|
121
135
|
- Blueprint metadata (name, description, author, source_url)
|
|
122
136
|
- Input definitions with selectors and defaults
|
|
123
|
-
-
|
|
137
|
+
- `config`: the full parsed blueprint body (only with the ha_mcp_tools
|
|
138
|
+
component; `!input` substitution points appear as `{"__input__": name}`)
|
|
124
139
|
"""
|
|
125
140
|
try:
|
|
126
141
|
# Validate domain
|
|
@@ -202,9 +217,11 @@ class BlueprintTools:
|
|
|
202
217
|
if "input" in meta:
|
|
203
218
|
result["inputs"] = meta["input"]
|
|
204
219
|
|
|
205
|
-
#
|
|
206
|
-
|
|
207
|
-
|
|
220
|
+
# Core's blueprint/list returns metadata only (never a body), so the
|
|
221
|
+
# full triggers/conditions/actions/sequence come from the ha_mcp_tools
|
|
222
|
+
# component when installed. Merge it additively under `config`; without
|
|
223
|
+
# the component the response stays metadata + inputs.
|
|
224
|
+
await self._merge_blueprint_config(result, domain, path)
|
|
208
225
|
|
|
209
226
|
return result
|
|
210
227
|
|
|
@@ -222,6 +239,73 @@ class BlueprintTools:
|
|
|
222
239
|
)
|
|
223
240
|
return None # unreachable: exception_to_structured_error always raises
|
|
224
241
|
|
|
242
|
+
async def _merge_blueprint_config(
|
|
243
|
+
self, result: dict[str, Any], domain: str, path: str
|
|
244
|
+
) -> None:
|
|
245
|
+
"""Fetch the component-served blueprint body and merge it into ``result``.
|
|
246
|
+
|
|
247
|
+
Adds ``config`` when the body was read, or a top-level ``warnings`` entry
|
|
248
|
+
when a present component returned an unreadable body; a metadata-only
|
|
249
|
+
outcome (no component / capability) leaves ``result`` untouched.
|
|
250
|
+
"""
|
|
251
|
+
config, config_warning = await self._blueprint_config_via_component(
|
|
252
|
+
domain, path
|
|
253
|
+
)
|
|
254
|
+
if config is not None:
|
|
255
|
+
result["config"] = config
|
|
256
|
+
elif config_warning is not None:
|
|
257
|
+
result.setdefault("warnings", []).append(config_warning)
|
|
258
|
+
|
|
259
|
+
async def _blueprint_config_via_component(
|
|
260
|
+
self, domain: str, path: str
|
|
261
|
+
) -> tuple[dict[str, Any] | None, str | None]:
|
|
262
|
+
"""Fetch a blueprint's full parsed body via the component.
|
|
263
|
+
|
|
264
|
+
core's ``blueprint/list`` returns only ``{metadata}`` (no body), so
|
|
265
|
+
without the component ``ha_get_blueprint`` can serve metadata + inputs
|
|
266
|
+
only. When the component advertises ``blueprint_get`` it reads the on-disk
|
|
267
|
+
blueprint file (path-jailed, executor-offloaded) and returns the full
|
|
268
|
+
parsed body, merged additively under ``config``. Returns
|
|
269
|
+
``(config, warning)``:
|
|
270
|
+
|
|
271
|
+
- ``(dict, None)`` — the parsed body was read.
|
|
272
|
+
- ``(None, None)`` — metadata-only is the expected outcome: the component
|
|
273
|
+
is absent / lacks the capability, was downgraded (``unknown_command`` →
|
|
274
|
+
invalidate the cached caps), or errored (logged).
|
|
275
|
+
- ``(None, warning)`` — the component is present and the server has already
|
|
276
|
+
confirmed the path is a real installed blueprint, yet it returned a null
|
|
277
|
+
``config`` (corrupt / unparseable file, read error). Metadata-only would
|
|
278
|
+
otherwise be indistinguishable from component-not-installed, so a
|
|
279
|
+
top-level warning is surfaced instead.
|
|
280
|
+
"""
|
|
281
|
+
caps = await get_component_caps(self._client)
|
|
282
|
+
if not component_supports(caps, "blueprint_get"):
|
|
283
|
+
return None, None
|
|
284
|
+
try:
|
|
285
|
+
ws = await get_websocket_client(
|
|
286
|
+
url=self._client.base_url, token=self._client.token
|
|
287
|
+
)
|
|
288
|
+
raw = await ws.send_command(
|
|
289
|
+
"ha_mcp_tools/blueprint_get", domain=domain, path=path
|
|
290
|
+
)
|
|
291
|
+
except (HomeAssistantCommandError, HomeAssistantCommandTimeout) as exc:
|
|
292
|
+
if is_unknown_command(exc):
|
|
293
|
+
invalidate_caps(self._client)
|
|
294
|
+
else:
|
|
295
|
+
logger.warning(
|
|
296
|
+
"ha_mcp_tools/blueprint_get failed; served metadata-only: %r",
|
|
297
|
+
exc,
|
|
298
|
+
)
|
|
299
|
+
return None, None
|
|
300
|
+
result = raw.get("result") or {}
|
|
301
|
+
config = result.get("config")
|
|
302
|
+
if isinstance(config, dict):
|
|
303
|
+
return config, None
|
|
304
|
+
return None, (
|
|
305
|
+
"Blueprint body could not be read or parsed by the ha_mcp_tools "
|
|
306
|
+
"component; returning metadata only"
|
|
307
|
+
)
|
|
308
|
+
|
|
225
309
|
async def _save_blueprint(
|
|
226
310
|
self,
|
|
227
311
|
url: str,
|
|
@@ -14,8 +14,19 @@ from fastmcp.exceptions import ToolError
|
|
|
14
14
|
from fastmcp.tools import tool
|
|
15
15
|
from pydantic import Field
|
|
16
16
|
|
|
17
|
+
from ..client.rest_client import (
|
|
18
|
+
HomeAssistantCommandError,
|
|
19
|
+
HomeAssistantCommandTimeout,
|
|
20
|
+
)
|
|
21
|
+
from ..client.websocket_client import get_websocket_client
|
|
17
22
|
from ..errors import ErrorCode, create_error_response
|
|
18
23
|
from .auto_backup import with_auto_backup
|
|
24
|
+
from .component_api import (
|
|
25
|
+
component_supports,
|
|
26
|
+
get_component_caps,
|
|
27
|
+
invalidate_caps,
|
|
28
|
+
is_unknown_command,
|
|
29
|
+
)
|
|
19
30
|
from .helpers import (
|
|
20
31
|
exception_to_structured_error,
|
|
21
32
|
extract_tool_error_message,
|
|
@@ -33,6 +44,11 @@ from .util_helpers import (
|
|
|
33
44
|
|
|
34
45
|
logger = logging.getLogger(__name__)
|
|
35
46
|
|
|
47
|
+
# The ha_mcp_tools/entity_enrich WS command: resolved area/floor/label NAMES +
|
|
48
|
+
# aliases per entity_id, byte-shaped like the component's registry join. Named
|
|
49
|
+
# once so the routing helper and its tests stay in lockstep.
|
|
50
|
+
WS_ENTITY_ENRICH = "ha_mcp_tools/entity_enrich"
|
|
51
|
+
|
|
36
52
|
# Bounds the per-frame size of a bulk config/entity_registry/get_entries call
|
|
37
53
|
# (extended entries, ~1KB each) so a large id list can't produce an over-cap
|
|
38
54
|
# WebSocket frame. Matches the chunk size smart_search uses for the same
|
|
@@ -73,6 +89,80 @@ def _format_fetched_entity(entry: dict[str, Any]) -> dict[str, Any]:
|
|
|
73
89
|
}
|
|
74
90
|
|
|
75
91
|
|
|
92
|
+
async def fetch_entity_enrichment_via_component(
|
|
93
|
+
client: Any, entity_ids: list[str]
|
|
94
|
+
) -> dict[str, dict[str, Any]] | None:
|
|
95
|
+
"""One ``ha_mcp_tools/entity_enrich`` read; ``None`` ⇒ skip enrichment (legacy).
|
|
96
|
+
|
|
97
|
+
Returns the component's ``{id: {area, floor, labels, aliases}}`` map (resolved
|
|
98
|
+
area/floor/label NAMES the raw registry entry lacks — it carries ``area_id`` /
|
|
99
|
+
label *ids*) or ``None`` when the component lacks the ``entity_enrich``
|
|
100
|
+
capability, was downgraded (``unknown_command`` → invalidate the cached caps),
|
|
101
|
+
or errored (logged). ``None`` means "no enrichment available", NOT "no such
|
|
102
|
+
entity": ``ha_get_entity``'s registry read already served the base record, so a
|
|
103
|
+
``None`` here simply leaves the additive fields off — the enrichment is
|
|
104
|
+
strictly additive, so its absence changes nothing else. A
|
|
105
|
+
``HomeAssistantConnectionError`` (WS down) is not caught here, so it propagates
|
|
106
|
+
to the tool's own error handling. Follows the same caps-gate discipline as
|
|
107
|
+
``component_devices.fetch_device_via_component``.
|
|
108
|
+
|
|
109
|
+
The ids are split into ``_GET_ENTRIES_CHUNK_SIZE`` chunks (the same bound the
|
|
110
|
+
sibling ``config/entity_registry/get_entries`` read uses) so a large bulk
|
|
111
|
+
request cannot produce an over-cap WebSocket frame or stall in the optional
|
|
112
|
+
enrichment step. Enrichment stays all-or-nothing: any chunk failing or drifting
|
|
113
|
+
in shape returns ``None`` (legacy shape), never a partially-enriched response.
|
|
114
|
+
"""
|
|
115
|
+
if not entity_ids:
|
|
116
|
+
return None
|
|
117
|
+
caps = await get_component_caps(client)
|
|
118
|
+
if not component_supports(caps, "entity_enrich"):
|
|
119
|
+
return None
|
|
120
|
+
ids = list(entity_ids)
|
|
121
|
+
chunks = [
|
|
122
|
+
ids[i : i + _GET_ENTRIES_CHUNK_SIZE]
|
|
123
|
+
for i in range(0, len(ids), _GET_ENTRIES_CHUNK_SIZE)
|
|
124
|
+
]
|
|
125
|
+
merged: dict[str, dict[str, Any]] = {}
|
|
126
|
+
try:
|
|
127
|
+
ws = await get_websocket_client(url=client.base_url, token=client.token)
|
|
128
|
+
for chunk in chunks:
|
|
129
|
+
raw = await ws.send_command(WS_ENTITY_ENRICH, entity_ids=chunk)
|
|
130
|
+
result = raw.get("result")
|
|
131
|
+
if not isinstance(result, dict):
|
|
132
|
+
return None
|
|
133
|
+
entities = result.get("entities")
|
|
134
|
+
if not isinstance(entities, dict):
|
|
135
|
+
return None
|
|
136
|
+
merged.update(entities)
|
|
137
|
+
except (HomeAssistantCommandError, HomeAssistantCommandTimeout) as exc:
|
|
138
|
+
if is_unknown_command(exc):
|
|
139
|
+
invalidate_caps(client)
|
|
140
|
+
else:
|
|
141
|
+
logger.warning("%s failed; skipped enrichment: %r", WS_ENTITY_ENRICH, exc)
|
|
142
|
+
return None
|
|
143
|
+
return merged
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _merge_entity_enrichment(
|
|
147
|
+
record: dict[str, Any], enrichment: dict[str, Any] | None
|
|
148
|
+
) -> None:
|
|
149
|
+
"""Additively attach resolved area/floor/label NAMES to an entity record.
|
|
150
|
+
|
|
151
|
+
The base record already carries ``area_id`` and label *ids* (``labels``); the
|
|
152
|
+
component join adds the resolved, device-inherited NAMES under non-clobbering
|
|
153
|
+
keys — ``area`` / ``floor`` / ``label_names`` — so nothing existing is
|
|
154
|
+
overwritten. ``aliases`` is already on the base record (same set — the
|
|
155
|
+
component sorts, so ordering may differ — and the base value is authoritative),
|
|
156
|
+
so it is not re-added. A ``None`` / empty enrichment is a no-op: on a capability
|
|
157
|
+
miss the fields are simply absent, leaving the legacy response shape unchanged.
|
|
158
|
+
"""
|
|
159
|
+
if not enrichment:
|
|
160
|
+
return
|
|
161
|
+
record["area"] = enrichment.get("area")
|
|
162
|
+
record["floor"] = enrichment.get("floor")
|
|
163
|
+
record["label_names"] = enrichment.get("labels") or []
|
|
164
|
+
|
|
165
|
+
|
|
76
166
|
def _match_registry_by_unique_id(
|
|
77
167
|
entries: list[dict[str, Any]],
|
|
78
168
|
unique_id: str,
|
|
@@ -1816,6 +1906,16 @@ class EntityTools:
|
|
|
1816
1906
|
helper's current config (template body, group members, etc.) without
|
|
1817
1907
|
scanning a domain list.
|
|
1818
1908
|
- unique_id: Integration's unique identifier
|
|
1909
|
+
|
|
1910
|
+
Resolved-name enrichment (present only when the ha_mcp_tools component
|
|
1911
|
+
advertises it; otherwise these keys are absent):
|
|
1912
|
+
- area: Assigned area NAME (device-inherited when the entity has none;
|
|
1913
|
+
resolves area_id above)
|
|
1914
|
+
- floor: Floor NAME of the assigned area
|
|
1915
|
+
- label_names: Assigned label NAMES (resolves the label ids in labels)
|
|
1916
|
+
Resolved label names live under label_names HERE (this tool's base
|
|
1917
|
+
`labels` already carries the label ids); ha_search result_fields and
|
|
1918
|
+
ha_get_entity_exposure instead emit the resolved names under `labels`.
|
|
1819
1919
|
"""
|
|
1820
1920
|
try:
|
|
1821
1921
|
# Resolver mode (unique_id) is mutually exclusive with entity_id.
|
|
@@ -1870,35 +1970,43 @@ class EntityTools:
|
|
|
1870
1970
|
)
|
|
1871
1971
|
return None # unreachable: exception_to_structured_error always raises
|
|
1872
1972
|
|
|
1973
|
+
async def _get_single_entity(self, eid: str) -> dict[str, Any]:
|
|
1974
|
+
"""Look up one entity registry entry, with additive enrichment."""
|
|
1975
|
+
logger.info(f"Getting entity registry entry for {eid}")
|
|
1976
|
+
try:
|
|
1977
|
+
result = await self._fetch_entity(eid)
|
|
1978
|
+
except ValueError as e:
|
|
1979
|
+
raise_tool_error(
|
|
1980
|
+
create_error_response(
|
|
1981
|
+
ErrorCode.SERVICE_CALL_FAILED,
|
|
1982
|
+
f"Entity not found: {e}",
|
|
1983
|
+
context={"entity_id": eid},
|
|
1984
|
+
suggestions=[
|
|
1985
|
+
"Use ha_search() to find valid entity IDs",
|
|
1986
|
+
"Check the entity_id spelling and format (e.g., 'sensor.temperature')",
|
|
1987
|
+
],
|
|
1988
|
+
)
|
|
1989
|
+
)
|
|
1990
|
+
# Additive area/floor/label-name enrichment via the component when it
|
|
1991
|
+
# advertises entity_enrich; a capability miss leaves the fields off
|
|
1992
|
+
# (legacy shape unchanged).
|
|
1993
|
+
enriched = await fetch_entity_enrichment_via_component(self._client, [eid])
|
|
1994
|
+
if enriched is not None:
|
|
1995
|
+
_merge_entity_enrichment(result, enriched.get(eid))
|
|
1996
|
+
return {
|
|
1997
|
+
"success": True,
|
|
1998
|
+
"entity_id": eid,
|
|
1999
|
+
"entity_entry": result,
|
|
2000
|
+
}
|
|
2001
|
+
|
|
1873
2002
|
async def _get_by_entity_id(self, entity_id: str | list[str]) -> dict[str, Any]:
|
|
1874
2003
|
"""Look up entities by entity_id (single or bulk list)."""
|
|
1875
2004
|
entity_ids, is_bulk, early_response = _parse_get_entity_ids(entity_id)
|
|
1876
2005
|
if early_response is not None:
|
|
1877
2006
|
return early_response
|
|
1878
2007
|
|
|
1879
|
-
# Single entity case
|
|
1880
2008
|
if not is_bulk:
|
|
1881
|
-
|
|
1882
|
-
logger.info(f"Getting entity registry entry for {eid}")
|
|
1883
|
-
try:
|
|
1884
|
-
result = await self._fetch_entity(eid)
|
|
1885
|
-
except ValueError as e:
|
|
1886
|
-
raise_tool_error(
|
|
1887
|
-
create_error_response(
|
|
1888
|
-
ErrorCode.SERVICE_CALL_FAILED,
|
|
1889
|
-
f"Entity not found: {e}",
|
|
1890
|
-
context={"entity_id": eid},
|
|
1891
|
-
suggestions=[
|
|
1892
|
-
"Use ha_search() to find valid entity IDs",
|
|
1893
|
-
"Check the entity_id spelling and format (e.g., 'sensor.temperature')",
|
|
1894
|
-
],
|
|
1895
|
-
)
|
|
1896
|
-
)
|
|
1897
|
-
return {
|
|
1898
|
-
"success": True,
|
|
1899
|
-
"entity_id": eid,
|
|
1900
|
-
"entity_entry": result,
|
|
1901
|
-
}
|
|
2009
|
+
return await self._get_single_entity(entity_ids[0])
|
|
1902
2010
|
|
|
1903
2011
|
# Bulk case - fetch all entities in one chunked get_entries call
|
|
1904
2012
|
# (native HA bulk command) instead of one registry get per id.
|
|
@@ -1922,6 +2030,20 @@ class EntityTools:
|
|
|
1922
2030
|
}
|
|
1923
2031
|
)
|
|
1924
2032
|
|
|
2033
|
+
# Additive area/floor/label-name enrichment for the found entities via the
|
|
2034
|
+
# component's entity_enrich capability; missing ⇒ fields simply absent.
|
|
2035
|
+
found_ids: list[str] = [
|
|
2036
|
+
e["entity_id"]
|
|
2037
|
+
for e in entity_entries
|
|
2038
|
+
if isinstance(e.get("entity_id"), str)
|
|
2039
|
+
]
|
|
2040
|
+
enriched = await fetch_entity_enrichment_via_component(self._client, found_ids)
|
|
2041
|
+
if enriched is not None:
|
|
2042
|
+
for entry in entity_entries:
|
|
2043
|
+
entry_id = entry.get("entity_id")
|
|
2044
|
+
if isinstance(entry_id, str):
|
|
2045
|
+
_merge_entity_enrichment(entry, enriched.get(entry_id))
|
|
2046
|
+
|
|
1925
2047
|
response: dict[str, Any] = {
|
|
1926
2048
|
"success": True,
|
|
1927
2049
|
"count": len(entity_entries),
|