ha-mcp-dev 7.12.3.dev1888__tar.gz → 7.12.3.dev1894__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.12.3.dev1888/src/ha_mcp_dev.egg-info → ha_mcp_dev-7.12.3.dev1894}/PKG-INFO +1 -1
- {ha_mcp_dev-7.12.3.dev1888 → ha_mcp_dev-7.12.3.dev1894}/pyproject.toml +1 -1
- {ha_mcp_dev-7.12.3.dev1888 → ha_mcp_dev-7.12.3.dev1894}/src/ha_mcp/client/rest_client.py +34 -10
- {ha_mcp_dev-7.12.3.dev1888 → ha_mcp_dev-7.12.3.dev1894}/src/ha_mcp/client/websocket_client.py +45 -6
- {ha_mcp_dev-7.12.3.dev1888 → ha_mcp_dev-7.12.3.dev1894}/src/ha_mcp/smoke_test.py +2 -2
- {ha_mcp_dev-7.12.3.dev1888 → ha_mcp_dev-7.12.3.dev1894}/src/ha_mcp/stdio_settings_sidecar.py +6 -6
- {ha_mcp_dev-7.12.3.dev1888 → ha_mcp_dev-7.12.3.dev1894}/src/ha_mcp/tools/backup.py +8 -27
- {ha_mcp_dev-7.12.3.dev1888 → ha_mcp_dev-7.12.3.dev1894}/src/ha_mcp/tools/tools_addons.py +12 -40
- {ha_mcp_dev-7.12.3.dev1888 → ha_mcp_dev-7.12.3.dev1894}/src/ha_mcp/tools/tools_bug_report.py +15 -4
- {ha_mcp_dev-7.12.3.dev1888 → ha_mcp_dev-7.12.3.dev1894}/src/ha_mcp/tools/tools_calendar.py +71 -73
- {ha_mcp_dev-7.12.3.dev1888 → ha_mcp_dev-7.12.3.dev1894}/src/ha_mcp/tools/tools_config_helpers.py +166 -1
- {ha_mcp_dev-7.12.3.dev1888 → ha_mcp_dev-7.12.3.dev1894}/src/ha_mcp/tools/tools_config_scenes.py +17 -6
- {ha_mcp_dev-7.12.3.dev1888 → ha_mcp_dev-7.12.3.dev1894}/src/ha_mcp/tools/tools_entities.py +693 -148
- {ha_mcp_dev-7.12.3.dev1888 → ha_mcp_dev-7.12.3.dev1894}/src/ha_mcp/tools/tools_filesystem.py +34 -1
- {ha_mcp_dev-7.12.3.dev1888 → ha_mcp_dev-7.12.3.dev1894}/src/ha_mcp/tools/tools_history.py +101 -87
- {ha_mcp_dev-7.12.3.dev1888 → ha_mcp_dev-7.12.3.dev1894}/src/ha_mcp/tools/tools_mcp_component.py +14 -2
- {ha_mcp_dev-7.12.3.dev1888 → ha_mcp_dev-7.12.3.dev1894}/src/ha_mcp/tools/tools_radio.py +24 -6
- {ha_mcp_dev-7.12.3.dev1888 → ha_mcp_dev-7.12.3.dev1894}/src/ha_mcp/tools/tools_search.py +11 -12
- {ha_mcp_dev-7.12.3.dev1888 → ha_mcp_dev-7.12.3.dev1894}/src/ha_mcp/tools/tools_system.py +158 -12
- {ha_mcp_dev-7.12.3.dev1888 → ha_mcp_dev-7.12.3.dev1894}/src/ha_mcp/tools/tools_zones.py +185 -35
- {ha_mcp_dev-7.12.3.dev1888 → ha_mcp_dev-7.12.3.dev1894}/src/ha_mcp/tools/util_helpers.py +32 -0
- {ha_mcp_dev-7.12.3.dev1888 → ha_mcp_dev-7.12.3.dev1894/src/ha_mcp_dev.egg-info}/PKG-INFO +1 -1
- {ha_mcp_dev-7.12.3.dev1888 → ha_mcp_dev-7.12.3.dev1894}/LICENSE +0 -0
- {ha_mcp_dev-7.12.3.dev1888 → ha_mcp_dev-7.12.3.dev1894}/MANIFEST.in +0 -0
- {ha_mcp_dev-7.12.3.dev1888 → ha_mcp_dev-7.12.3.dev1894}/README.md +0 -0
- {ha_mcp_dev-7.12.3.dev1888 → ha_mcp_dev-7.12.3.dev1894}/setup.cfg +0 -0
- {ha_mcp_dev-7.12.3.dev1888 → ha_mcp_dev-7.12.3.dev1894}/src/ha_mcp/__init__.py +0 -0
- {ha_mcp_dev-7.12.3.dev1888 → ha_mcp_dev-7.12.3.dev1894}/src/ha_mcp/__main__.py +0 -0
- {ha_mcp_dev-7.12.3.dev1888 → ha_mcp_dev-7.12.3.dev1894}/src/ha_mcp/_pypi_marker +0 -0
- {ha_mcp_dev-7.12.3.dev1888 → ha_mcp_dev-7.12.3.dev1894}/src/ha_mcp/_version.py +0 -0
- {ha_mcp_dev-7.12.3.dev1888 → ha_mcp_dev-7.12.3.dev1894}/src/ha_mcp/auth/__init__.py +0 -0
- {ha_mcp_dev-7.12.3.dev1888 → ha_mcp_dev-7.12.3.dev1894}/src/ha_mcp/auth/consent_form.py +0 -0
- {ha_mcp_dev-7.12.3.dev1888 → ha_mcp_dev-7.12.3.dev1894}/src/ha_mcp/auth/provider.py +0 -0
- {ha_mcp_dev-7.12.3.dev1888 → ha_mcp_dev-7.12.3.dev1894}/src/ha_mcp/backup_manager.py +0 -0
- {ha_mcp_dev-7.12.3.dev1888 → ha_mcp_dev-7.12.3.dev1894}/src/ha_mcp/browser_landing.py +0 -0
- {ha_mcp_dev-7.12.3.dev1888 → ha_mcp_dev-7.12.3.dev1894}/src/ha_mcp/client/__init__.py +0 -0
- {ha_mcp_dev-7.12.3.dev1888 → ha_mcp_dev-7.12.3.dev1894}/src/ha_mcp/client/supervisor_client.py +0 -0
- {ha_mcp_dev-7.12.3.dev1888 → ha_mcp_dev-7.12.3.dev1894}/src/ha_mcp/client/websocket_listener.py +0 -0
- {ha_mcp_dev-7.12.3.dev1888 → ha_mcp_dev-7.12.3.dev1894}/src/ha_mcp/config.py +0 -0
- {ha_mcp_dev-7.12.3.dev1888 → ha_mcp_dev-7.12.3.dev1894}/src/ha_mcp/dashboard_screenshot/__init__.py +0 -0
- {ha_mcp_dev-7.12.3.dev1888 → ha_mcp_dev-7.12.3.dev1894}/src/ha_mcp/dashboard_screenshot/capture.py +0 -0
- {ha_mcp_dev-7.12.3.dev1888 → ha_mcp_dev-7.12.3.dev1894}/src/ha_mcp/dashboard_screenshot/provision.py +0 -0
- {ha_mcp_dev-7.12.3.dev1888 → ha_mcp_dev-7.12.3.dev1894}/src/ha_mcp/errors.py +0 -0
- {ha_mcp_dev-7.12.3.dev1888 → ha_mcp_dev-7.12.3.dev1894}/src/ha_mcp/llm_exposure.py +0 -0
- {ha_mcp_dev-7.12.3.dev1888 → ha_mcp_dev-7.12.3.dev1894}/src/ha_mcp/policy/__init__.py +0 -0
- {ha_mcp_dev-7.12.3.dev1888 → ha_mcp_dev-7.12.3.dev1894}/src/ha_mcp/policy/approval_queue.py +0 -0
- {ha_mcp_dev-7.12.3.dev1888 → ha_mcp_dev-7.12.3.dev1894}/src/ha_mcp/policy/evaluator.py +0 -0
- {ha_mcp_dev-7.12.3.dev1888 → ha_mcp_dev-7.12.3.dev1894}/src/ha_mcp/policy/handlers.py +0 -0
- {ha_mcp_dev-7.12.3.dev1888 → ha_mcp_dev-7.12.3.dev1894}/src/ha_mcp/policy/middleware.py +0 -0
- {ha_mcp_dev-7.12.3.dev1888 → ha_mcp_dev-7.12.3.dev1894}/src/ha_mcp/policy/model.py +0 -0
- {ha_mcp_dev-7.12.3.dev1888 → ha_mcp_dev-7.12.3.dev1894}/src/ha_mcp/policy/persistence.py +0 -0
- {ha_mcp_dev-7.12.3.dev1888 → ha_mcp_dev-7.12.3.dev1894}/src/ha_mcp/policy/value_sources.py +0 -0
- {ha_mcp_dev-7.12.3.dev1888 → ha_mcp_dev-7.12.3.dev1894}/src/ha_mcp/py.typed +0 -0
- {ha_mcp_dev-7.12.3.dev1888 → ha_mcp_dev-7.12.3.dev1894}/src/ha_mcp/read_only.py +0 -0
- {ha_mcp_dev-7.12.3.dev1888 → ha_mcp_dev-7.12.3.dev1894}/src/ha_mcp/resources/skills-vendor/.claude/settings.json +0 -0
- {ha_mcp_dev-7.12.3.dev1888 → ha_mcp_dev-7.12.3.dev1894}/src/ha_mcp/resources/skills-vendor/.claude-plugin/marketplace.json +0 -0
- {ha_mcp_dev-7.12.3.dev1888 → ha_mcp_dev-7.12.3.dev1894}/src/ha_mcp/resources/skills-vendor/.claude-plugin/plugin.json +0 -0
- {ha_mcp_dev-7.12.3.dev1888 → ha_mcp_dev-7.12.3.dev1894}/src/ha_mcp/resources/skills-vendor/.github/ISSUE_TEMPLATE/skill-rca.md +0 -0
- {ha_mcp_dev-7.12.3.dev1888 → ha_mcp_dev-7.12.3.dev1894}/src/ha_mcp/resources/skills-vendor/.github/pull_request_template.md +0 -0
- {ha_mcp_dev-7.12.3.dev1888 → ha_mcp_dev-7.12.3.dev1894}/src/ha_mcp/resources/skills-vendor/AGENTS.md +0 -0
- {ha_mcp_dev-7.12.3.dev1888 → ha_mcp_dev-7.12.3.dev1894}/src/ha_mcp/resources/skills-vendor/CLAUDE.md +0 -0
- {ha_mcp_dev-7.12.3.dev1888 → ha_mcp_dev-7.12.3.dev1894}/src/ha_mcp/resources/skills-vendor/CONTRIBUTING.md +0 -0
- {ha_mcp_dev-7.12.3.dev1888 → ha_mcp_dev-7.12.3.dev1894}/src/ha_mcp/resources/skills-vendor/LICENSE +0 -0
- {ha_mcp_dev-7.12.3.dev1888 → ha_mcp_dev-7.12.3.dev1894}/src/ha_mcp/resources/skills-vendor/README.md +0 -0
- {ha_mcp_dev-7.12.3.dev1888 → ha_mcp_dev-7.12.3.dev1894}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/SKILL.md +0 -0
- {ha_mcp_dev-7.12.3.dev1888 → ha_mcp_dev-7.12.3.dev1894}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/evals/evals.json +0 -0
- {ha_mcp_dev-7.12.3.dev1888 → ha_mcp_dev-7.12.3.dev1894}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/appdaemon.md +0 -0
- {ha_mcp_dev-7.12.3.dev1888 → ha_mcp_dev-7.12.3.dev1894}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/automation-patterns.md +0 -0
- {ha_mcp_dev-7.12.3.dev1888 → ha_mcp_dev-7.12.3.dev1894}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/blueprint-guide.md +0 -0
- {ha_mcp_dev-7.12.3.dev1888 → ha_mcp_dev-7.12.3.dev1894}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-cards.md +0 -0
- {ha_mcp_dev-7.12.3.dev1888 → ha_mcp_dev-7.12.3.dev1894}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-guide.md +0 -0
- {ha_mcp_dev-7.12.3.dev1888 → ha_mcp_dev-7.12.3.dev1894}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/device-control.md +0 -0
- {ha_mcp_dev-7.12.3.dev1888 → ha_mcp_dev-7.12.3.dev1894}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/domain-docs.md +0 -0
- {ha_mcp_dev-7.12.3.dev1888 → ha_mcp_dev-7.12.3.dev1894}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/examples.yaml +0 -0
- {ha_mcp_dev-7.12.3.dev1888 → ha_mcp_dev-7.12.3.dev1894}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/helper-selection.md +0 -0
- {ha_mcp_dev-7.12.3.dev1888 → ha_mcp_dev-7.12.3.dev1894}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/safe-refactoring.md +0 -0
- {ha_mcp_dev-7.12.3.dev1888 → ha_mcp_dev-7.12.3.dev1894}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/scenes.md +0 -0
- {ha_mcp_dev-7.12.3.dev1888 → ha_mcp_dev-7.12.3.dev1894}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/template-guidelines.md +0 -0
- {ha_mcp_dev-7.12.3.dev1888 → ha_mcp_dev-7.12.3.dev1894}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/yaml-only-integrations.md +0 -0
- {ha_mcp_dev-7.12.3.dev1888 → ha_mcp_dev-7.12.3.dev1894}/src/ha_mcp/server.py +0 -0
- {ha_mcp_dev-7.12.3.dev1888 → ha_mcp_dev-7.12.3.dev1894}/src/ha_mcp/settings_ui/__init__.py +0 -0
- {ha_mcp_dev-7.12.3.dev1888 → ha_mcp_dev-7.12.3.dev1894}/src/ha_mcp/settings_ui/settings.css +0 -0
- {ha_mcp_dev-7.12.3.dev1888 → ha_mcp_dev-7.12.3.dev1894}/src/ha_mcp/settings_ui/settings.html +0 -0
- {ha_mcp_dev-7.12.3.dev1888 → ha_mcp_dev-7.12.3.dev1894}/src/ha_mcp/settings_ui/settings.js +0 -0
- {ha_mcp_dev-7.12.3.dev1888 → ha_mcp_dev-7.12.3.dev1894}/src/ha_mcp/strict_bps.py +0 -0
- {ha_mcp_dev-7.12.3.dev1888 → ha_mcp_dev-7.12.3.dev1894}/src/ha_mcp/tools/__init__.py +0 -0
- {ha_mcp_dev-7.12.3.dev1888 → ha_mcp_dev-7.12.3.dev1894}/src/ha_mcp/tools/auto_backup.py +0 -0
- {ha_mcp_dev-7.12.3.dev1888 → ha_mcp_dev-7.12.3.dev1894}/src/ha_mcp/tools/best_practice_checker.py +0 -0
- {ha_mcp_dev-7.12.3.dev1888 → ha_mcp_dev-7.12.3.dev1894}/src/ha_mcp/tools/component_api.py +0 -0
- {ha_mcp_dev-7.12.3.dev1888 → ha_mcp_dev-7.12.3.dev1894}/src/ha_mcp/tools/config_entry_flow.py +0 -0
- {ha_mcp_dev-7.12.3.dev1888 → ha_mcp_dev-7.12.3.dev1894}/src/ha_mcp/tools/device_control.py +0 -0
- {ha_mcp_dev-7.12.3.dev1888 → ha_mcp_dev-7.12.3.dev1894}/src/ha_mcp/tools/enhanced.py +0 -0
- {ha_mcp_dev-7.12.3.dev1888 → ha_mcp_dev-7.12.3.dev1894}/src/ha_mcp/tools/helpers.py +0 -0
- {ha_mcp_dev-7.12.3.dev1888 → ha_mcp_dev-7.12.3.dev1894}/src/ha_mcp/tools/radio/__init__.py +0 -0
- {ha_mcp_dev-7.12.3.dev1888 → ha_mcp_dev-7.12.3.dev1894}/src/ha_mcp/tools/radio/base.py +0 -0
- {ha_mcp_dev-7.12.3.dev1888 → ha_mcp_dev-7.12.3.dev1894}/src/ha_mcp/tools/radio/matter.py +0 -0
- {ha_mcp_dev-7.12.3.dev1888 → ha_mcp_dev-7.12.3.dev1894}/src/ha_mcp/tools/radio/thread.py +0 -0
- {ha_mcp_dev-7.12.3.dev1888 → ha_mcp_dev-7.12.3.dev1894}/src/ha_mcp/tools/radio/zigbee.py +0 -0
- {ha_mcp_dev-7.12.3.dev1888 → ha_mcp_dev-7.12.3.dev1894}/src/ha_mcp/tools/radio/zwave.py +0 -0
- {ha_mcp_dev-7.12.3.dev1888 → ha_mcp_dev-7.12.3.dev1894}/src/ha_mcp/tools/reference_validator.py +0 -0
- {ha_mcp_dev-7.12.3.dev1888 → ha_mcp_dev-7.12.3.dev1894}/src/ha_mcp/tools/registry.py +0 -0
- {ha_mcp_dev-7.12.3.dev1888 → ha_mcp_dev-7.12.3.dev1894}/src/ha_mcp/tools/smart_search/__init__.py +0 -0
- {ha_mcp_dev-7.12.3.dev1888 → ha_mcp_dev-7.12.3.dev1894}/src/ha_mcp/tools/smart_search/_base.py +0 -0
- {ha_mcp_dev-7.12.3.dev1888 → ha_mcp_dev-7.12.3.dev1894}/src/ha_mcp/tools/smart_search/_config.py +0 -0
- {ha_mcp_dev-7.12.3.dev1888 → ha_mcp_dev-7.12.3.dev1894}/src/ha_mcp/tools/smart_search/_deep.py +0 -0
- {ha_mcp_dev-7.12.3.dev1888 → ha_mcp_dev-7.12.3.dev1894}/src/ha_mcp/tools/smart_search/_entities.py +0 -0
- {ha_mcp_dev-7.12.3.dev1888 → ha_mcp_dev-7.12.3.dev1894}/src/ha_mcp/tools/smart_search/_fetch.py +0 -0
- {ha_mcp_dev-7.12.3.dev1888 → ha_mcp_dev-7.12.3.dev1894}/src/ha_mcp/tools/smart_search/_overview.py +0 -0
- {ha_mcp_dev-7.12.3.dev1888 → ha_mcp_dev-7.12.3.dev1894}/src/ha_mcp/tools/smart_search/_scenes.py +0 -0
- {ha_mcp_dev-7.12.3.dev1888 → ha_mcp_dev-7.12.3.dev1894}/src/ha_mcp/tools/smart_search/_scoring.py +0 -0
- {ha_mcp_dev-7.12.3.dev1888 → ha_mcp_dev-7.12.3.dev1894}/src/ha_mcp/tools/tool_search_hint_middleware.py +0 -0
- {ha_mcp_dev-7.12.3.dev1888 → ha_mcp_dev-7.12.3.dev1894}/src/ha_mcp/tools/tools_areas.py +0 -0
- {ha_mcp_dev-7.12.3.dev1888 → ha_mcp_dev-7.12.3.dev1894}/src/ha_mcp/tools/tools_blueprints.py +0 -0
- {ha_mcp_dev-7.12.3.dev1888 → ha_mcp_dev-7.12.3.dev1894}/src/ha_mcp/tools/tools_camera.py +0 -0
- {ha_mcp_dev-7.12.3.dev1888 → ha_mcp_dev-7.12.3.dev1894}/src/ha_mcp/tools/tools_categories.py +0 -0
- {ha_mcp_dev-7.12.3.dev1888 → ha_mcp_dev-7.12.3.dev1894}/src/ha_mcp/tools/tools_code.py +0 -0
- {ha_mcp_dev-7.12.3.dev1888 → ha_mcp_dev-7.12.3.dev1894}/src/ha_mcp/tools/tools_config_automations.py +0 -0
- {ha_mcp_dev-7.12.3.dev1888 → ha_mcp_dev-7.12.3.dev1894}/src/ha_mcp/tools/tools_config_dashboards.py +0 -0
- {ha_mcp_dev-7.12.3.dev1888 → ha_mcp_dev-7.12.3.dev1894}/src/ha_mcp/tools/tools_config_scripts.py +0 -0
- {ha_mcp_dev-7.12.3.dev1888 → ha_mcp_dev-7.12.3.dev1894}/src/ha_mcp/tools/tools_dashboard_screenshot.py +0 -0
- {ha_mcp_dev-7.12.3.dev1888 → ha_mcp_dev-7.12.3.dev1894}/src/ha_mcp/tools/tools_dev.py +0 -0
- {ha_mcp_dev-7.12.3.dev1888 → ha_mcp_dev-7.12.3.dev1894}/src/ha_mcp/tools/tools_energy.py +0 -0
- {ha_mcp_dev-7.12.3.dev1888 → ha_mcp_dev-7.12.3.dev1894}/src/ha_mcp/tools/tools_groups.py +0 -0
- {ha_mcp_dev-7.12.3.dev1888 → ha_mcp_dev-7.12.3.dev1894}/src/ha_mcp/tools/tools_hacs.py +0 -0
- {ha_mcp_dev-7.12.3.dev1888 → ha_mcp_dev-7.12.3.dev1894}/src/ha_mcp/tools/tools_integrations.py +0 -0
- {ha_mcp_dev-7.12.3.dev1888 → ha_mcp_dev-7.12.3.dev1894}/src/ha_mcp/tools/tools_labels.py +0 -0
- {ha_mcp_dev-7.12.3.dev1888 → ha_mcp_dev-7.12.3.dev1894}/src/ha_mcp/tools/tools_registry.py +0 -0
- {ha_mcp_dev-7.12.3.dev1888 → ha_mcp_dev-7.12.3.dev1894}/src/ha_mcp/tools/tools_resources.py +0 -0
- {ha_mcp_dev-7.12.3.dev1888 → ha_mcp_dev-7.12.3.dev1894}/src/ha_mcp/tools/tools_service.py +0 -0
- {ha_mcp_dev-7.12.3.dev1888 → ha_mcp_dev-7.12.3.dev1894}/src/ha_mcp/tools/tools_services.py +0 -0
- {ha_mcp_dev-7.12.3.dev1888 → ha_mcp_dev-7.12.3.dev1894}/src/ha_mcp/tools/tools_themes.py +0 -0
- {ha_mcp_dev-7.12.3.dev1888 → ha_mcp_dev-7.12.3.dev1894}/src/ha_mcp/tools/tools_todo.py +0 -0
- {ha_mcp_dev-7.12.3.dev1888 → ha_mcp_dev-7.12.3.dev1894}/src/ha_mcp/tools/tools_traces.py +0 -0
- {ha_mcp_dev-7.12.3.dev1888 → ha_mcp_dev-7.12.3.dev1894}/src/ha_mcp/tools/tools_updates.py +0 -0
- {ha_mcp_dev-7.12.3.dev1888 → ha_mcp_dev-7.12.3.dev1894}/src/ha_mcp/tools/tools_utility.py +0 -0
- {ha_mcp_dev-7.12.3.dev1888 → ha_mcp_dev-7.12.3.dev1894}/src/ha_mcp/tools/tools_voice_assistant.py +0 -0
- {ha_mcp_dev-7.12.3.dev1888 → ha_mcp_dev-7.12.3.dev1894}/src/ha_mcp/tools/tools_yaml_config.py +0 -0
- {ha_mcp_dev-7.12.3.dev1888 → ha_mcp_dev-7.12.3.dev1894}/src/ha_mcp/tools/validation_middleware.py +0 -0
- {ha_mcp_dev-7.12.3.dev1888 → ha_mcp_dev-7.12.3.dev1894}/src/ha_mcp/transforms/__init__.py +0 -0
- {ha_mcp_dev-7.12.3.dev1888 → ha_mcp_dev-7.12.3.dev1894}/src/ha_mcp/transforms/categorized_search.py +0 -0
- {ha_mcp_dev-7.12.3.dev1888 → ha_mcp_dev-7.12.3.dev1894}/src/ha_mcp/transforms/lite_docstrings.py +0 -0
- {ha_mcp_dev-7.12.3.dev1888 → ha_mcp_dev-7.12.3.dev1894}/src/ha_mcp/transport_security.py +0 -0
- {ha_mcp_dev-7.12.3.dev1888 → ha_mcp_dev-7.12.3.dev1894}/src/ha_mcp/update_check.py +0 -0
- {ha_mcp_dev-7.12.3.dev1888 → ha_mcp_dev-7.12.3.dev1894}/src/ha_mcp/utils/__init__.py +0 -0
- {ha_mcp_dev-7.12.3.dev1888 → ha_mcp_dev-7.12.3.dev1894}/src/ha_mcp/utils/config_hash.py +0 -0
- {ha_mcp_dev-7.12.3.dev1888 → ha_mcp_dev-7.12.3.dev1894}/src/ha_mcp/utils/data_paths.py +0 -0
- {ha_mcp_dev-7.12.3.dev1888 → ha_mcp_dev-7.12.3.dev1894}/src/ha_mcp/utils/domain_handlers.py +0 -0
- {ha_mcp_dev-7.12.3.dev1888 → ha_mcp_dev-7.12.3.dev1894}/src/ha_mcp/utils/fuzzy_search.py +0 -0
- {ha_mcp_dev-7.12.3.dev1888 → ha_mcp_dev-7.12.3.dev1894}/src/ha_mcp/utils/kill_signal_diagnostics.py +0 -0
- {ha_mcp_dev-7.12.3.dev1888 → ha_mcp_dev-7.12.3.dev1894}/src/ha_mcp/utils/operation_manager.py +0 -0
- {ha_mcp_dev-7.12.3.dev1888 → ha_mcp_dev-7.12.3.dev1894}/src/ha_mcp/utils/python_sandbox.py +0 -0
- {ha_mcp_dev-7.12.3.dev1888 → ha_mcp_dev-7.12.3.dev1894}/src/ha_mcp/utils/skill_loader.py +0 -0
- {ha_mcp_dev-7.12.3.dev1888 → ha_mcp_dev-7.12.3.dev1894}/src/ha_mcp/utils/usage_logger.py +0 -0
- {ha_mcp_dev-7.12.3.dev1888 → ha_mcp_dev-7.12.3.dev1894}/src/ha_mcp/visibility/__init__.py +0 -0
- {ha_mcp_dev-7.12.3.dev1888 → ha_mcp_dev-7.12.3.dev1894}/src/ha_mcp/visibility/model.py +0 -0
- {ha_mcp_dev-7.12.3.dev1888 → ha_mcp_dev-7.12.3.dev1894}/src/ha_mcp/visibility/persistence.py +0 -0
- {ha_mcp_dev-7.12.3.dev1888 → ha_mcp_dev-7.12.3.dev1894}/src/ha_mcp/visibility/resolver.py +0 -0
- {ha_mcp_dev-7.12.3.dev1888 → ha_mcp_dev-7.12.3.dev1894}/src/ha_mcp_dev.egg-info/SOURCES.txt +0 -0
- {ha_mcp_dev-7.12.3.dev1888 → ha_mcp_dev-7.12.3.dev1894}/src/ha_mcp_dev.egg-info/dependency_links.txt +0 -0
- {ha_mcp_dev-7.12.3.dev1888 → ha_mcp_dev-7.12.3.dev1894}/src/ha_mcp_dev.egg-info/entry_points.txt +0 -0
- {ha_mcp_dev-7.12.3.dev1888 → ha_mcp_dev-7.12.3.dev1894}/src/ha_mcp_dev.egg-info/requires.txt +0 -0
- {ha_mcp_dev-7.12.3.dev1888 → ha_mcp_dev-7.12.3.dev1894}/src/ha_mcp_dev.egg-info/top_level.txt +0 -0
- {ha_mcp_dev-7.12.3.dev1888 → ha_mcp_dev-7.12.3.dev1894}/tests/__init__.py +0 -0
- {ha_mcp_dev-7.12.3.dev1888 → ha_mcp_dev-7.12.3.dev1894}/tests/test_constants.py +0 -0
- {ha_mcp_dev-7.12.3.dev1888 → ha_mcp_dev-7.12.3.dev1894}/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.12.3.
|
|
7
|
+
version = "7.12.3.dev1894"
|
|
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
|
|
@@ -1388,8 +1388,12 @@ class HomeAssistantClient:
|
|
|
1388
1388
|
for attempt in range(max_retries):
|
|
1389
1389
|
try:
|
|
1390
1390
|
# Use per-client WebSocket keyed to this client's credentials
|
|
1391
|
+
# (verify_ssl included so a verify_ssl=False client never
|
|
1392
|
+
# shares a default-verification pooled connection).
|
|
1391
1393
|
ws_client = await get_websocket_client(
|
|
1392
|
-
url=self.base_url,
|
|
1394
|
+
url=self.base_url,
|
|
1395
|
+
token=self.token,
|
|
1396
|
+
verify_ssl=self.verify_ssl,
|
|
1393
1397
|
)
|
|
1394
1398
|
|
|
1395
1399
|
# Special handling for render_template which returns an event with the actual result
|
|
@@ -1642,9 +1646,17 @@ class HomeAssistantClient:
|
|
|
1642
1646
|
)
|
|
1643
1647
|
return bare_id
|
|
1644
1648
|
|
|
1645
|
-
async def get_scene_config(
|
|
1646
|
-
|
|
1647
|
-
|
|
1649
|
+
async def get_scene_config(
|
|
1650
|
+
self, scene_id: str, *, _resolved: bool = False
|
|
1651
|
+
) -> dict[str, Any]:
|
|
1652
|
+
"""Get Home Assistant scene configuration by scene_id.
|
|
1653
|
+
|
|
1654
|
+
``_resolved=True`` signals ``scene_id`` is already the resolved storage
|
|
1655
|
+
key (the caller ran :meth:`resolve_scene_id`), skipping the redundant
|
|
1656
|
+
registry lookup. ``resolve_scene_id`` is idempotent, so the result is
|
|
1657
|
+
identical either way.
|
|
1658
|
+
"""
|
|
1659
|
+
resolved_id = scene_id if _resolved else await self.resolve_scene_id(scene_id)
|
|
1648
1660
|
try:
|
|
1649
1661
|
endpoint = f"config/scene/config/{resolved_id}"
|
|
1650
1662
|
response = await self._request("GET", endpoint)
|
|
@@ -1662,10 +1674,15 @@ class HomeAssistantClient:
|
|
|
1662
1674
|
raise
|
|
1663
1675
|
|
|
1664
1676
|
async def upsert_scene_config(
|
|
1665
|
-
self, config: dict[str, Any], scene_id: str
|
|
1677
|
+
self, config: dict[str, Any], scene_id: str, *, _resolved: bool = False
|
|
1666
1678
|
) -> dict[str, Any]:
|
|
1667
|
-
"""Create or update Home Assistant scene configuration.
|
|
1668
|
-
|
|
1679
|
+
"""Create or update Home Assistant scene configuration.
|
|
1680
|
+
|
|
1681
|
+
``_resolved=True`` signals ``scene_id`` is already the resolved storage
|
|
1682
|
+
key, skipping the redundant registry lookup (``resolve_scene_id`` is
|
|
1683
|
+
idempotent, so the endpoint id is unchanged).
|
|
1684
|
+
"""
|
|
1685
|
+
resolved_id = scene_id if _resolved else await self.resolve_scene_id(scene_id)
|
|
1669
1686
|
try:
|
|
1670
1687
|
endpoint = f"config/scene/config/{resolved_id}"
|
|
1671
1688
|
|
|
@@ -1699,9 +1716,16 @@ class HomeAssistantClient:
|
|
|
1699
1716
|
logger.error(f"Failed to upsert scene config for {scene_id}: {e}")
|
|
1700
1717
|
raise
|
|
1701
1718
|
|
|
1702
|
-
async def delete_scene_config(
|
|
1703
|
-
|
|
1704
|
-
|
|
1719
|
+
async def delete_scene_config(
|
|
1720
|
+
self, scene_id: str, *, _resolved: bool = False
|
|
1721
|
+
) -> dict[str, Any]:
|
|
1722
|
+
"""Delete Home Assistant scene configuration.
|
|
1723
|
+
|
|
1724
|
+
``_resolved=True`` signals ``scene_id`` is already the resolved storage
|
|
1725
|
+
key, skipping the redundant registry lookup (``resolve_scene_id`` is
|
|
1726
|
+
idempotent, so the endpoint id is unchanged).
|
|
1727
|
+
"""
|
|
1728
|
+
resolved_id = scene_id if _resolved else await self.resolve_scene_id(scene_id)
|
|
1705
1729
|
try:
|
|
1706
1730
|
endpoint = f"config/scene/config/{resolved_id}"
|
|
1707
1731
|
response = await self._request("DELETE", endpoint)
|
{ha_mcp_dev-7.12.3.dev1888 → ha_mcp_dev-7.12.3.dev1894}/src/ha_mcp/client/websocket_client.py
RENAMED
|
@@ -1037,7 +1037,7 @@ class WebSocketManager:
|
|
|
1037
1037
|
_current_loop: asyncio.AbstractEventLoop | None = None
|
|
1038
1038
|
_lock: asyncio.Lock | None = None
|
|
1039
1039
|
_lock_loop: asyncio.AbstractEventLoop | None = None
|
|
1040
|
-
_client_factory: Callable[
|
|
1040
|
+
_client_factory: Callable[..., HomeAssistantWebSocketClient] | None = None
|
|
1041
1041
|
|
|
1042
1042
|
def __new__(cls) -> "WebSocketManager":
|
|
1043
1043
|
if cls._instance is None:
|
|
@@ -1052,8 +1052,7 @@ class WebSocketManager:
|
|
|
1052
1052
|
def configure(
|
|
1053
1053
|
self,
|
|
1054
1054
|
*,
|
|
1055
|
-
client_factory: Callable[
|
|
1056
|
-
| None = None,
|
|
1055
|
+
client_factory: Callable[..., HomeAssistantWebSocketClient] | None = None,
|
|
1057
1056
|
) -> None:
|
|
1058
1057
|
"""Configure the manager with injectable dependencies."""
|
|
1059
1058
|
if client_factory is not None:
|
|
@@ -1084,10 +1083,28 @@ class WebSocketManager:
|
|
|
1084
1083
|
"""Create a cache key from credentials."""
|
|
1085
1084
|
return hashlib.sha256(f"{url.rstrip('/')}:{token}".encode()).hexdigest()
|
|
1086
1085
|
|
|
1086
|
+
@staticmethod
|
|
1087
|
+
def _effective_verify_ssl(verify_ssl: bool | None) -> bool:
|
|
1088
|
+
"""Resolve the effective TLS-verification mode for the pool key."""
|
|
1089
|
+
if verify_ssl is not None:
|
|
1090
|
+
return verify_ssl
|
|
1091
|
+
try:
|
|
1092
|
+
return bool(get_global_settings().verify_ssl)
|
|
1093
|
+
except Exception as e:
|
|
1094
|
+
# Mirror HomeAssistantWebSocketClient.__init__: a bad env var
|
|
1095
|
+
# elsewhere should not crash pooling or silently flip TLS off.
|
|
1096
|
+
logger.warning(
|
|
1097
|
+
"Could not load settings while resolving the pool verify_ssl "
|
|
1098
|
+
"key (%s); falling back to verify_ssl=True.",
|
|
1099
|
+
e,
|
|
1100
|
+
)
|
|
1101
|
+
return True
|
|
1102
|
+
|
|
1087
1103
|
async def get_client(
|
|
1088
1104
|
self,
|
|
1089
1105
|
url: str | None = None,
|
|
1090
1106
|
token: str | None = None,
|
|
1107
|
+
verify_ssl: bool | None = None,
|
|
1091
1108
|
) -> HomeAssistantWebSocketClient:
|
|
1092
1109
|
"""Get WebSocket client, creating connection if needed.
|
|
1093
1110
|
|
|
@@ -1100,6 +1117,10 @@ class WebSocketManager:
|
|
|
1100
1117
|
credentials instead of global settings. This is required
|
|
1101
1118
|
for OAuth mode where each request has its own credentials.
|
|
1102
1119
|
token: Optional HA token. Must be provided with url.
|
|
1120
|
+
verify_ssl: TLS verification override, keyed into the pool so a
|
|
1121
|
+
``HomeAssistantClient(verify_ssl=False)`` caller never shares
|
|
1122
|
+
a connection built with default verification. ``None`` keeps
|
|
1123
|
+
the client's own settings-based default.
|
|
1103
1124
|
"""
|
|
1104
1125
|
current_loop = asyncio.get_event_loop()
|
|
1105
1126
|
|
|
@@ -1134,7 +1155,16 @@ class WebSocketManager:
|
|
|
1134
1155
|
ws_url = settings.homeassistant_url
|
|
1135
1156
|
ws_token = settings.homeassistant_token
|
|
1136
1157
|
|
|
1137
|
-
|
|
1158
|
+
# Key on the EFFECTIVE verification mode: a caller passing the
|
|
1159
|
+
# resolved settings default (send_websocket_message) must share
|
|
1160
|
+
# the pooled connection with callers that omit the argument
|
|
1161
|
+
# (listener, HACS, installer) — only a genuine override such as
|
|
1162
|
+
# verify_ssl=False gets its own isolated connection.
|
|
1163
|
+
effective_verify_ssl = self._effective_verify_ssl(verify_ssl)
|
|
1164
|
+
key = (
|
|
1165
|
+
f"{self._client_key(ws_url, ws_token)}"
|
|
1166
|
+
f"|verify_ssl={effective_verify_ssl}"
|
|
1167
|
+
)
|
|
1138
1168
|
|
|
1139
1169
|
# Return existing connected client for these credentials
|
|
1140
1170
|
existing = self._clients.get(key)
|
|
@@ -1148,7 +1178,11 @@ class WebSocketManager:
|
|
|
1148
1178
|
self._last_used.pop(key, None)
|
|
1149
1179
|
|
|
1150
1180
|
factory = self._client_factory or HomeAssistantWebSocketClient
|
|
1151
|
-
client =
|
|
1181
|
+
client = (
|
|
1182
|
+
factory(ws_url, ws_token)
|
|
1183
|
+
if verify_ssl is None
|
|
1184
|
+
else factory(ws_url, ws_token, verify_ssl=verify_ssl)
|
|
1185
|
+
)
|
|
1152
1186
|
|
|
1153
1187
|
connected = await client.connect()
|
|
1154
1188
|
if not connected:
|
|
@@ -1210,11 +1244,16 @@ websocket_manager = WebSocketManager()
|
|
|
1210
1244
|
async def get_websocket_client(
|
|
1211
1245
|
url: str | None = None,
|
|
1212
1246
|
token: str | None = None,
|
|
1247
|
+
verify_ssl: bool | None = None,
|
|
1213
1248
|
) -> HomeAssistantWebSocketClient:
|
|
1214
1249
|
"""Get the global WebSocket client instance.
|
|
1215
1250
|
|
|
1216
1251
|
Args:
|
|
1217
1252
|
url: Optional HA URL for per-client credentials (OAuth mode).
|
|
1218
1253
|
token: Optional HA token for per-client credentials (OAuth mode).
|
|
1254
|
+
verify_ssl: Optional TLS-verification override propagated into the
|
|
1255
|
+
pool key and client construction (None = settings default).
|
|
1219
1256
|
"""
|
|
1220
|
-
return await websocket_manager.get_client(
|
|
1257
|
+
return await websocket_manager.get_client(
|
|
1258
|
+
url=url, token=token, verify_ssl=verify_ssl
|
|
1259
|
+
)
|
|
@@ -8,8 +8,8 @@ from typing import Any
|
|
|
8
8
|
|
|
9
9
|
# Force UTF-8 encoding on Windows for Unicode output
|
|
10
10
|
if sys.platform == "win32":
|
|
11
|
-
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
|
|
12
|
-
sys.stderr.reconfigure(encoding="utf-8", errors="replace")
|
|
11
|
+
sys.stdout.reconfigure(encoding="utf-8", errors="replace") # type: ignore[union-attr,unused-ignore]
|
|
12
|
+
sys.stderr.reconfigure(encoding="utf-8", errors="replace") # type: ignore[union-attr,unused-ignore]
|
|
13
13
|
|
|
14
14
|
# Set dummy credentials before any imports try to use them
|
|
15
15
|
os.environ.setdefault("HOMEASSISTANT_URL", "http://smoke-test:8123")
|
{ha_mcp_dev-7.12.3.dev1888 → ha_mcp_dev-7.12.3.dev1894}/src/ha_mcp/stdio_settings_sidecar.py
RENAMED
|
@@ -125,7 +125,7 @@ def _pid_alive(pid: int) -> bool:
|
|
|
125
125
|
# ctypes.windll.kernel32 (no external deps).
|
|
126
126
|
import ctypes
|
|
127
127
|
|
|
128
|
-
kernel32 = ctypes.windll.kernel32 # type: ignore[attr-defined]
|
|
128
|
+
kernel32 = ctypes.windll.kernel32 # type: ignore[attr-defined,unused-ignore]
|
|
129
129
|
SYNCHRONIZE = 0x00100000
|
|
130
130
|
handle = kernel32.OpenProcess(SYNCHRONIZE, False, pid)
|
|
131
131
|
if not handle:
|
|
@@ -394,13 +394,13 @@ def _do_spawn() -> None:
|
|
|
394
394
|
# mode the user saw.
|
|
395
395
|
pythonw = Path(sys.executable).with_name("pythonw.exe")
|
|
396
396
|
cmd_python = str(pythonw) if pythonw.exists() else sys.executable
|
|
397
|
-
startupinfo = subprocess.STARTUPINFO() # type: ignore[attr-defined]
|
|
398
|
-
startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW # type: ignore[attr-defined]
|
|
399
|
-
startupinfo.wShowWindow = subprocess.SW_HIDE # type: ignore[attr-defined]
|
|
397
|
+
startupinfo = subprocess.STARTUPINFO() # type: ignore[attr-defined,unused-ignore]
|
|
398
|
+
startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW # type: ignore[attr-defined,unused-ignore]
|
|
399
|
+
startupinfo.wShowWindow = subprocess.SW_HIDE # type: ignore[attr-defined,unused-ignore]
|
|
400
400
|
popen_kwargs["startupinfo"] = startupinfo
|
|
401
401
|
popen_kwargs["creationflags"] = (
|
|
402
|
-
subprocess.CREATE_NEW_PROCESS_GROUP # type: ignore[attr-defined]
|
|
403
|
-
| subprocess.CREATE_NO_WINDOW # type: ignore[attr-defined]
|
|
402
|
+
subprocess.CREATE_NEW_PROCESS_GROUP # type: ignore[attr-defined,unused-ignore]
|
|
403
|
+
| subprocess.CREATE_NO_WINDOW # type: ignore[attr-defined,unused-ignore]
|
|
404
404
|
)
|
|
405
405
|
cmd = [cmd_python, "-m", "ha_mcp.stdio_settings_sidecar"]
|
|
406
406
|
else:
|
|
@@ -899,22 +899,15 @@ async def list_backups(client: HomeAssistantClient, limit: int = 200) -> dict[st
|
|
|
899
899
|
confirm a specific backup landed; they had to already know the ID. Newest
|
|
900
900
|
first. Read-only: no safety backup, no restart.
|
|
901
901
|
"""
|
|
902
|
-
ws_client = None
|
|
903
902
|
try:
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
"Failed to connect to Home Assistant WebSocket to list backups",
|
|
913
|
-
)
|
|
914
|
-
)
|
|
915
|
-
ws_client = cast(HomeAssistantWebSocketClient, ws_client)
|
|
916
|
-
|
|
917
|
-
info = await ws_client.send_command("backup/info")
|
|
903
|
+
# Route through the shared pooled WebSocket (issue #1813) rather than a
|
|
904
|
+
# dedicated connect/auth handshake per call. ``list_backups`` issues a
|
|
905
|
+
# single request/response ``backup/info`` command; the pooled client
|
|
906
|
+
# owns the connection lifecycle, so there is no per-call connect or
|
|
907
|
+
# disconnect. A failed command surfaces as ``success=False`` and is
|
|
908
|
+
# handled by the guard below (send_websocket_message never raises for
|
|
909
|
+
# WS command failures).
|
|
910
|
+
info = await client.send_websocket_message({"type": "backup/info"})
|
|
918
911
|
if not info.get("success"):
|
|
919
912
|
raise_tool_error(
|
|
920
913
|
create_error_response(
|
|
@@ -955,18 +948,6 @@ async def list_backups(client: HomeAssistantClient, limit: int = 200) -> dict[st
|
|
|
955
948
|
suggestions=["Check Home Assistant connection and the backup integration"],
|
|
956
949
|
)
|
|
957
950
|
return None # unreachable: exception_to_structured_error always raises
|
|
958
|
-
finally:
|
|
959
|
-
# Always disconnect WebSocket — narrow to transport errors; a
|
|
960
|
-
# programming error during cleanup should still surface.
|
|
961
|
-
if ws_client:
|
|
962
|
-
try:
|
|
963
|
-
await ws_client.disconnect()
|
|
964
|
-
except (TimeoutError, OSError, ConnectionError) as err:
|
|
965
|
-
logger.debug(
|
|
966
|
-
"ws disconnect (cleanup) transport error: %s: %s",
|
|
967
|
-
type(err).__name__,
|
|
968
|
-
err,
|
|
969
|
-
)
|
|
970
951
|
|
|
971
952
|
|
|
972
953
|
# Valid (scope, action) combinations. Anything outside this set is
|
|
@@ -22,10 +22,9 @@ from pydantic import Field
|
|
|
22
22
|
from websockets.asyncio.client import ClientConnection
|
|
23
23
|
|
|
24
24
|
from .._version import is_running_in_addon
|
|
25
|
-
from ..client.rest_client import HomeAssistantClient
|
|
25
|
+
from ..client.rest_client import HomeAssistantClient, HomeAssistantCommandError
|
|
26
26
|
from ..errors import (
|
|
27
27
|
ErrorCode,
|
|
28
|
-
create_connection_error,
|
|
29
28
|
create_error_response,
|
|
30
29
|
create_validation_error,
|
|
31
30
|
)
|
|
@@ -36,7 +35,6 @@ from ..utils.python_sandbox import (
|
|
|
36
35
|
)
|
|
37
36
|
from .helpers import (
|
|
38
37
|
exception_to_structured_error,
|
|
39
|
-
get_connected_ws_client,
|
|
40
38
|
log_tool_usage,
|
|
41
39
|
raise_tool_error,
|
|
42
40
|
validate_identifier_not_empty,
|
|
@@ -233,16 +231,7 @@ async def _supervisor_api_call(
|
|
|
233
231
|
Returns:
|
|
234
232
|
The "result" field from a successful response, or an error dict.
|
|
235
233
|
"""
|
|
236
|
-
ws_client = None
|
|
237
234
|
try:
|
|
238
|
-
ws_client, error = await get_connected_ws_client(
|
|
239
|
-
client.base_url, client.token, verify_ssl=client.verify_ssl
|
|
240
|
-
)
|
|
241
|
-
if error or ws_client is None:
|
|
242
|
-
return error or create_connection_error(
|
|
243
|
-
"Failed to establish WebSocket connection",
|
|
244
|
-
)
|
|
245
|
-
|
|
246
235
|
kwargs: dict[str, Any] = {"endpoint": endpoint, "method": method}
|
|
247
236
|
if data is not None:
|
|
248
237
|
kwargs["data"] = data
|
|
@@ -256,29 +245,20 @@ async def _supervisor_api_call(
|
|
|
256
245
|
kwargs["timeout"] = timeout
|
|
257
246
|
wait_timeout = float(timeout) + 15.0
|
|
258
247
|
|
|
259
|
-
|
|
260
|
-
|
|
248
|
+
# Route through the shared pooled WebSocket (issue #1813) rather than
|
|
249
|
+
# opening a dedicated connect/auth handshake per call. ``_wait_timeout``
|
|
250
|
+
# is consumed by send_command (it never reaches Home Assistant). The
|
|
251
|
+
# pooled path collapses a failed WS command into
|
|
252
|
+
# ``{"success": False, "error": ...}``; re-raise it as the same
|
|
253
|
+
# ``HomeAssistantCommandError`` the dedicated send_command used to raise
|
|
254
|
+
# so the classifier below maps schema/not-found/etc. identically.
|
|
255
|
+
result = await client.send_websocket_message(
|
|
256
|
+
{"type": "supervisor/api", "_wait_timeout": wait_timeout, **kwargs}
|
|
261
257
|
)
|
|
262
258
|
|
|
263
259
|
if not result.get("success"):
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
raise_tool_error(
|
|
267
|
-
create_error_response(
|
|
268
|
-
ErrorCode.RESOURCE_NOT_FOUND,
|
|
269
|
-
"Supervisor API not available",
|
|
270
|
-
details=str(result),
|
|
271
|
-
suggestions=[
|
|
272
|
-
"This feature requires Home Assistant OS or Supervised installation",
|
|
273
|
-
],
|
|
274
|
-
)
|
|
275
|
-
)
|
|
276
|
-
raise_tool_error(
|
|
277
|
-
create_error_response(
|
|
278
|
-
ErrorCode.SERVICE_CALL_FAILED,
|
|
279
|
-
f"Supervisor API call failed: {endpoint}",
|
|
280
|
-
details=str(result),
|
|
281
|
-
)
|
|
260
|
+
raise HomeAssistantCommandError(
|
|
261
|
+
str(result.get("error", f"Supervisor API call failed: {endpoint}"))
|
|
282
262
|
)
|
|
283
263
|
|
|
284
264
|
return {"success": True, "result": result.get("result", {})}
|
|
@@ -293,14 +273,6 @@ async def _supervisor_api_call(
|
|
|
293
273
|
suggestions=["Check Home Assistant connection and Supervisor availability"],
|
|
294
274
|
)
|
|
295
275
|
return None # unreachable: exception_to_structured_error always raises
|
|
296
|
-
finally:
|
|
297
|
-
if ws_client:
|
|
298
|
-
try:
|
|
299
|
-
await ws_client.disconnect()
|
|
300
|
-
except Exception:
|
|
301
|
-
# Best-effort cleanup: the WS connection is being torn down, so a
|
|
302
|
-
# disconnect failure is non-fatal and must not mask the real result.
|
|
303
|
-
pass
|
|
304
276
|
|
|
305
277
|
|
|
306
278
|
def _addon_connection_failure_suggestions(
|
{ha_mcp_dev-7.12.3.dev1888 → ha_mcp_dev-7.12.3.dev1894}/src/ha_mcp/tools/tools_bug_report.py
RENAMED
|
@@ -32,6 +32,7 @@ from ..utils.usage_logger import (
|
|
|
32
32
|
get_recent_logs,
|
|
33
33
|
get_startup_logs,
|
|
34
34
|
)
|
|
35
|
+
from .component_api import get_component_caps
|
|
35
36
|
from .helpers import log_tool_usage, register_tool_methods
|
|
36
37
|
from .util_helpers import ANSI_ESCAPE_RE
|
|
37
38
|
|
|
@@ -613,11 +614,21 @@ class BugReportTools:
|
|
|
613
614
|
async def _detect_component_version(self) -> str | None:
|
|
614
615
|
"""Read the ha_mcp_tools custom component's version, best-effort.
|
|
615
616
|
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
617
|
+
Prefers the shared cached capability probe (``get_component_caps`` — one
|
|
618
|
+
``ha_mcp_tools/info`` round-trip per client): a component new enough to
|
|
619
|
+
answer it (1.1.0+) reports its version there, so the report reuses that
|
|
620
|
+
cached probe instead of a bespoke round-trip. A component in the
|
|
621
|
+
0.11.0-1.1.0 band has services but no ``info`` command (caps is None),
|
|
622
|
+
so it falls back to the ``get_caller_token`` bootstrap service, whose
|
|
623
|
+
response also carries the manifest version. Returns None when the
|
|
624
|
+
component is not installed or the call fails — the report path must
|
|
625
|
+
never break on it.
|
|
620
626
|
"""
|
|
627
|
+
# get_component_caps never raises (it caches/returns None on every
|
|
628
|
+
# failure mode), so no guard is needed around it.
|
|
629
|
+
caps = await get_component_caps(self._client)
|
|
630
|
+
if caps is not None:
|
|
631
|
+
return caps.component_version or None
|
|
621
632
|
try:
|
|
622
633
|
resp = await self._client.call_service(
|
|
623
634
|
"ha_mcp_tools", "get_caller_token", {}, return_response=True
|
|
@@ -15,16 +15,20 @@ from fastmcp.exceptions import ToolError
|
|
|
15
15
|
from fastmcp.tools import tool
|
|
16
16
|
from pydantic import Field
|
|
17
17
|
|
|
18
|
+
from ..client.rest_client import (
|
|
19
|
+
HomeAssistantCommandError,
|
|
20
|
+
HomeAssistantConnectionError,
|
|
21
|
+
)
|
|
18
22
|
from ..errors import ErrorCode, create_error_response
|
|
19
23
|
from .auto_backup import with_auto_backup
|
|
20
24
|
from .helpers import (
|
|
21
25
|
exception_to_structured_error,
|
|
22
|
-
get_connected_ws_client,
|
|
23
26
|
log_tool_usage,
|
|
24
27
|
raise_tool_error,
|
|
25
28
|
register_tool_methods,
|
|
26
29
|
validate_identifier_not_empty,
|
|
27
30
|
)
|
|
31
|
+
from .util_helpers import is_connection_error_message
|
|
28
32
|
|
|
29
33
|
logger = logging.getLogger(__name__)
|
|
30
34
|
|
|
@@ -199,35 +203,23 @@ class CalendarTools:
|
|
|
199
203
|
if location is not None:
|
|
200
204
|
event["location"] = location
|
|
201
205
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
+
# Route through the shared pooled WebSocket (issue #1813) instead of a
|
|
207
|
+
# dedicated connect/auth handshake per call. The pooled path collapses a
|
|
208
|
+
# failed WS command into ``{"success": False, "error": ...}``; a
|
|
209
|
+
# transport-shaped failure re-raises as ``HomeAssistantConnectionError``
|
|
210
|
+
# (the classifier type-matches it to connectivity guidance), anything
|
|
211
|
+
# else as the same ``HomeAssistantCommandError`` the dedicated
|
|
212
|
+
# send_command used to raise so the caller's error handler still
|
|
213
|
+
# attaches the rrule-specific suggestions.
|
|
214
|
+
result = await self._client.send_websocket_message(
|
|
215
|
+
{"type": "calendar/event/create", "entity_id": entity_id, "event": event}
|
|
206
216
|
)
|
|
207
|
-
if
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
context={"entity_id": entity_id},
|
|
214
|
-
)
|
|
215
|
-
)
|
|
216
|
-
|
|
217
|
-
try:
|
|
218
|
-
return await ws_client.send_command(
|
|
219
|
-
"calendar/event/create", entity_id=entity_id, event=event
|
|
220
|
-
)
|
|
221
|
-
finally:
|
|
222
|
-
# Guard disconnect: a transport-teardown error here would
|
|
223
|
-
# otherwise replace the original send_command exception.
|
|
224
|
-
try:
|
|
225
|
-
await ws_client.disconnect()
|
|
226
|
-
except Exception as disconnect_error:
|
|
227
|
-
logger.debug(
|
|
228
|
-
f"WebSocket disconnect after create_event for "
|
|
229
|
-
f"{entity_id}: {disconnect_error}"
|
|
230
|
-
)
|
|
217
|
+
if not result.get("success"):
|
|
218
|
+
error = str(result.get("error", "calendar/event/create failed"))
|
|
219
|
+
if is_connection_error_message(error):
|
|
220
|
+
raise HomeAssistantConnectionError(error)
|
|
221
|
+
raise HomeAssistantCommandError(error)
|
|
222
|
+
return result
|
|
231
223
|
|
|
232
224
|
async def _create_simple_calendar_event(
|
|
233
225
|
self,
|
|
@@ -257,6 +249,13 @@ class CalendarTools:
|
|
|
257
249
|
self, entity_id: str, rrule: str | None, error: Exception
|
|
258
250
|
) -> list[str]:
|
|
259
251
|
"""Build suggestions for a failed ha_config_set_calendar_event call."""
|
|
252
|
+
if isinstance(error, HomeAssistantConnectionError):
|
|
253
|
+
# A transport drop is not a calendar problem — domain hints would
|
|
254
|
+
# send the agent chasing a non-issue during an HA restart.
|
|
255
|
+
return [
|
|
256
|
+
"Home Assistant may be restarting or unreachable — retry shortly",
|
|
257
|
+
"Check the connection to Home Assistant",
|
|
258
|
+
]
|
|
260
259
|
suggestions = [
|
|
261
260
|
f"Verify calendar entity '{entity_id}' exists and supports event creation",
|
|
262
261
|
"Check datetime format (ISO 8601)",
|
|
@@ -541,35 +540,21 @@ class CalendarTools:
|
|
|
541
540
|
if recurrence_range:
|
|
542
541
|
ws_kwargs["recurrence_range"] = recurrence_range
|
|
543
542
|
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
543
|
+
# Route through the shared pooled WebSocket (issue #1813) instead of a
|
|
544
|
+
# dedicated connect/auth handshake per call. Transport-shaped
|
|
545
|
+
# failures raise ``HomeAssistantConnectionError`` (connectivity
|
|
546
|
+
# guidance); anything else re-raises as the same
|
|
547
|
+
# ``HomeAssistantCommandError`` the dedicated send_command used to
|
|
548
|
+
# raise so the outer handler builds the delete-specific
|
|
549
|
+
# suggestions (404 / not-supported).
|
|
550
|
+
result = await self._client.send_websocket_message(
|
|
551
|
+
{"type": "calendar/event/delete", **ws_kwargs}
|
|
548
552
|
)
|
|
549
|
-
if
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
"Failed to connect to Home Assistant WebSocket",
|
|
555
|
-
context={"entity_id": entity_id, "uid": uid},
|
|
556
|
-
)
|
|
557
|
-
)
|
|
558
|
-
|
|
559
|
-
try:
|
|
560
|
-
result = await ws_client.send_command(
|
|
561
|
-
"calendar/event/delete", **ws_kwargs
|
|
562
|
-
)
|
|
563
|
-
finally:
|
|
564
|
-
# Guard disconnect: a transport-teardown error here would
|
|
565
|
-
# otherwise replace the original send_command exception.
|
|
566
|
-
try:
|
|
567
|
-
await ws_client.disconnect()
|
|
568
|
-
except Exception as disconnect_error:
|
|
569
|
-
logger.debug(
|
|
570
|
-
f"WebSocket disconnect after delete_event for "
|
|
571
|
-
f"{entity_id} uid={uid}: {disconnect_error}"
|
|
572
|
-
)
|
|
553
|
+
if not result.get("success"):
|
|
554
|
+
error = str(result.get("error", "calendar/event/delete failed"))
|
|
555
|
+
if is_connection_error_message(error):
|
|
556
|
+
raise HomeAssistantConnectionError(error)
|
|
557
|
+
raise HomeAssistantCommandError(error)
|
|
573
558
|
|
|
574
559
|
return {
|
|
575
560
|
"success": True,
|
|
@@ -586,28 +571,41 @@ class CalendarTools:
|
|
|
586
571
|
except Exception as error:
|
|
587
572
|
logger.error(f"Failed to delete calendar event from {entity_id}: {error}")
|
|
588
573
|
|
|
589
|
-
suggestions = [
|
|
590
|
-
f"Verify calendar entity '{entity_id}' exists",
|
|
591
|
-
f"Verify event with UID '{uid}' exists in the calendar",
|
|
592
|
-
"Use ha_config_get_calendar_events() to find the correct event UID",
|
|
593
|
-
"Some calendar integrations may not support event deletion",
|
|
594
|
-
]
|
|
595
|
-
|
|
596
|
-
error_str = str(error)
|
|
597
|
-
if "404" in error_str or "not found" in error_str.lower():
|
|
598
|
-
suggestions.insert(
|
|
599
|
-
0, f"Calendar entity '{entity_id}' or event '{uid}' not found"
|
|
600
|
-
)
|
|
601
|
-
if "not supported" in error_str.lower():
|
|
602
|
-
suggestions.insert(0, "This calendar does not support event deletion")
|
|
603
|
-
|
|
604
574
|
exception_to_structured_error(
|
|
605
575
|
error,
|
|
606
576
|
context={"entity_id": entity_id, "uid": uid},
|
|
607
|
-
suggestions=
|
|
577
|
+
suggestions=self._build_remove_calendar_event_error_suggestions(
|
|
578
|
+
entity_id, uid, error
|
|
579
|
+
),
|
|
608
580
|
)
|
|
609
581
|
return None # unreachable: exception_to_structured_error always raises
|
|
610
582
|
|
|
583
|
+
def _build_remove_calendar_event_error_suggestions(
|
|
584
|
+
self, entity_id: str, uid: str, error: Exception
|
|
585
|
+
) -> list[str]:
|
|
586
|
+
"""Build suggestions for a failed ha_config_remove_calendar_event call."""
|
|
587
|
+
if isinstance(error, HomeAssistantConnectionError):
|
|
588
|
+
# A transport drop is not a calendar problem — domain hints would
|
|
589
|
+
# send the agent chasing a non-issue during an HA restart.
|
|
590
|
+
return [
|
|
591
|
+
"Home Assistant may be restarting or unreachable — retry shortly",
|
|
592
|
+
"Check the connection to Home Assistant",
|
|
593
|
+
]
|
|
594
|
+
suggestions = [
|
|
595
|
+
f"Verify calendar entity '{entity_id}' exists",
|
|
596
|
+
f"Verify event with UID '{uid}' exists in the calendar",
|
|
597
|
+
"Use ha_config_get_calendar_events() to find the correct event UID",
|
|
598
|
+
"Some calendar integrations may not support event deletion",
|
|
599
|
+
]
|
|
600
|
+
error_str = str(error)
|
|
601
|
+
if "404" in error_str or "not found" in error_str.lower():
|
|
602
|
+
suggestions.insert(
|
|
603
|
+
0, f"Calendar entity '{entity_id}' or event '{uid}' not found"
|
|
604
|
+
)
|
|
605
|
+
if "not supported" in error_str.lower():
|
|
606
|
+
suggestions.insert(0, "This calendar does not support event deletion")
|
|
607
|
+
return suggestions
|
|
608
|
+
|
|
611
609
|
|
|
612
610
|
def register_calendar_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
|
|
613
611
|
"""Register calendar management tools with the MCP server."""
|