ha-mcp-dev 7.14.0.dev2053__tar.gz → 7.14.0.dev2056__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.14.0.dev2053/src/ha_mcp_dev.egg-info → ha_mcp_dev-7.14.0.dev2056}/PKG-INFO +1 -1
- {ha_mcp_dev-7.14.0.dev2053 → ha_mcp_dev-7.14.0.dev2056}/pyproject.toml +1 -1
- {ha_mcp_dev-7.14.0.dev2053 → ha_mcp_dev-7.14.0.dev2056}/src/ha_mcp/client/rest_client.py +115 -26
- {ha_mcp_dev-7.14.0.dev2053 → ha_mcp_dev-7.14.0.dev2056}/src/ha_mcp/client/websocket_client.py +7 -6
- {ha_mcp_dev-7.14.0.dev2053 → ha_mcp_dev-7.14.0.dev2056}/src/ha_mcp/tools/backup.py +4 -5
- {ha_mcp_dev-7.14.0.dev2053 → ha_mcp_dev-7.14.0.dev2056}/src/ha_mcp/tools/component_api.py +4 -5
- {ha_mcp_dev-7.14.0.dev2053 → ha_mcp_dev-7.14.0.dev2056}/src/ha_mcp/tools/component_config_reads.py +14 -15
- {ha_mcp_dev-7.14.0.dev2053 → ha_mcp_dev-7.14.0.dev2056}/src/ha_mcp/tools/component_devices.py +9 -9
- {ha_mcp_dev-7.14.0.dev2053 → ha_mcp_dev-7.14.0.dev2056}/src/ha_mcp/tools/component_registries.py +8 -7
- {ha_mcp_dev-7.14.0.dev2053 → ha_mcp_dev-7.14.0.dev2056}/src/ha_mcp/tools/component_registry_lookup.py +9 -8
- ha_mcp_dev-7.14.0.dev2056/src/ha_mcp/tools/smart_search/_base.py +51 -0
- {ha_mcp_dev-7.14.0.dev2053 → ha_mcp_dev-7.14.0.dev2056}/src/ha_mcp/tools/smart_search/_entities.py +23 -11
- {ha_mcp_dev-7.14.0.dev2053 → ha_mcp_dev-7.14.0.dev2056}/src/ha_mcp/tools/smart_search/_overview.py +14 -4
- {ha_mcp_dev-7.14.0.dev2053 → ha_mcp_dev-7.14.0.dev2056}/src/ha_mcp/tools/smart_search/_scenes.py +3 -2
- {ha_mcp_dev-7.14.0.dev2053 → ha_mcp_dev-7.14.0.dev2056}/src/ha_mcp/tools/tools_config_automations.py +8 -2
- {ha_mcp_dev-7.14.0.dev2053 → ha_mcp_dev-7.14.0.dev2056}/src/ha_mcp/tools/tools_config_dashboards.py +13 -10
- {ha_mcp_dev-7.14.0.dev2053 → ha_mcp_dev-7.14.0.dev2056}/src/ha_mcp/tools/tools_config_helpers.py +20 -17
- {ha_mcp_dev-7.14.0.dev2053 → ha_mcp_dev-7.14.0.dev2056}/src/ha_mcp/tools/tools_config_scenes.py +6 -1
- {ha_mcp_dev-7.14.0.dev2053 → ha_mcp_dev-7.14.0.dev2056}/src/ha_mcp/tools/tools_config_scripts.py +10 -2
- {ha_mcp_dev-7.14.0.dev2053 → ha_mcp_dev-7.14.0.dev2056}/src/ha_mcp/tools/tools_dev.py +5 -5
- {ha_mcp_dev-7.14.0.dev2053 → ha_mcp_dev-7.14.0.dev2056}/src/ha_mcp/tools/tools_entities.py +2 -3
- {ha_mcp_dev-7.14.0.dev2053 → ha_mcp_dev-7.14.0.dev2056}/src/ha_mcp/tools/tools_integrations.py +57 -19
- {ha_mcp_dev-7.14.0.dev2053 → ha_mcp_dev-7.14.0.dev2056}/src/ha_mcp/tools/tools_registry.py +45 -10
- {ha_mcp_dev-7.14.0.dev2053 → ha_mcp_dev-7.14.0.dev2056}/src/ha_mcp/tools/tools_search.py +25 -13
- {ha_mcp_dev-7.14.0.dev2053 → ha_mcp_dev-7.14.0.dev2056}/src/ha_mcp/tools/tools_service.py +33 -7
- {ha_mcp_dev-7.14.0.dev2053 → ha_mcp_dev-7.14.0.dev2056}/src/ha_mcp/tools/tools_services.py +46 -14
- {ha_mcp_dev-7.14.0.dev2053 → ha_mcp_dev-7.14.0.dev2056}/src/ha_mcp/tools/tools_system.py +22 -12
- {ha_mcp_dev-7.14.0.dev2053 → ha_mcp_dev-7.14.0.dev2056}/src/ha_mcp/tools/tools_traces.py +6 -5
- {ha_mcp_dev-7.14.0.dev2053 → ha_mcp_dev-7.14.0.dev2056}/src/ha_mcp/tools/tools_voice_assistant.py +8 -8
- {ha_mcp_dev-7.14.0.dev2053 → ha_mcp_dev-7.14.0.dev2056}/src/ha_mcp/tools/util_helpers.py +56 -14
- {ha_mcp_dev-7.14.0.dev2053 → ha_mcp_dev-7.14.0.dev2056}/src/ha_mcp/visibility/resolver.py +3 -0
- {ha_mcp_dev-7.14.0.dev2053 → ha_mcp_dev-7.14.0.dev2056/src/ha_mcp_dev.egg-info}/PKG-INFO +1 -1
- ha_mcp_dev-7.14.0.dev2053/src/ha_mcp/tools/smart_search/_base.py +0 -38
- {ha_mcp_dev-7.14.0.dev2053 → ha_mcp_dev-7.14.0.dev2056}/LICENSE +0 -0
- {ha_mcp_dev-7.14.0.dev2053 → ha_mcp_dev-7.14.0.dev2056}/MANIFEST.in +0 -0
- {ha_mcp_dev-7.14.0.dev2053 → ha_mcp_dev-7.14.0.dev2056}/README.md +0 -0
- {ha_mcp_dev-7.14.0.dev2053 → ha_mcp_dev-7.14.0.dev2056}/setup.cfg +0 -0
- {ha_mcp_dev-7.14.0.dev2053 → ha_mcp_dev-7.14.0.dev2056}/src/ha_mcp/__init__.py +0 -0
- {ha_mcp_dev-7.14.0.dev2053 → ha_mcp_dev-7.14.0.dev2056}/src/ha_mcp/__main__.py +0 -0
- {ha_mcp_dev-7.14.0.dev2053 → ha_mcp_dev-7.14.0.dev2056}/src/ha_mcp/_pypi_marker +0 -0
- {ha_mcp_dev-7.14.0.dev2053 → ha_mcp_dev-7.14.0.dev2056}/src/ha_mcp/_version.py +0 -0
- {ha_mcp_dev-7.14.0.dev2053 → ha_mcp_dev-7.14.0.dev2056}/src/ha_mcp/auth/__init__.py +0 -0
- {ha_mcp_dev-7.14.0.dev2053 → ha_mcp_dev-7.14.0.dev2056}/src/ha_mcp/auth/consent_form.py +0 -0
- {ha_mcp_dev-7.14.0.dev2053 → ha_mcp_dev-7.14.0.dev2056}/src/ha_mcp/auth/provider.py +0 -0
- {ha_mcp_dev-7.14.0.dev2053 → ha_mcp_dev-7.14.0.dev2056}/src/ha_mcp/backup_manager.py +0 -0
- {ha_mcp_dev-7.14.0.dev2053 → ha_mcp_dev-7.14.0.dev2056}/src/ha_mcp/browser_landing.py +0 -0
- {ha_mcp_dev-7.14.0.dev2053 → ha_mcp_dev-7.14.0.dev2056}/src/ha_mcp/client/__init__.py +0 -0
- {ha_mcp_dev-7.14.0.dev2053 → ha_mcp_dev-7.14.0.dev2056}/src/ha_mcp/client/supervisor_client.py +0 -0
- {ha_mcp_dev-7.14.0.dev2053 → ha_mcp_dev-7.14.0.dev2056}/src/ha_mcp/client/websocket_listener.py +0 -0
- {ha_mcp_dev-7.14.0.dev2053 → ha_mcp_dev-7.14.0.dev2056}/src/ha_mcp/config.py +0 -0
- {ha_mcp_dev-7.14.0.dev2053 → ha_mcp_dev-7.14.0.dev2056}/src/ha_mcp/dashboard_screenshot/__init__.py +0 -0
- {ha_mcp_dev-7.14.0.dev2053 → ha_mcp_dev-7.14.0.dev2056}/src/ha_mcp/dashboard_screenshot/capture.py +0 -0
- {ha_mcp_dev-7.14.0.dev2053 → ha_mcp_dev-7.14.0.dev2056}/src/ha_mcp/dashboard_screenshot/content.py +0 -0
- {ha_mcp_dev-7.14.0.dev2053 → ha_mcp_dev-7.14.0.dev2056}/src/ha_mcp/dashboard_screenshot/paths.py +0 -0
- {ha_mcp_dev-7.14.0.dev2053 → ha_mcp_dev-7.14.0.dev2056}/src/ha_mcp/dashboard_screenshot/provision.py +0 -0
- {ha_mcp_dev-7.14.0.dev2053 → ha_mcp_dev-7.14.0.dev2056}/src/ha_mcp/dashboard_screenshot/theme_guard.py +0 -0
- {ha_mcp_dev-7.14.0.dev2053 → ha_mcp_dev-7.14.0.dev2056}/src/ha_mcp/errors.py +0 -0
- {ha_mcp_dev-7.14.0.dev2053 → ha_mcp_dev-7.14.0.dev2056}/src/ha_mcp/llm_exposure.py +0 -0
- {ha_mcp_dev-7.14.0.dev2053 → ha_mcp_dev-7.14.0.dev2056}/src/ha_mcp/policy/__init__.py +0 -0
- {ha_mcp_dev-7.14.0.dev2053 → ha_mcp_dev-7.14.0.dev2056}/src/ha_mcp/policy/approval_queue.py +0 -0
- {ha_mcp_dev-7.14.0.dev2053 → ha_mcp_dev-7.14.0.dev2056}/src/ha_mcp/policy/evaluator.py +0 -0
- {ha_mcp_dev-7.14.0.dev2053 → ha_mcp_dev-7.14.0.dev2056}/src/ha_mcp/policy/handlers.py +0 -0
- {ha_mcp_dev-7.14.0.dev2053 → ha_mcp_dev-7.14.0.dev2056}/src/ha_mcp/policy/middleware.py +0 -0
- {ha_mcp_dev-7.14.0.dev2053 → ha_mcp_dev-7.14.0.dev2056}/src/ha_mcp/policy/model.py +0 -0
- {ha_mcp_dev-7.14.0.dev2053 → ha_mcp_dev-7.14.0.dev2056}/src/ha_mcp/policy/persistence.py +0 -0
- {ha_mcp_dev-7.14.0.dev2053 → ha_mcp_dev-7.14.0.dev2056}/src/ha_mcp/policy/value_sources.py +0 -0
- {ha_mcp_dev-7.14.0.dev2053 → ha_mcp_dev-7.14.0.dev2056}/src/ha_mcp/py.typed +0 -0
- {ha_mcp_dev-7.14.0.dev2053 → ha_mcp_dev-7.14.0.dev2056}/src/ha_mcp/read_only.py +0 -0
- {ha_mcp_dev-7.14.0.dev2053 → ha_mcp_dev-7.14.0.dev2056}/src/ha_mcp/resources/skills-vendor/.claude/settings.json +0 -0
- {ha_mcp_dev-7.14.0.dev2053 → ha_mcp_dev-7.14.0.dev2056}/src/ha_mcp/resources/skills-vendor/.claude-plugin/marketplace.json +0 -0
- {ha_mcp_dev-7.14.0.dev2053 → ha_mcp_dev-7.14.0.dev2056}/src/ha_mcp/resources/skills-vendor/.claude-plugin/plugin.json +0 -0
- {ha_mcp_dev-7.14.0.dev2053 → ha_mcp_dev-7.14.0.dev2056}/src/ha_mcp/resources/skills-vendor/.github/ISSUE_TEMPLATE/skill-rca.md +0 -0
- {ha_mcp_dev-7.14.0.dev2053 → ha_mcp_dev-7.14.0.dev2056}/src/ha_mcp/resources/skills-vendor/.github/pull_request_template.md +0 -0
- {ha_mcp_dev-7.14.0.dev2053 → ha_mcp_dev-7.14.0.dev2056}/src/ha_mcp/resources/skills-vendor/AGENTS.md +0 -0
- {ha_mcp_dev-7.14.0.dev2053 → ha_mcp_dev-7.14.0.dev2056}/src/ha_mcp/resources/skills-vendor/CLAUDE.md +0 -0
- {ha_mcp_dev-7.14.0.dev2053 → ha_mcp_dev-7.14.0.dev2056}/src/ha_mcp/resources/skills-vendor/CONTRIBUTING.md +0 -0
- {ha_mcp_dev-7.14.0.dev2053 → ha_mcp_dev-7.14.0.dev2056}/src/ha_mcp/resources/skills-vendor/LICENSE +0 -0
- {ha_mcp_dev-7.14.0.dev2053 → ha_mcp_dev-7.14.0.dev2056}/src/ha_mcp/resources/skills-vendor/README.md +0 -0
- {ha_mcp_dev-7.14.0.dev2053 → ha_mcp_dev-7.14.0.dev2056}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/SKILL.md +0 -0
- {ha_mcp_dev-7.14.0.dev2053 → ha_mcp_dev-7.14.0.dev2056}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/evals/evals.json +0 -0
- {ha_mcp_dev-7.14.0.dev2053 → ha_mcp_dev-7.14.0.dev2056}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/appdaemon.md +0 -0
- {ha_mcp_dev-7.14.0.dev2053 → ha_mcp_dev-7.14.0.dev2056}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/automation-patterns.md +0 -0
- {ha_mcp_dev-7.14.0.dev2053 → ha_mcp_dev-7.14.0.dev2056}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/blueprint-guide.md +0 -0
- {ha_mcp_dev-7.14.0.dev2053 → ha_mcp_dev-7.14.0.dev2056}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-cards.md +0 -0
- {ha_mcp_dev-7.14.0.dev2053 → ha_mcp_dev-7.14.0.dev2056}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-guide.md +0 -0
- {ha_mcp_dev-7.14.0.dev2053 → ha_mcp_dev-7.14.0.dev2056}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/device-control.md +0 -0
- {ha_mcp_dev-7.14.0.dev2053 → ha_mcp_dev-7.14.0.dev2056}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/domain-docs.md +0 -0
- {ha_mcp_dev-7.14.0.dev2053 → ha_mcp_dev-7.14.0.dev2056}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/examples.yaml +0 -0
- {ha_mcp_dev-7.14.0.dev2053 → ha_mcp_dev-7.14.0.dev2056}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/helper-selection.md +0 -0
- {ha_mcp_dev-7.14.0.dev2053 → ha_mcp_dev-7.14.0.dev2056}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/safe-refactoring.md +0 -0
- {ha_mcp_dev-7.14.0.dev2053 → ha_mcp_dev-7.14.0.dev2056}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/scenes.md +0 -0
- {ha_mcp_dev-7.14.0.dev2053 → ha_mcp_dev-7.14.0.dev2056}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/template-guidelines.md +0 -0
- {ha_mcp_dev-7.14.0.dev2053 → ha_mcp_dev-7.14.0.dev2056}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/yaml-only-integrations.md +0 -0
- {ha_mcp_dev-7.14.0.dev2053 → ha_mcp_dev-7.14.0.dev2056}/src/ha_mcp/server.py +0 -0
- {ha_mcp_dev-7.14.0.dev2053 → ha_mcp_dev-7.14.0.dev2056}/src/ha_mcp/settings_ui/__init__.py +0 -0
- {ha_mcp_dev-7.14.0.dev2053 → ha_mcp_dev-7.14.0.dev2056}/src/ha_mcp/settings_ui/_handlers_advanced.py +0 -0
- {ha_mcp_dev-7.14.0.dev2053 → ha_mcp_dev-7.14.0.dev2056}/src/ha_mcp/settings_ui/_handlers_backups.py +0 -0
- {ha_mcp_dev-7.14.0.dev2053 → ha_mcp_dev-7.14.0.dev2056}/src/ha_mcp/settings_ui/_handlers_fs.py +0 -0
- {ha_mcp_dev-7.14.0.dev2053 → ha_mcp_dev-7.14.0.dev2056}/src/ha_mcp/settings_ui/_handlers_server.py +0 -0
- {ha_mcp_dev-7.14.0.dev2053 → ha_mcp_dev-7.14.0.dev2056}/src/ha_mcp/settings_ui/_handlers_theme.py +0 -0
- {ha_mcp_dev-7.14.0.dev2053 → ha_mcp_dev-7.14.0.dev2056}/src/ha_mcp/settings_ui/_handlers_tools.py +0 -0
- {ha_mcp_dev-7.14.0.dev2053 → ha_mcp_dev-7.14.0.dev2056}/src/ha_mcp/settings_ui/_i18n.py +0 -0
- {ha_mcp_dev-7.14.0.dev2053 → ha_mcp_dev-7.14.0.dev2056}/src/ha_mcp/settings_ui/_persistence.py +0 -0
- {ha_mcp_dev-7.14.0.dev2053 → ha_mcp_dev-7.14.0.dev2056}/src/ha_mcp/settings_ui/_supervisor.py +0 -0
- {ha_mcp_dev-7.14.0.dev2053 → ha_mcp_dev-7.14.0.dev2056}/src/ha_mcp/settings_ui/_theme.py +0 -0
- {ha_mcp_dev-7.14.0.dev2053 → ha_mcp_dev-7.14.0.dev2056}/src/ha_mcp/settings_ui/_tools_meta.py +0 -0
- {ha_mcp_dev-7.14.0.dev2053 → ha_mcp_dev-7.14.0.dev2056}/src/ha_mcp/settings_ui/locales/README.md +0 -0
- {ha_mcp_dev-7.14.0.dev2053 → ha_mcp_dev-7.14.0.dev2056}/src/ha_mcp/settings_ui/locales/de.json +0 -0
- {ha_mcp_dev-7.14.0.dev2053 → ha_mcp_dev-7.14.0.dev2056}/src/ha_mcp/settings_ui/locales/en.json +0 -0
- {ha_mcp_dev-7.14.0.dev2053 → ha_mcp_dev-7.14.0.dev2056}/src/ha_mcp/settings_ui/locales/ru.json +0 -0
- {ha_mcp_dev-7.14.0.dev2053 → ha_mcp_dev-7.14.0.dev2056}/src/ha_mcp/settings_ui/settings.css +0 -0
- {ha_mcp_dev-7.14.0.dev2053 → ha_mcp_dev-7.14.0.dev2056}/src/ha_mcp/settings_ui/settings.html +0 -0
- {ha_mcp_dev-7.14.0.dev2053 → ha_mcp_dev-7.14.0.dev2056}/src/ha_mcp/settings_ui/settings.js +0 -0
- {ha_mcp_dev-7.14.0.dev2053 → ha_mcp_dev-7.14.0.dev2056}/src/ha_mcp/smoke_test.py +0 -0
- {ha_mcp_dev-7.14.0.dev2053 → ha_mcp_dev-7.14.0.dev2056}/src/ha_mcp/stdio_settings_sidecar.py +0 -0
- {ha_mcp_dev-7.14.0.dev2053 → ha_mcp_dev-7.14.0.dev2056}/src/ha_mcp/strict_bps.py +0 -0
- {ha_mcp_dev-7.14.0.dev2053 → ha_mcp_dev-7.14.0.dev2056}/src/ha_mcp/tools/__init__.py +0 -0
- {ha_mcp_dev-7.14.0.dev2053 → ha_mcp_dev-7.14.0.dev2056}/src/ha_mcp/tools/auto_backup.py +0 -0
- {ha_mcp_dev-7.14.0.dev2053 → ha_mcp_dev-7.14.0.dev2056}/src/ha_mcp/tools/best_practice_checker.py +0 -0
- {ha_mcp_dev-7.14.0.dev2053 → ha_mcp_dev-7.14.0.dev2056}/src/ha_mcp/tools/config_entry_flow.py +0 -0
- {ha_mcp_dev-7.14.0.dev2053 → ha_mcp_dev-7.14.0.dev2056}/src/ha_mcp/tools/device_control.py +0 -0
- {ha_mcp_dev-7.14.0.dev2053 → ha_mcp_dev-7.14.0.dev2056}/src/ha_mcp/tools/enhanced.py +0 -0
- {ha_mcp_dev-7.14.0.dev2053 → ha_mcp_dev-7.14.0.dev2056}/src/ha_mcp/tools/helpers.py +0 -0
- {ha_mcp_dev-7.14.0.dev2053 → ha_mcp_dev-7.14.0.dev2056}/src/ha_mcp/tools/radio/__init__.py +0 -0
- {ha_mcp_dev-7.14.0.dev2053 → ha_mcp_dev-7.14.0.dev2056}/src/ha_mcp/tools/radio/base.py +0 -0
- {ha_mcp_dev-7.14.0.dev2053 → ha_mcp_dev-7.14.0.dev2056}/src/ha_mcp/tools/radio/matter.py +0 -0
- {ha_mcp_dev-7.14.0.dev2053 → ha_mcp_dev-7.14.0.dev2056}/src/ha_mcp/tools/radio/thread.py +0 -0
- {ha_mcp_dev-7.14.0.dev2053 → ha_mcp_dev-7.14.0.dev2056}/src/ha_mcp/tools/radio/zigbee.py +0 -0
- {ha_mcp_dev-7.14.0.dev2053 → ha_mcp_dev-7.14.0.dev2056}/src/ha_mcp/tools/radio/zwave.py +0 -0
- {ha_mcp_dev-7.14.0.dev2053 → ha_mcp_dev-7.14.0.dev2056}/src/ha_mcp/tools/reference_validator.py +0 -0
- {ha_mcp_dev-7.14.0.dev2053 → ha_mcp_dev-7.14.0.dev2056}/src/ha_mcp/tools/registry.py +0 -0
- {ha_mcp_dev-7.14.0.dev2053 → ha_mcp_dev-7.14.0.dev2056}/src/ha_mcp/tools/smart_search/__init__.py +0 -0
- {ha_mcp_dev-7.14.0.dev2053 → ha_mcp_dev-7.14.0.dev2056}/src/ha_mcp/tools/smart_search/_config.py +0 -0
- {ha_mcp_dev-7.14.0.dev2053 → ha_mcp_dev-7.14.0.dev2056}/src/ha_mcp/tools/smart_search/_deep.py +0 -0
- {ha_mcp_dev-7.14.0.dev2053 → ha_mcp_dev-7.14.0.dev2056}/src/ha_mcp/tools/smart_search/_fetch.py +0 -0
- {ha_mcp_dev-7.14.0.dev2053 → ha_mcp_dev-7.14.0.dev2056}/src/ha_mcp/tools/smart_search/_scoring.py +0 -0
- {ha_mcp_dev-7.14.0.dev2053 → ha_mcp_dev-7.14.0.dev2056}/src/ha_mcp/tools/tool_search_hint_middleware.py +0 -0
- {ha_mcp_dev-7.14.0.dev2053 → ha_mcp_dev-7.14.0.dev2056}/src/ha_mcp/tools/tools_addons.py +0 -0
- {ha_mcp_dev-7.14.0.dev2053 → ha_mcp_dev-7.14.0.dev2056}/src/ha_mcp/tools/tools_areas.py +0 -0
- {ha_mcp_dev-7.14.0.dev2053 → ha_mcp_dev-7.14.0.dev2056}/src/ha_mcp/tools/tools_blueprints.py +0 -0
- {ha_mcp_dev-7.14.0.dev2053 → ha_mcp_dev-7.14.0.dev2056}/src/ha_mcp/tools/tools_bug_report.py +0 -0
- {ha_mcp_dev-7.14.0.dev2053 → ha_mcp_dev-7.14.0.dev2056}/src/ha_mcp/tools/tools_calendar.py +0 -0
- {ha_mcp_dev-7.14.0.dev2053 → ha_mcp_dev-7.14.0.dev2056}/src/ha_mcp/tools/tools_camera.py +0 -0
- {ha_mcp_dev-7.14.0.dev2053 → ha_mcp_dev-7.14.0.dev2056}/src/ha_mcp/tools/tools_categories.py +0 -0
- {ha_mcp_dev-7.14.0.dev2053 → ha_mcp_dev-7.14.0.dev2056}/src/ha_mcp/tools/tools_code.py +0 -0
- {ha_mcp_dev-7.14.0.dev2053 → ha_mcp_dev-7.14.0.dev2056}/src/ha_mcp/tools/tools_dashboard_screenshot.py +0 -0
- {ha_mcp_dev-7.14.0.dev2053 → ha_mcp_dev-7.14.0.dev2056}/src/ha_mcp/tools/tools_energy.py +0 -0
- {ha_mcp_dev-7.14.0.dev2053 → ha_mcp_dev-7.14.0.dev2056}/src/ha_mcp/tools/tools_filesystem.py +0 -0
- {ha_mcp_dev-7.14.0.dev2053 → ha_mcp_dev-7.14.0.dev2056}/src/ha_mcp/tools/tools_groups.py +0 -0
- {ha_mcp_dev-7.14.0.dev2053 → ha_mcp_dev-7.14.0.dev2056}/src/ha_mcp/tools/tools_hacs.py +0 -0
- {ha_mcp_dev-7.14.0.dev2053 → ha_mcp_dev-7.14.0.dev2056}/src/ha_mcp/tools/tools_history.py +0 -0
- {ha_mcp_dev-7.14.0.dev2053 → ha_mcp_dev-7.14.0.dev2056}/src/ha_mcp/tools/tools_labels.py +0 -0
- {ha_mcp_dev-7.14.0.dev2053 → ha_mcp_dev-7.14.0.dev2056}/src/ha_mcp/tools/tools_radio.py +0 -0
- {ha_mcp_dev-7.14.0.dev2053 → ha_mcp_dev-7.14.0.dev2056}/src/ha_mcp/tools/tools_resources.py +0 -0
- {ha_mcp_dev-7.14.0.dev2053 → ha_mcp_dev-7.14.0.dev2056}/src/ha_mcp/tools/tools_themes.py +0 -0
- {ha_mcp_dev-7.14.0.dev2053 → ha_mcp_dev-7.14.0.dev2056}/src/ha_mcp/tools/tools_todo.py +0 -0
- {ha_mcp_dev-7.14.0.dev2053 → ha_mcp_dev-7.14.0.dev2056}/src/ha_mcp/tools/tools_updates.py +0 -0
- {ha_mcp_dev-7.14.0.dev2053 → ha_mcp_dev-7.14.0.dev2056}/src/ha_mcp/tools/tools_utility.py +0 -0
- {ha_mcp_dev-7.14.0.dev2053 → ha_mcp_dev-7.14.0.dev2056}/src/ha_mcp/tools/tools_yaml_config.py +0 -0
- {ha_mcp_dev-7.14.0.dev2053 → ha_mcp_dev-7.14.0.dev2056}/src/ha_mcp/tools/tools_yaml_read.py +0 -0
- {ha_mcp_dev-7.14.0.dev2053 → ha_mcp_dev-7.14.0.dev2056}/src/ha_mcp/tools/tools_zones.py +0 -0
- {ha_mcp_dev-7.14.0.dev2053 → ha_mcp_dev-7.14.0.dev2056}/src/ha_mcp/tools/validation_middleware.py +0 -0
- {ha_mcp_dev-7.14.0.dev2053 → ha_mcp_dev-7.14.0.dev2056}/src/ha_mcp/transforms/__init__.py +0 -0
- {ha_mcp_dev-7.14.0.dev2053 → ha_mcp_dev-7.14.0.dev2056}/src/ha_mcp/transforms/categorized_search.py +0 -0
- {ha_mcp_dev-7.14.0.dev2053 → ha_mcp_dev-7.14.0.dev2056}/src/ha_mcp/transforms/lite_docstrings.py +0 -0
- {ha_mcp_dev-7.14.0.dev2053 → ha_mcp_dev-7.14.0.dev2056}/src/ha_mcp/transport_security.py +0 -0
- {ha_mcp_dev-7.14.0.dev2053 → ha_mcp_dev-7.14.0.dev2056}/src/ha_mcp/update_check.py +0 -0
- {ha_mcp_dev-7.14.0.dev2053 → ha_mcp_dev-7.14.0.dev2056}/src/ha_mcp/utils/__init__.py +0 -0
- {ha_mcp_dev-7.14.0.dev2053 → ha_mcp_dev-7.14.0.dev2056}/src/ha_mcp/utils/config_hash.py +0 -0
- {ha_mcp_dev-7.14.0.dev2053 → ha_mcp_dev-7.14.0.dev2056}/src/ha_mcp/utils/data_paths.py +0 -0
- {ha_mcp_dev-7.14.0.dev2053 → ha_mcp_dev-7.14.0.dev2056}/src/ha_mcp/utils/domain_handlers.py +0 -0
- {ha_mcp_dev-7.14.0.dev2053 → ha_mcp_dev-7.14.0.dev2056}/src/ha_mcp/utils/fuzzy_search.py +0 -0
- {ha_mcp_dev-7.14.0.dev2053 → ha_mcp_dev-7.14.0.dev2056}/src/ha_mcp/utils/kill_signal_diagnostics.py +0 -0
- {ha_mcp_dev-7.14.0.dev2053 → ha_mcp_dev-7.14.0.dev2056}/src/ha_mcp/utils/operation_manager.py +0 -0
- {ha_mcp_dev-7.14.0.dev2053 → ha_mcp_dev-7.14.0.dev2056}/src/ha_mcp/utils/python_sandbox.py +0 -0
- {ha_mcp_dev-7.14.0.dev2053 → ha_mcp_dev-7.14.0.dev2056}/src/ha_mcp/utils/skill_loader.py +0 -0
- {ha_mcp_dev-7.14.0.dev2053 → ha_mcp_dev-7.14.0.dev2056}/src/ha_mcp/utils/usage_logger.py +0 -0
- {ha_mcp_dev-7.14.0.dev2053 → ha_mcp_dev-7.14.0.dev2056}/src/ha_mcp/visibility/__init__.py +0 -0
- {ha_mcp_dev-7.14.0.dev2053 → ha_mcp_dev-7.14.0.dev2056}/src/ha_mcp/visibility/model.py +0 -0
- {ha_mcp_dev-7.14.0.dev2053 → ha_mcp_dev-7.14.0.dev2056}/src/ha_mcp/visibility/persistence.py +0 -0
- {ha_mcp_dev-7.14.0.dev2053 → ha_mcp_dev-7.14.0.dev2056}/src/ha_mcp_dev.egg-info/SOURCES.txt +0 -0
- {ha_mcp_dev-7.14.0.dev2053 → ha_mcp_dev-7.14.0.dev2056}/src/ha_mcp_dev.egg-info/dependency_links.txt +0 -0
- {ha_mcp_dev-7.14.0.dev2053 → ha_mcp_dev-7.14.0.dev2056}/src/ha_mcp_dev.egg-info/entry_points.txt +0 -0
- {ha_mcp_dev-7.14.0.dev2053 → ha_mcp_dev-7.14.0.dev2056}/src/ha_mcp_dev.egg-info/requires.txt +0 -0
- {ha_mcp_dev-7.14.0.dev2053 → ha_mcp_dev-7.14.0.dev2056}/src/ha_mcp_dev.egg-info/top_level.txt +0 -0
- {ha_mcp_dev-7.14.0.dev2053 → ha_mcp_dev-7.14.0.dev2056}/tests/__init__.py +0 -0
- {ha_mcp_dev-7.14.0.dev2053 → ha_mcp_dev-7.14.0.dev2056}/tests/test_constants.py +0 -0
- {ha_mcp_dev-7.14.0.dev2053 → ha_mcp_dev-7.14.0.dev2056}/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.14.0.
|
|
7
|
+
version = "7.14.0.dev2056"
|
|
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
|
|
@@ -11,6 +11,7 @@ import time
|
|
|
11
11
|
from typing import Any
|
|
12
12
|
|
|
13
13
|
import httpx
|
|
14
|
+
from websockets.exceptions import WebSocketException
|
|
14
15
|
|
|
15
16
|
from .._version import get_supervisor_base_url, is_running_in_addon
|
|
16
17
|
from ..config import get_global_settings
|
|
@@ -133,6 +134,20 @@ class HomeAssistantCommandTimeout(HomeAssistantError):
|
|
|
133
134
|
"""
|
|
134
135
|
|
|
135
136
|
|
|
137
|
+
# Exception classes that are evidence no answer came back from Home Assistant,
|
|
138
|
+
# as opposed to Home Assistant answering with a rejection. ``OSError`` covers
|
|
139
|
+
# ``ConnectionResetError``; ``WebSocketException`` covers the
|
|
140
|
+
# ``ConnectionClosed`` family that ``send_command`` re-raises unwrapped;
|
|
141
|
+
# ``HomeAssistantCommandTimeout`` covers a socket that is still open but has
|
|
142
|
+
# stopped answering, which leaves a caller just as blind as a closed one.
|
|
143
|
+
_NO_ANSWER_ERRORS = (
|
|
144
|
+
HomeAssistantConnectionError,
|
|
145
|
+
HomeAssistantCommandTimeout,
|
|
146
|
+
OSError,
|
|
147
|
+
WebSocketException,
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
|
|
136
151
|
class HomeAssistantClient:
|
|
137
152
|
"""Authenticated HTTP client for Home Assistant API."""
|
|
138
153
|
|
|
@@ -1437,6 +1452,11 @@ class HomeAssistantClient:
|
|
|
1437
1452
|
retry_delay = 0.5 # seconds
|
|
1438
1453
|
|
|
1439
1454
|
for attempt in range(max_retries):
|
|
1455
|
+
# Whether the failure happened before a usable connection existed.
|
|
1456
|
+
# Failing to obtain one is transport death whatever class the
|
|
1457
|
+
# manager raises, and it raises a bare ``Exception`` when
|
|
1458
|
+
# ``connect()`` returns False, so this cannot be decided by type.
|
|
1459
|
+
acquiring = True
|
|
1440
1460
|
try:
|
|
1441
1461
|
# Use per-client WebSocket keyed to this client's credentials
|
|
1442
1462
|
# (verify_ssl included so a verify_ssl=False client never
|
|
@@ -1446,6 +1466,7 @@ class HomeAssistantClient:
|
|
|
1446
1466
|
token=self.token,
|
|
1447
1467
|
verify_ssl=self.verify_ssl,
|
|
1448
1468
|
)
|
|
1469
|
+
acquiring = False
|
|
1449
1470
|
|
|
1450
1471
|
# Special handling for render_template which returns an event with the actual result
|
|
1451
1472
|
if message.get("type") == "render_template":
|
|
@@ -1460,31 +1481,70 @@ class HomeAssistantClient:
|
|
|
1460
1481
|
|
|
1461
1482
|
except Exception as e:
|
|
1462
1483
|
error_str = str(e)
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1484
|
+
is_403 = "403" in error_str and "Forbidden" in error_str
|
|
1485
|
+
|
|
1486
|
+
# Transient 403 (rate limiting / reverse proxy throttling): retry
|
|
1487
|
+
# before deciding anything, since the next attempt may succeed.
|
|
1488
|
+
if is_403 and attempt < max_retries - 1:
|
|
1489
|
+
logger.warning(
|
|
1490
|
+
f"WebSocket 403 error (attempt {attempt + 1}/{max_retries}), "
|
|
1491
|
+
f"retrying after {retry_delay}s: {error_str}"
|
|
1492
|
+
)
|
|
1493
|
+
await asyncio.sleep(retry_delay)
|
|
1494
|
+
continue
|
|
1495
|
+
|
|
1496
|
+
# Phase first, type second. Failing before a client exists is
|
|
1497
|
+
# evidence about the transport whatever class was raised, and
|
|
1498
|
+
# the pooled manager raises a bare ``Exception`` when
|
|
1499
|
+
# ``connect()`` returns False, so that case cannot be decided
|
|
1500
|
+
# by type. Past acquisition only the no-answer classes count:
|
|
1501
|
+
# ``send_command`` deliberately re-raises the original
|
|
1502
|
+
# ``ConnectionClosed`` / ``ConnectionResetError`` from the send
|
|
1503
|
+
# rather than wrapping it, to keep at-most-once semantics for
|
|
1504
|
+
# write callers, so those arrive here untranslated. Anything
|
|
1505
|
+
# else (a ``ValueError`` from building the message, say) is a
|
|
1506
|
+
# bug rather than evidence about the connection.
|
|
1507
|
+
if acquiring or isinstance(e, _NO_ANSWER_ERRORS):
|
|
1508
|
+
# One error channel for "no answer came back" (#1947): the
|
|
1509
|
+
# caller decides the policy, rather than every consumer
|
|
1510
|
+
# having to notice a marker on an otherwise ordinary
|
|
1511
|
+
# failure envelope. Callers that legitimately continue on a
|
|
1512
|
+
# missing answer catch this and say in ``warnings`` what
|
|
1513
|
+
# they skipped; callers whose output would read as a
|
|
1514
|
+
# negative finding let it propagate.
|
|
1515
|
+
logger.error(
|
|
1516
|
+
f"WebSocket transport failed ({message.get('type')}): {e}"
|
|
1517
|
+
)
|
|
1518
|
+
if isinstance(e, HomeAssistantConnectionError):
|
|
1519
|
+
# Already the right class — re-raise unchanged so
|
|
1520
|
+
# subtypes (``HomeAssistantCommandNotSent``) and the
|
|
1521
|
+
# original traceback survive.
|
|
1522
|
+
raise
|
|
1523
|
+
raise HomeAssistantConnectionError(error_str) from e
|
|
1524
|
+
|
|
1525
|
+
# Deliberate asymmetry (#1966 review item 8): a 403 that
|
|
1526
|
+
# exhausts its retries DURING acquisition is transport death -
|
|
1527
|
+
# no client was ever obtained - so it raises and loses the
|
|
1528
|
+
# reverse-proxy suggestions below. The 403 text survives in the
|
|
1529
|
+
# message, and returning a soft envelope there instead would
|
|
1530
|
+
# put a blocked transport back on the degrade path this change
|
|
1531
|
+
# exists to close. Past acquisition the socket is alive and HA
|
|
1532
|
+
# (or a proxy) answered, so the tailored suggestions apply.
|
|
1533
|
+
if is_403:
|
|
1534
|
+
logger.error(
|
|
1535
|
+
f"WebSocket 403 error after {max_retries} attempts: {error_str}"
|
|
1536
|
+
)
|
|
1537
|
+
return {
|
|
1538
|
+
"success": False,
|
|
1539
|
+
"error": f"WebSocket request blocked (403 Forbidden): {error_str}",
|
|
1540
|
+
"error_code": getattr(e, "code", None),
|
|
1541
|
+
"suggestions": [
|
|
1542
|
+
"This may be caused by a reverse proxy or security filter",
|
|
1543
|
+
"Try simplifying the request (e.g., shorter templates, fewer parameters)",
|
|
1544
|
+
"If using complex templates, try breaking them into smaller parts",
|
|
1545
|
+
"Check if your Home Assistant is behind a reverse proxy with security rules",
|
|
1546
|
+
],
|
|
1547
|
+
}
|
|
1488
1548
|
|
|
1489
1549
|
# Name the command in the log line: a bare "Command failed:
|
|
1490
1550
|
# Unknown command." is undiagnosable from a user's log
|
|
@@ -1499,7 +1559,11 @@ class HomeAssistantClient:
|
|
|
1499
1559
|
"error_code": getattr(e, "code", None),
|
|
1500
1560
|
}
|
|
1501
1561
|
|
|
1502
|
-
|
|
1562
|
+
# Unreachable: every attempt either returns, raises, or ``continue``s,
|
|
1563
|
+
# and the last attempt cannot ``continue`` (the 403 branch guards on
|
|
1564
|
+
# ``attempt < max_retries - 1``). Assert rather than return a
|
|
1565
|
+
# contextless failure envelope that would read as a soft error.
|
|
1566
|
+
raise AssertionError("send_websocket_message loop exited without a result")
|
|
1503
1567
|
|
|
1504
1568
|
async def _handle_render_template(
|
|
1505
1569
|
self, ws_client: Any, message: dict[str, Any]
|
|
@@ -1536,12 +1600,23 @@ class HomeAssistantClient:
|
|
|
1536
1600
|
}
|
|
1537
1601
|
|
|
1538
1602
|
except TimeoutError:
|
|
1603
|
+
# Template-level, not transport-level: the wait budget here is the
|
|
1604
|
+
# caller's own ``timeout`` (default 3s) plus two seconds, and a
|
|
1605
|
+
# template that is merely slow to render leaves the socket healthy.
|
|
1606
|
+
# Deliberately NOT treated as a dead transport, unlike the 30s
|
|
1607
|
+
# round-trip ``HomeAssistantCommandTimeout`` in ``send_command``.
|
|
1539
1608
|
return {
|
|
1540
1609
|
"success": False,
|
|
1541
1610
|
"error": "Event timeout - template result not received",
|
|
1542
1611
|
"template": message.get("template"),
|
|
1543
1612
|
}
|
|
1544
1613
|
except Exception as e:
|
|
1614
|
+
if isinstance(e, _NO_ANSWER_ERRORS):
|
|
1615
|
+
# Let the caller's classification decide. Returning an envelope
|
|
1616
|
+
# here would route a transport death around the fail-loud policy
|
|
1617
|
+
# in ``send_websocket_message`` — the very swallow this branch
|
|
1618
|
+
# used to hide (#1947).
|
|
1619
|
+
raise
|
|
1545
1620
|
return {
|
|
1546
1621
|
"success": False,
|
|
1547
1622
|
"error": str(e),
|
|
@@ -1579,6 +1654,13 @@ class HomeAssistantClient:
|
|
|
1579
1654
|
f"Resolved script entity_id {entity_id} to storage key {unique_id}"
|
|
1580
1655
|
)
|
|
1581
1656
|
return str(unique_id)
|
|
1657
|
+
except HomeAssistantConnectionError:
|
|
1658
|
+
# A dead transport must not become a silent guess: the fallback
|
|
1659
|
+
# below assumes the storage key equals the bare entity_id, which is
|
|
1660
|
+
# exactly what a UI rename breaks. Guessing it on a write path
|
|
1661
|
+
# writes a NEW object under the wrong key instead of updating the
|
|
1662
|
+
# renamed one, so let the caller fail loud (#1947).
|
|
1663
|
+
raise
|
|
1582
1664
|
except Exception:
|
|
1583
1665
|
logger.debug(
|
|
1584
1666
|
f"Entity registry lookup failed for {entity_id}, using bare id: {bare_id}",
|
|
@@ -1720,6 +1802,13 @@ class HomeAssistantClient:
|
|
|
1720
1802
|
f"Resolved scene entity_id {entity_id} to storage key {unique_id}"
|
|
1721
1803
|
)
|
|
1722
1804
|
return str(unique_id)
|
|
1805
|
+
except HomeAssistantConnectionError:
|
|
1806
|
+
# A dead transport must not become a silent guess: the fallback
|
|
1807
|
+
# below assumes the storage key equals the bare entity_id, which is
|
|
1808
|
+
# exactly what a UI rename breaks. Guessing it on a write path
|
|
1809
|
+
# writes a NEW object under the wrong key instead of updating the
|
|
1810
|
+
# renamed one, so let the caller fail loud (#1947).
|
|
1811
|
+
raise
|
|
1723
1812
|
except Exception:
|
|
1724
1813
|
logger.debug(
|
|
1725
1814
|
f"Entity registry lookup failed for {entity_id}, using bare id: {bare_id}",
|
{ha_mcp_dev-7.14.0.dev2053 → ha_mcp_dev-7.14.0.dev2056}/src/ha_mcp/client/websocket_client.py
RENAMED
|
@@ -22,6 +22,7 @@ import websockets
|
|
|
22
22
|
|
|
23
23
|
from ..config import get_global_settings
|
|
24
24
|
from .rest_client import (
|
|
25
|
+
HomeAssistantAuthError,
|
|
25
26
|
HomeAssistantCommandError,
|
|
26
27
|
HomeAssistantCommandNotSent,
|
|
27
28
|
HomeAssistantCommandTimeout,
|
|
@@ -368,8 +369,8 @@ class HomeAssistantWebSocketClient:
|
|
|
368
369
|
message_type="auth_invalid", timeout=1
|
|
369
370
|
)
|
|
370
371
|
if auth_invalid:
|
|
371
|
-
raise
|
|
372
|
-
raise
|
|
372
|
+
raise HomeAssistantAuthError("Authentication failed: Invalid token")
|
|
373
|
+
raise HomeAssistantConnectionError("Authentication timeout")
|
|
373
374
|
|
|
374
375
|
self._state.mark_authenticated()
|
|
375
376
|
logger.info("WebSocket connected and authenticated successfully")
|
|
@@ -413,7 +414,7 @@ class HomeAssistantWebSocketClient:
|
|
|
413
414
|
async def _send_auth(self) -> None:
|
|
414
415
|
"""Send authentication message."""
|
|
415
416
|
if not self.websocket:
|
|
416
|
-
raise
|
|
417
|
+
raise HomeAssistantConnectionError("WebSocket not connected")
|
|
417
418
|
auth_message = {"type": "auth", "access_token": self.token}
|
|
418
419
|
await self.websocket.send(json.dumps(auth_message))
|
|
419
420
|
|
|
@@ -434,7 +435,7 @@ class HomeAssistantWebSocketClient:
|
|
|
434
435
|
async def _message_handler(self) -> None:
|
|
435
436
|
"""Background task to handle incoming WebSocket messages."""
|
|
436
437
|
if not self.websocket:
|
|
437
|
-
raise
|
|
438
|
+
raise HomeAssistantConnectionError("WebSocket not connected")
|
|
438
439
|
# None means a clean exit (the async-for loop simply ended); the
|
|
439
440
|
# except blocks below fill this in so pending futures — and the
|
|
440
441
|
# log — carry *why* the connection went away instead of a bare
|
|
@@ -573,7 +574,7 @@ class HomeAssistantWebSocketClient:
|
|
|
573
574
|
|
|
574
575
|
async with self._send_lock:
|
|
575
576
|
if not self.websocket:
|
|
576
|
-
raise
|
|
577
|
+
raise HomeAssistantConnectionError("WebSocket not connected")
|
|
577
578
|
logger.debug(f"WebSocket sending: {message}")
|
|
578
579
|
await self.websocket.send(json.dumps(message))
|
|
579
580
|
|
|
@@ -1204,7 +1205,7 @@ class WebSocketManager:
|
|
|
1204
1205
|
# keeps a non-str (e.g. a MagicMock in tests) from polluting
|
|
1205
1206
|
# the message with a repr.
|
|
1206
1207
|
detail = f": {reason}" if isinstance(reason, str) else ""
|
|
1207
|
-
raise
|
|
1208
|
+
raise HomeAssistantConnectionError(
|
|
1208
1209
|
"Failed to connect to Home Assistant WebSocket" + detail
|
|
1209
1210
|
)
|
|
1210
1211
|
|
|
@@ -244,9 +244,8 @@ async def _backup_prep_via_component(
|
|
|
244
244
|
in-process read. ``None`` on capability miss, downgrade (``unknown_command``
|
|
245
245
|
→ invalidate the cached caps), or command error/timeout (logged) — the
|
|
246
246
|
caller falls back to the legacy sequential calls. A
|
|
247
|
-
``HomeAssistantConnectionError``
|
|
248
|
-
|
|
249
|
-
mapped to ``None``: the legacy probes (``_get_local_backup_agent_id`` /
|
|
247
|
+
``HomeAssistantConnectionError`` — a pooled-WS drop, or a failed
|
|
248
|
+
(re)connect — is caught here and mapped to ``None``: the legacy probes (``_get_local_backup_agent_id`` /
|
|
250
249
|
``_get_backup_password``) run on a DEDICATED ``get_connected_ws_client`` socket
|
|
251
250
|
already connected before this read, NOT this pooled one — so a wedged pooled
|
|
252
251
|
socket must fall back to the legacy calls rather than fail create/restore.
|
|
@@ -265,8 +264,8 @@ async def _backup_prep_via_component(
|
|
|
265
264
|
logger.warning("%s failed; fell back to legacy: %r", WS_BACKUP_PREP, exc)
|
|
266
265
|
return None
|
|
267
266
|
except Exception as exc:
|
|
268
|
-
# HomeAssistantConnectionError
|
|
269
|
-
#
|
|
267
|
+
# HomeAssistantConnectionError: a pooled-WS drop or a failed
|
|
268
|
+
# (re)connect. The legacy probes
|
|
270
269
|
# ride a dedicated already-connected socket, so fall back rather than fail
|
|
271
270
|
# create/restore on a wedged pooled socket.
|
|
272
271
|
logger.warning(
|
|
@@ -243,8 +243,8 @@ async def get_component_caps(client: Any) -> ComponentCaps | None:
|
|
|
243
243
|
installed / upgraded mid-session (the REST client — the cache key — is not
|
|
244
244
|
recreated on an HA restart) is eventually adopted instead of pinned absent.
|
|
245
245
|
- ``HomeAssistantConnectionError`` / ``HomeAssistantCommandTimeout`` (WS
|
|
246
|
-
down or slow
|
|
247
|
-
|
|
246
|
+
down or slow, including the failed-connect raise from
|
|
247
|
+
``WebSocketManager``): cache a SHORT transient
|
|
248
248
|
negative (``_TRANSIENT_NEGATIVE_CACHE_TTL_S``) so repeated calls on a
|
|
249
249
|
WS-broken install skip the slow connect and go straight to legacy, then
|
|
250
250
|
re-probe once the window lapses (self-healing). The consuming tool's legacy
|
|
@@ -297,9 +297,8 @@ async def get_component_caps(client: Any) -> ComponentCaps | None:
|
|
|
297
297
|
_store_transient_negative(client)
|
|
298
298
|
return None
|
|
299
299
|
except Exception:
|
|
300
|
-
#
|
|
301
|
-
#
|
|
302
|
-
# failure review-5 M8 targets. Short transient cache, self-healing.
|
|
300
|
+
# Broad on purpose: the probe must never promote an unexpected
|
|
301
|
+
# fault into a tool failure. Short transient cache, self-healing.
|
|
303
302
|
logger.debug("%s probe failed unexpectedly", INFO_COMMAND, exc_info=True)
|
|
304
303
|
_store_transient_negative(client)
|
|
305
304
|
return None
|
{ha_mcp_dev-7.14.0.dev2053 → ha_mcp_dev-7.14.0.dev2056}/src/ha_mcp/tools/component_config_reads.py
RENAMED
|
@@ -42,9 +42,10 @@ still completes:
|
|
|
42
42
|
|
|
43
43
|
- ``fetch_entity_lookup_via_component``'s consumers degrade gracefully. The scene
|
|
44
44
|
resolver's ``config/entity_registry/list`` dump rides
|
|
45
|
-
``client.send_websocket_message
|
|
46
|
-
``{"success": False}
|
|
47
|
-
|
|
45
|
+
``client.send_websocket_message``, which answers a command HA rejected with
|
|
46
|
+
``{"success": False}``, so the resolver walks its retry loop to the naive
|
|
47
|
+
``scene.{id}`` fallback (a dead transport raises there instead, #1947) - and
|
|
48
|
+
the automation resolver scans REST
|
|
48
49
|
``get_states()`` and additionally catches broadly → ``None``. Its only routed
|
|
49
50
|
call sites run AFTER a scene/automation upsert commits or BEFORE a REST delete,
|
|
50
51
|
so an escaping transport error would report a landed write as failed / abort a
|
|
@@ -104,7 +105,7 @@ async def fetch_entity_lookup_via_component(
|
|
|
104
105
|
``None`` on capability miss, downgrade (``unknown_command`` → invalidate the
|
|
105
106
|
cached caps), command error/timeout (logged), a connection-establishment
|
|
106
107
|
failure (logged — see the module docstring: the resolvers' legacy paths ride
|
|
107
|
-
the
|
|
108
|
+
the WS bridge / REST, NOT this pooled socket, so a transport
|
|
108
109
|
failure must fall back rather than abort a landed write), or a shape-drift
|
|
109
110
|
payload (no ``matches`` list). Same error-taxonomy and silent fallback as
|
|
110
111
|
``component_devices.fetch_device_via_component``.
|
|
@@ -125,8 +126,8 @@ async def fetch_entity_lookup_via_component(
|
|
|
125
126
|
logger.warning("%s failed; fell back to legacy: %r", WS_ENTITY_LOOKUP, exc)
|
|
126
127
|
return None
|
|
127
128
|
except Exception as exc:
|
|
128
|
-
# HomeAssistantConnectionError
|
|
129
|
-
#
|
|
129
|
+
# HomeAssistantConnectionError: a pooled-WS drop or a failed
|
|
130
|
+
# (re)connect.
|
|
130
131
|
# The resolvers' legacy paths do NOT die identically (see module docstring),
|
|
131
132
|
# so route to legacy rather than escape.
|
|
132
133
|
logger.warning(
|
|
@@ -163,11 +164,10 @@ async def fetch_reference_data_via_component(
|
|
|
163
164
|
|
|
164
165
|
Like every component fetch helper, a transport failure routes to ``None``
|
|
165
166
|
(→ legacy REST) rather than escaping; the reference validator's legacy path is
|
|
166
|
-
the REST ``get_services()`` / ``get_states()`` pair. The catch
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
swallow-all fetch guard and skip ALL reference warnings even when REST is up.
|
|
167
|
+
the REST ``get_services()`` / ``get_states()`` pair. The catch stays broad
|
|
168
|
+
so no unexpected fault escapes: that would make ``validate_config_references``
|
|
169
|
+
hit its swallow-all fetch guard and skip ALL reference warnings even when
|
|
170
|
+
REST is up.
|
|
171
171
|
Mirrors ``get_component_caps``' own broad-catch precedent.
|
|
172
172
|
"""
|
|
173
173
|
caps = await get_component_caps(client)
|
|
@@ -187,10 +187,9 @@ async def fetch_reference_data_via_component(
|
|
|
187
187
|
return None
|
|
188
188
|
except Exception as exc:
|
|
189
189
|
# Legacy is REST (get_services/get_states), NOT the pooled WS, so a
|
|
190
|
-
# pooled-WS drop (
|
|
191
|
-
#
|
|
192
|
-
#
|
|
193
|
-
# swallow-all guard (which would skip every reference warning).
|
|
190
|
+
# pooled-WS drop or a failed (re)connect must route to the REST fetch
|
|
191
|
+
# rather than escape into the validator's swallow-all guard (which would
|
|
192
|
+
# skip every reference warning).
|
|
194
193
|
logger.warning(
|
|
195
194
|
"%s connection error; falling back to REST legacy: %r",
|
|
196
195
|
WS_REFERENCE_DATA,
|
{ha_mcp_dev-7.14.0.dev2053 → ha_mcp_dev-7.14.0.dev2056}/src/ha_mcp/tools/component_devices.py
RENAMED
|
@@ -71,10 +71,10 @@ async def fetch_device_via_component(
|
|
|
71
71
|
unavailable, fall back"). Falls back **silently**, mirroring
|
|
72
72
|
``ha_get_state``: the legacy path returns the byte-identical device either way
|
|
73
73
|
and the ``log.warning`` preserves operator visibility. A
|
|
74
|
-
``HomeAssistantConnectionError``
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
74
|
+
``HomeAssistantConnectionError`` — a pooled-WS drop, or a failed
|
|
75
|
+
(re)connect — is caught here and mapped to ``None``: the consumers' legacy
|
|
76
|
+
paths ride the ``send_websocket_message`` bridge (registry tools) or a
|
|
77
|
+
dedicated one-shot WS
|
|
78
78
|
client (the auto-backup capture) — NOT this pooled socket — so a transport
|
|
79
79
|
failure must fall back rather than block a tool / a wrapped write.
|
|
80
80
|
|
|
@@ -100,10 +100,10 @@ async def fetch_device_via_component(
|
|
|
100
100
|
logger.warning("%s failed; fell back to legacy: %r", WS_DEVICE_GET, exc)
|
|
101
101
|
return None
|
|
102
102
|
except Exception as exc:
|
|
103
|
-
# HomeAssistantConnectionError
|
|
104
|
-
#
|
|
105
|
-
#
|
|
106
|
-
#
|
|
103
|
+
# HomeAssistantConnectionError: a pooled-WS drop or a failed
|
|
104
|
+
# (re)connect. The legacy paths ride the send_websocket_message bridge /
|
|
105
|
+
# a dedicated capture socket, so fall back to legacy; if the transport
|
|
106
|
+
# itself is dead the bridge raises there in turn (#1947).
|
|
107
107
|
logger.warning(
|
|
108
108
|
"%s connection error; falling back to legacy: %r", WS_DEVICE_GET, exc
|
|
109
109
|
)
|
|
@@ -173,7 +173,7 @@ async def fetch_device_list_via_component(client: Any) -> dict[str, Any] | None:
|
|
|
173
173
|
return None
|
|
174
174
|
except Exception as exc:
|
|
175
175
|
# HomeAssistantConnectionError / plain establish Exception → legacy (the
|
|
176
|
-
# legacy device list rides the
|
|
176
|
+
# legacy device list rides the send_websocket_message bridge).
|
|
177
177
|
logger.warning(
|
|
178
178
|
"%s connection error; falling back to legacy: %r", WS_DEVICE_LIST, exc
|
|
179
179
|
)
|
{ha_mcp_dev-7.14.0.dev2053 → ha_mcp_dev-7.14.0.dev2056}/src/ha_mcp/tools/component_registries.py
RENAMED
|
@@ -100,14 +100,15 @@ async def fetch_registries_via_component(
|
|
|
100
100
|
failure (logged), or a malformed response — the outer ``result`` isn't a
|
|
101
101
|
dict, or a requested slice isn't its expected list/dict shape (logged; see
|
|
102
102
|
``_has_valid_shape``) — the caller falls back to its legacy WS list call(s)
|
|
103
|
-
in every case. A ``HomeAssistantConnectionError``
|
|
104
|
-
|
|
105
|
-
is caught here and mapped to ``None``: the auto-backup capture fetchers'
|
|
103
|
+
in every case. A ``HomeAssistantConnectionError`` — a pooled-WS drop, or a
|
|
104
|
+
failed (re)connect — is caught here and mapped to ``None``: the auto-backup capture fetchers'
|
|
106
105
|
legacy ``_ws_send`` builds a DEDICATED one-shot WS client (which can succeed
|
|
107
106
|
while the pooled socket is wedged) under a best-effort contract that requires
|
|
108
107
|
warn-and-skip, never a blocked write; ``ha_list_floors_areas``' legacy rides
|
|
109
|
-
the
|
|
110
|
-
|
|
108
|
+
the ``send_websocket_message`` bridge, which is the SAME pooled connection
|
|
109
|
+
and raises on a dead transport (#1947) rather than degrading. Only the
|
|
110
|
+
capture path's dedicated socket survives a wedged pool, so a
|
|
111
|
+
component-side fault must fall back rather than escape.
|
|
111
112
|
"""
|
|
112
113
|
caps = await get_component_caps(client)
|
|
113
114
|
if not component_supports(caps, "registries"):
|
|
@@ -125,8 +126,8 @@ async def fetch_registries_via_component(
|
|
|
125
126
|
logger.warning("%s failed; fell back to legacy: %r", WS_REGISTRIES, exc)
|
|
126
127
|
return None
|
|
127
128
|
except Exception as exc:
|
|
128
|
-
# HomeAssistantConnectionError
|
|
129
|
-
#
|
|
129
|
+
# HomeAssistantConnectionError: a pooled-WS drop or a failed
|
|
130
|
+
# (re)connect. The capture
|
|
130
131
|
# fetchers use a dedicated one-shot socket and forbid a blocked write, so
|
|
131
132
|
# this must fall back to legacy rather than escape into the write path.
|
|
132
133
|
logger.warning(
|
|
@@ -32,14 +32,15 @@ on any component error — lives in one place (the pattern ``component_devices``
|
|
|
32
32
|
established for the ``device_get`` / ``device_list`` capabilities). Both helpers
|
|
33
33
|
return ``None`` to mean "component unavailable — use the legacy path"; a component
|
|
34
34
|
that answers returns its payload. Per the uniform transport-fallback taxonomy, a
|
|
35
|
-
``HomeAssistantConnectionError``
|
|
36
|
-
``
|
|
37
|
-
``None`` so the legacy path runs: the consumers' legacy reads (the whole-registry
|
|
35
|
+
``HomeAssistantConnectionError`` — a pooled-WS drop, or a failed (re)connect —
|
|
36
|
+
is caught and mapped to ``None`` so the legacy path runs: the consumers' legacy reads (the whole-registry
|
|
38
37
|
``config/entity_registry/list`` dump and the per-id ``config/entity_registry/get``
|
|
39
|
-
retry loop) ride the
|
|
40
|
-
``{"success": False}``
|
|
41
|
-
|
|
42
|
-
``resolve_entities_via_component`` consumer's legacy retry loop entirely.
|
|
38
|
+
retry loop) ride the ``send_websocket_message`` bridge, which answers a
|
|
39
|
+
command HA rejected with ``{"success": False}`` - so a component-side fault
|
|
40
|
+
does not kill them, and letting it escape would skip the
|
|
41
|
+
``resolve_entities_via_component`` consumer's legacy retry loop entirely. The
|
|
42
|
+
bridge uses the SAME pooled connection, so a dead transport raises there too
|
|
43
|
+
(#1947) rather than degrading.
|
|
43
44
|
"""
|
|
44
45
|
|
|
45
46
|
from __future__ import annotations
|
|
@@ -96,7 +97,7 @@ async def fetch_entities_for_config_entry_via_component(
|
|
|
96
97
|
return None
|
|
97
98
|
except Exception as exc:
|
|
98
99
|
# HomeAssistantConnectionError / plain establish Exception → legacy (the
|
|
99
|
-
# legacy entity_registry/list dump rides the
|
|
100
|
+
# legacy entity_registry/list dump rides the bridge; see module
|
|
100
101
|
# docstring). Never propagate a transport failure out of the read.
|
|
101
102
|
logger.warning(
|
|
102
103
|
"%s connection error; falling back to legacy: %r", WS_REGISTRY_LOOKUP, exc
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""Shared base for the smart-search feature mixins.
|
|
2
|
+
|
|
3
|
+
``_SearchBase`` declares the instance attributes that
|
|
4
|
+
``SmartSearchTools.__init__`` sets, so each feature mixin type-checks in
|
|
5
|
+
isolation, and hosts the registry-list helper shared by the overview and
|
|
6
|
+
entity/area search mixins.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from ...client.rest_client import HomeAssistantClient
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class _SearchBase:
|
|
18
|
+
"""Attributes set by ``SmartSearchTools.__init__`` plus shared helpers."""
|
|
19
|
+
|
|
20
|
+
client: HomeAssistantClient
|
|
21
|
+
fuzzy_searcher: Any
|
|
22
|
+
settings: Any
|
|
23
|
+
|
|
24
|
+
@staticmethod
|
|
25
|
+
def _extract_registry_list(
|
|
26
|
+
result: Any, label: str, warnings: list[str] | None = None
|
|
27
|
+
) -> list[dict[str, Any]]:
|
|
28
|
+
"""Unwrap a WS registry-list result, returning ``[]`` on error/failure.
|
|
29
|
+
|
|
30
|
+
Every caller treats missing registry data as non-fatal rather than
|
|
31
|
+
raising: the overview degrades its area enrichment, and the area
|
|
32
|
+
search degrades to "no match found". Degrading is fine; degrading
|
|
33
|
+
silently is not — a caller that passes ``warnings`` gets a line
|
|
34
|
+
naming what was unavailable and why, so a thinner answer is
|
|
35
|
+
distinguishable from a genuinely empty registry (#1947).
|
|
36
|
+
"""
|
|
37
|
+
cause: str | None = None
|
|
38
|
+
registry: list[dict[str, Any]] = []
|
|
39
|
+
if isinstance(result, Exception):
|
|
40
|
+
logger.debug(f"Could not fetch {label}: {result}")
|
|
41
|
+
cause = str(result) or type(result).__name__
|
|
42
|
+
elif isinstance(result, dict) and result.get("success"):
|
|
43
|
+
registry = result.get("result", [])
|
|
44
|
+
elif isinstance(result, dict):
|
|
45
|
+
cause = str(result.get("error") or "request failed")
|
|
46
|
+
else:
|
|
47
|
+
cause = f"unexpected response type: {type(result).__name__}"
|
|
48
|
+
|
|
49
|
+
if cause is not None and warnings is not None:
|
|
50
|
+
warnings.append(f"{label} unavailable: {cause}")
|
|
51
|
+
return registry
|
{ha_mcp_dev-7.14.0.dev2053 → ha_mcp_dev-7.14.0.dev2056}/src/ha_mcp/tools/smart_search/_entities.py
RENAMED
|
@@ -457,9 +457,15 @@ class EntitySearchMixin(_SearchBase):
|
|
|
457
457
|
visibility_hidden, visibility_warnings = await load_hidden_set(
|
|
458
458
|
results[2], results[0], self.client, results[3]
|
|
459
459
|
)
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
460
|
+
# Registry failures still fail open, but each names itself: an area
|
|
461
|
+
# search that answers "no match found" because the area registry
|
|
462
|
+
# never arrived must not look like a search that genuinely found
|
|
463
|
+
# nothing (#1947).
|
|
464
|
+
registry_warnings: list[str] = []
|
|
465
|
+
area_registry = self._parse_area_registry(results[1], registry_warnings)
|
|
466
|
+
entity_reg_map = self._parse_entity_reg_map(results[2], registry_warnings)
|
|
467
|
+
device_area_map = self._parse_device_area_map(results[3], registry_warnings)
|
|
468
|
+
degraded_warnings = registry_warnings + visibility_warnings
|
|
463
469
|
|
|
464
470
|
area_query_lower = area_query.lower().strip()
|
|
465
471
|
matched_area_ids = self._match_area_ids(area_registry, area_query_lower)
|
|
@@ -476,7 +482,7 @@ class EntitySearchMixin(_SearchBase):
|
|
|
476
482
|
for aid, ainfo in area_registry.items()
|
|
477
483
|
],
|
|
478
484
|
},
|
|
479
|
-
|
|
485
|
+
degraded_warnings,
|
|
480
486
|
)
|
|
481
487
|
|
|
482
488
|
entity_area_resolved, hidden_entity_ids = self._resolve_entity_areas(
|
|
@@ -499,7 +505,7 @@ class EntitySearchMixin(_SearchBase):
|
|
|
499
505
|
"total_entities": total_entities,
|
|
500
506
|
"areas": formatted_areas,
|
|
501
507
|
},
|
|
502
|
-
|
|
508
|
+
degraded_warnings,
|
|
503
509
|
)
|
|
504
510
|
|
|
505
511
|
except Exception as e:
|
|
@@ -519,20 +525,24 @@ class EntitySearchMixin(_SearchBase):
|
|
|
519
525
|
raise
|
|
520
526
|
|
|
521
527
|
@classmethod
|
|
522
|
-
def _parse_area_registry(
|
|
528
|
+
def _parse_area_registry(
|
|
529
|
+
cls, result: Any, warnings: list[str] | None = None
|
|
530
|
+
) -> dict[str, dict[str, Any]]:
|
|
523
531
|
"""Parse the area registry into ``area_id -> area info``."""
|
|
524
532
|
area_registry: dict[str, dict[str, Any]] = {}
|
|
525
|
-
for area in cls._extract_registry_list(result, "area registry"):
|
|
533
|
+
for area in cls._extract_registry_list(result, "area registry", warnings):
|
|
526
534
|
area_id = area.get("area_id", "")
|
|
527
535
|
if area_id:
|
|
528
536
|
area_registry[area_id] = area
|
|
529
537
|
return area_registry
|
|
530
538
|
|
|
531
539
|
@classmethod
|
|
532
|
-
def _parse_entity_reg_map(
|
|
540
|
+
def _parse_entity_reg_map(
|
|
541
|
+
cls, result: Any, warnings: list[str] | None = None
|
|
542
|
+
) -> dict[str, dict[str, str | None]]:
|
|
533
543
|
"""Parse the entity registry into ``entity_id -> {area_id, device_id, hidden_by}``."""
|
|
534
544
|
entity_reg_map: dict[str, dict[str, str | None]] = {}
|
|
535
|
-
for entry in cls._extract_registry_list(result, "entity registry"):
|
|
545
|
+
for entry in cls._extract_registry_list(result, "entity registry", warnings):
|
|
536
546
|
entity_id = entry.get("entity_id")
|
|
537
547
|
if entity_id:
|
|
538
548
|
entity_reg_map[entity_id] = {
|
|
@@ -543,10 +553,12 @@ class EntitySearchMixin(_SearchBase):
|
|
|
543
553
|
return entity_reg_map
|
|
544
554
|
|
|
545
555
|
@classmethod
|
|
546
|
-
def _parse_device_area_map(
|
|
556
|
+
def _parse_device_area_map(
|
|
557
|
+
cls, result: Any, warnings: list[str] | None = None
|
|
558
|
+
) -> dict[str, str | None]:
|
|
547
559
|
"""Parse the device registry into ``device_id -> area_id``."""
|
|
548
560
|
device_area_map: dict[str, str | None] = {}
|
|
549
|
-
for device in cls._extract_registry_list(result, "device registry"):
|
|
561
|
+
for device in cls._extract_registry_list(result, "device registry", warnings):
|
|
550
562
|
device_id = device.get("id", "")
|
|
551
563
|
if device_id:
|
|
552
564
|
device_area_map[device_id] = device.get("area_id")
|