ha-mcp-dev 7.12.3.dev1885__tar.gz → 7.12.3.dev1891__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.dev1885/src/ha_mcp_dev.egg-info → ha_mcp_dev-7.12.3.dev1891}/PKG-INFO +1 -1
- {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/pyproject.toml +1 -1
- {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/client/rest_client.py +34 -10
- {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/client/websocket_client.py +20 -5
- {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/smoke_test.py +2 -2
- {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/stdio_settings_sidecar.py +6 -6
- {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/tools/backup.py +8 -27
- {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/tools/tools_addons.py +12 -40
- {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/tools/tools_bug_report.py +15 -4
- {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/tools/tools_calendar.py +23 -55
- {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/tools/tools_config_helpers.py +276 -5
- {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/tools/tools_config_scenes.py +17 -6
- {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/tools/tools_entities.py +693 -148
- {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/tools/tools_filesystem.py +34 -1
- {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/tools/tools_history.py +111 -87
- {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/tools/tools_mcp_component.py +14 -2
- {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/tools/tools_radio.py +9 -5
- {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/tools/tools_search.py +11 -12
- {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/tools/tools_system.py +158 -12
- {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/tools/tools_zones.py +185 -35
- {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891/src/ha_mcp_dev.egg-info}/PKG-INFO +1 -1
- {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/LICENSE +0 -0
- {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/MANIFEST.in +0 -0
- {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/README.md +0 -0
- {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/setup.cfg +0 -0
- {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/__init__.py +0 -0
- {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/__main__.py +0 -0
- {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/_pypi_marker +0 -0
- {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/_version.py +0 -0
- {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/auth/__init__.py +0 -0
- {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/auth/consent_form.py +0 -0
- {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/auth/provider.py +0 -0
- {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/backup_manager.py +0 -0
- {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/browser_landing.py +0 -0
- {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/client/__init__.py +0 -0
- {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/client/supervisor_client.py +0 -0
- {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/client/websocket_listener.py +0 -0
- {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/config.py +0 -0
- {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/dashboard_screenshot/__init__.py +0 -0
- {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/dashboard_screenshot/capture.py +0 -0
- {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/dashboard_screenshot/provision.py +0 -0
- {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/errors.py +0 -0
- {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/llm_exposure.py +0 -0
- {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/policy/__init__.py +0 -0
- {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/policy/approval_queue.py +0 -0
- {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/policy/evaluator.py +0 -0
- {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/policy/handlers.py +0 -0
- {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/policy/middleware.py +0 -0
- {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/policy/model.py +0 -0
- {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/policy/persistence.py +0 -0
- {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/policy/value_sources.py +0 -0
- {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/py.typed +0 -0
- {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/read_only.py +0 -0
- {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/resources/skills-vendor/.claude/settings.json +0 -0
- {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/resources/skills-vendor/.claude-plugin/marketplace.json +0 -0
- {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/resources/skills-vendor/.claude-plugin/plugin.json +0 -0
- {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/resources/skills-vendor/.github/ISSUE_TEMPLATE/skill-rca.md +0 -0
- {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/resources/skills-vendor/.github/pull_request_template.md +0 -0
- {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/resources/skills-vendor/AGENTS.md +0 -0
- {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/resources/skills-vendor/CLAUDE.md +0 -0
- {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/resources/skills-vendor/CONTRIBUTING.md +0 -0
- {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/resources/skills-vendor/LICENSE +0 -0
- {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/resources/skills-vendor/README.md +0 -0
- {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/SKILL.md +0 -0
- {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/evals/evals.json +0 -0
- {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/appdaemon.md +0 -0
- {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/automation-patterns.md +0 -0
- {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/blueprint-guide.md +0 -0
- {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-cards.md +0 -0
- {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-guide.md +0 -0
- {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/device-control.md +0 -0
- {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/domain-docs.md +0 -0
- {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/examples.yaml +0 -0
- {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/helper-selection.md +0 -0
- {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/safe-refactoring.md +0 -0
- {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/scenes.md +0 -0
- {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/template-guidelines.md +0 -0
- {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/yaml-only-integrations.md +0 -0
- {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/server.py +0 -0
- {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/settings_ui/__init__.py +0 -0
- {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/settings_ui/settings.css +0 -0
- {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/settings_ui/settings.html +0 -0
- {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/settings_ui/settings.js +0 -0
- {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/strict_bps.py +0 -0
- {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/tools/__init__.py +0 -0
- {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/tools/auto_backup.py +0 -0
- {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/tools/best_practice_checker.py +0 -0
- {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/tools/component_api.py +0 -0
- {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/tools/config_entry_flow.py +0 -0
- {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/tools/device_control.py +0 -0
- {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/tools/enhanced.py +0 -0
- {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/tools/helpers.py +0 -0
- {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/tools/radio/__init__.py +0 -0
- {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/tools/radio/base.py +0 -0
- {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/tools/radio/matter.py +0 -0
- {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/tools/radio/thread.py +0 -0
- {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/tools/radio/zigbee.py +0 -0
- {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/tools/radio/zwave.py +0 -0
- {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/tools/reference_validator.py +0 -0
- {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/tools/registry.py +0 -0
- {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/tools/smart_search/__init__.py +0 -0
- {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/tools/smart_search/_base.py +0 -0
- {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/tools/smart_search/_config.py +0 -0
- {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/tools/smart_search/_deep.py +0 -0
- {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/tools/smart_search/_entities.py +0 -0
- {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/tools/smart_search/_fetch.py +0 -0
- {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/tools/smart_search/_overview.py +0 -0
- {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/tools/smart_search/_scenes.py +0 -0
- {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/tools/smart_search/_scoring.py +0 -0
- {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/tools/tool_search_hint_middleware.py +0 -0
- {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/tools/tools_areas.py +0 -0
- {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/tools/tools_blueprints.py +0 -0
- {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/tools/tools_camera.py +0 -0
- {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/tools/tools_categories.py +0 -0
- {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/tools/tools_code.py +0 -0
- {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/tools/tools_config_automations.py +0 -0
- {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/tools/tools_config_dashboards.py +0 -0
- {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/tools/tools_config_scripts.py +0 -0
- {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/tools/tools_dashboard_screenshot.py +0 -0
- {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/tools/tools_dev.py +0 -0
- {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/tools/tools_energy.py +0 -0
- {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/tools/tools_groups.py +0 -0
- {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/tools/tools_hacs.py +0 -0
- {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/tools/tools_integrations.py +0 -0
- {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/tools/tools_labels.py +0 -0
- {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/tools/tools_registry.py +0 -0
- {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/tools/tools_resources.py +0 -0
- {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/tools/tools_service.py +0 -0
- {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/tools/tools_services.py +0 -0
- {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/tools/tools_themes.py +0 -0
- {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/tools/tools_todo.py +0 -0
- {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/tools/tools_traces.py +0 -0
- {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/tools/tools_updates.py +0 -0
- {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/tools/tools_utility.py +0 -0
- {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/tools/tools_voice_assistant.py +0 -0
- {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/tools/tools_yaml_config.py +0 -0
- {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/tools/util_helpers.py +0 -0
- {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/tools/validation_middleware.py +0 -0
- {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/transforms/__init__.py +0 -0
- {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/transforms/categorized_search.py +0 -0
- {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/transforms/lite_docstrings.py +0 -0
- {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/transport_security.py +0 -0
- {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/update_check.py +0 -0
- {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/utils/__init__.py +0 -0
- {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/utils/config_hash.py +0 -0
- {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/utils/data_paths.py +0 -0
- {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/utils/domain_handlers.py +0 -0
- {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/utils/fuzzy_search.py +0 -0
- {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/utils/kill_signal_diagnostics.py +0 -0
- {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/utils/operation_manager.py +0 -0
- {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/utils/python_sandbox.py +0 -0
- {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/utils/skill_loader.py +0 -0
- {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/utils/usage_logger.py +0 -0
- {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/visibility/__init__.py +0 -0
- {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/visibility/model.py +0 -0
- {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/visibility/persistence.py +0 -0
- {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp/visibility/resolver.py +0 -0
- {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp_dev.egg-info/SOURCES.txt +0 -0
- {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp_dev.egg-info/dependency_links.txt +0 -0
- {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp_dev.egg-info/entry_points.txt +0 -0
- {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp_dev.egg-info/requires.txt +0 -0
- {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/src/ha_mcp_dev.egg-info/top_level.txt +0 -0
- {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/tests/__init__.py +0 -0
- {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/tests/test_constants.py +0 -0
- {ha_mcp_dev-7.12.3.dev1885 → ha_mcp_dev-7.12.3.dev1891}/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.dev1891"
|
|
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.dev1885 → ha_mcp_dev-7.12.3.dev1891}/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:
|
|
@@ -1088,6 +1087,7 @@ class WebSocketManager:
|
|
|
1088
1087
|
self,
|
|
1089
1088
|
url: str | None = None,
|
|
1090
1089
|
token: str | None = None,
|
|
1090
|
+
verify_ssl: bool | None = None,
|
|
1091
1091
|
) -> HomeAssistantWebSocketClient:
|
|
1092
1092
|
"""Get WebSocket client, creating connection if needed.
|
|
1093
1093
|
|
|
@@ -1100,6 +1100,10 @@ class WebSocketManager:
|
|
|
1100
1100
|
credentials instead of global settings. This is required
|
|
1101
1101
|
for OAuth mode where each request has its own credentials.
|
|
1102
1102
|
token: Optional HA token. Must be provided with url.
|
|
1103
|
+
verify_ssl: TLS verification override, keyed into the pool so a
|
|
1104
|
+
``HomeAssistantClient(verify_ssl=False)`` caller never shares
|
|
1105
|
+
a connection built with default verification. ``None`` keeps
|
|
1106
|
+
the client's own settings-based default.
|
|
1103
1107
|
"""
|
|
1104
1108
|
current_loop = asyncio.get_event_loop()
|
|
1105
1109
|
|
|
@@ -1135,6 +1139,8 @@ class WebSocketManager:
|
|
|
1135
1139
|
ws_token = settings.homeassistant_token
|
|
1136
1140
|
|
|
1137
1141
|
key = self._client_key(ws_url, ws_token)
|
|
1142
|
+
if verify_ssl is not None:
|
|
1143
|
+
key = f"{key}|verify_ssl={verify_ssl}"
|
|
1138
1144
|
|
|
1139
1145
|
# Return existing connected client for these credentials
|
|
1140
1146
|
existing = self._clients.get(key)
|
|
@@ -1148,7 +1154,11 @@ class WebSocketManager:
|
|
|
1148
1154
|
self._last_used.pop(key, None)
|
|
1149
1155
|
|
|
1150
1156
|
factory = self._client_factory or HomeAssistantWebSocketClient
|
|
1151
|
-
client =
|
|
1157
|
+
client = (
|
|
1158
|
+
factory(ws_url, ws_token)
|
|
1159
|
+
if verify_ssl is None
|
|
1160
|
+
else factory(ws_url, ws_token, verify_ssl=verify_ssl)
|
|
1161
|
+
)
|
|
1152
1162
|
|
|
1153
1163
|
connected = await client.connect()
|
|
1154
1164
|
if not connected:
|
|
@@ -1210,11 +1220,16 @@ websocket_manager = WebSocketManager()
|
|
|
1210
1220
|
async def get_websocket_client(
|
|
1211
1221
|
url: str | None = None,
|
|
1212
1222
|
token: str | None = None,
|
|
1223
|
+
verify_ssl: bool | None = None,
|
|
1213
1224
|
) -> HomeAssistantWebSocketClient:
|
|
1214
1225
|
"""Get the global WebSocket client instance.
|
|
1215
1226
|
|
|
1216
1227
|
Args:
|
|
1217
1228
|
url: Optional HA URL for per-client credentials (OAuth mode).
|
|
1218
1229
|
token: Optional HA token for per-client credentials (OAuth mode).
|
|
1230
|
+
verify_ssl: Optional TLS-verification override propagated into the
|
|
1231
|
+
pool key and client construction (None = settings default).
|
|
1219
1232
|
"""
|
|
1220
|
-
return await websocket_manager.get_client(
|
|
1233
|
+
return await websocket_manager.get_client(
|
|
1234
|
+
url=url, token=token, verify_ssl=verify_ssl
|
|
1235
|
+
)
|
|
@@ -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.dev1885 → ha_mcp_dev-7.12.3.dev1891}/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.dev1885 → ha_mcp_dev-7.12.3.dev1891}/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,11 +15,11 @@ 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 HomeAssistantCommandError
|
|
18
19
|
from ..errors import ErrorCode, create_error_response
|
|
19
20
|
from .auto_backup import with_auto_backup
|
|
20
21
|
from .helpers import (
|
|
21
22
|
exception_to_structured_error,
|
|
22
|
-
get_connected_ws_client,
|
|
23
23
|
log_tool_usage,
|
|
24
24
|
raise_tool_error,
|
|
25
25
|
register_tool_methods,
|
|
@@ -199,35 +199,20 @@ class CalendarTools:
|
|
|
199
199
|
if location is not None:
|
|
200
200
|
event["location"] = location
|
|
201
201
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
202
|
+
# Route through the shared pooled WebSocket (issue #1813) instead of a
|
|
203
|
+
# dedicated connect/auth handshake per call. The pooled path collapses a
|
|
204
|
+
# failed WS command into ``{"success": False, "error": ...}``; re-raise
|
|
205
|
+
# it as the same ``HomeAssistantCommandError`` the dedicated send_command
|
|
206
|
+
# used to raise so the caller's error handler still attaches the
|
|
207
|
+
# rrule-specific suggestions.
|
|
208
|
+
result = await self._client.send_websocket_message(
|
|
209
|
+
{"type": "calendar/event/create", "entity_id": entity_id, "event": event}
|
|
206
210
|
)
|
|
207
|
-
if
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
or create_error_response(
|
|
211
|
-
ErrorCode.CONNECTION_FAILED,
|
|
212
|
-
"Failed to connect to Home Assistant WebSocket",
|
|
213
|
-
context={"entity_id": entity_id},
|
|
214
|
-
)
|
|
211
|
+
if not result.get("success"):
|
|
212
|
+
raise HomeAssistantCommandError(
|
|
213
|
+
str(result.get("error", "calendar/event/create failed"))
|
|
215
214
|
)
|
|
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
|
-
)
|
|
215
|
+
return result
|
|
231
216
|
|
|
232
217
|
async def _create_simple_calendar_event(
|
|
233
218
|
self,
|
|
@@ -541,36 +526,19 @@ class CalendarTools:
|
|
|
541
526
|
if recurrence_range:
|
|
542
527
|
ws_kwargs["recurrence_range"] = recurrence_range
|
|
543
528
|
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
529
|
+
# Route through the shared pooled WebSocket (issue #1813) instead of a
|
|
530
|
+
# dedicated connect/auth handshake per call. Re-raise a failed WS
|
|
531
|
+
# command as the same ``HomeAssistantCommandError`` the dedicated
|
|
532
|
+
# send_command used to raise so the outer handler builds the
|
|
533
|
+
# delete-specific suggestions (404 / not-supported).
|
|
534
|
+
result = await self._client.send_websocket_message(
|
|
535
|
+
{"type": "calendar/event/delete", **ws_kwargs}
|
|
548
536
|
)
|
|
549
|
-
if
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
or create_error_response(
|
|
553
|
-
ErrorCode.CONNECTION_FAILED,
|
|
554
|
-
"Failed to connect to Home Assistant WebSocket",
|
|
555
|
-
context={"entity_id": entity_id, "uid": uid},
|
|
556
|
-
)
|
|
537
|
+
if not result.get("success"):
|
|
538
|
+
raise HomeAssistantCommandError(
|
|
539
|
+
str(result.get("error", "calendar/event/delete failed"))
|
|
557
540
|
)
|
|
558
541
|
|
|
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
|
-
)
|
|
573
|
-
|
|
574
542
|
return {
|
|
575
543
|
"success": True,
|
|
576
544
|
"entity_id": entity_id,
|