ha-mcp-dev 7.8.1.dev715__tar.gz → 7.8.1.dev717__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.8.1.dev715/src/ha_mcp_dev.egg-info → ha_mcp_dev-7.8.1.dev717}/PKG-INFO +2 -1
- {ha_mcp_dev-7.8.1.dev715 → ha_mcp_dev-7.8.1.dev717}/pyproject.toml +2 -1
- {ha_mcp_dev-7.8.1.dev715 → ha_mcp_dev-7.8.1.dev717}/src/ha_mcp/__main__.py +20 -1
- {ha_mcp_dev-7.8.1.dev715 → ha_mcp_dev-7.8.1.dev717}/src/ha_mcp/read_only.py +2 -2
- {ha_mcp_dev-7.8.1.dev715 → ha_mcp_dev-7.8.1.dev717}/src/ha_mcp/tools/tools_addons.py +39 -12
- {ha_mcp_dev-7.8.1.dev715 → ha_mcp_dev-7.8.1.dev717}/src/ha_mcp/tools/tools_search.py +24 -5
- {ha_mcp_dev-7.8.1.dev715 → ha_mcp_dev-7.8.1.dev717}/src/ha_mcp/tools/tools_system.py +19 -0
- {ha_mcp_dev-7.8.1.dev715 → ha_mcp_dev-7.8.1.dev717}/src/ha_mcp/tools/tools_updates.py +19 -1
- ha_mcp_dev-7.8.1.dev717/src/ha_mcp/update_check.py +261 -0
- {ha_mcp_dev-7.8.1.dev715 → ha_mcp_dev-7.8.1.dev717/src/ha_mcp_dev.egg-info}/PKG-INFO +2 -1
- {ha_mcp_dev-7.8.1.dev715 → ha_mcp_dev-7.8.1.dev717}/src/ha_mcp_dev.egg-info/SOURCES.txt +1 -0
- {ha_mcp_dev-7.8.1.dev715 → ha_mcp_dev-7.8.1.dev717}/src/ha_mcp_dev.egg-info/requires.txt +1 -0
- {ha_mcp_dev-7.8.1.dev715 → ha_mcp_dev-7.8.1.dev717}/LICENSE +0 -0
- {ha_mcp_dev-7.8.1.dev715 → ha_mcp_dev-7.8.1.dev717}/MANIFEST.in +0 -0
- {ha_mcp_dev-7.8.1.dev715 → ha_mcp_dev-7.8.1.dev717}/README.md +0 -0
- {ha_mcp_dev-7.8.1.dev715 → ha_mcp_dev-7.8.1.dev717}/setup.cfg +0 -0
- {ha_mcp_dev-7.8.1.dev715 → ha_mcp_dev-7.8.1.dev717}/src/ha_mcp/__init__.py +0 -0
- {ha_mcp_dev-7.8.1.dev715 → ha_mcp_dev-7.8.1.dev717}/src/ha_mcp/_pypi_marker +0 -0
- {ha_mcp_dev-7.8.1.dev715 → ha_mcp_dev-7.8.1.dev717}/src/ha_mcp/_version.py +0 -0
- {ha_mcp_dev-7.8.1.dev715 → ha_mcp_dev-7.8.1.dev717}/src/ha_mcp/auth/__init__.py +0 -0
- {ha_mcp_dev-7.8.1.dev715 → ha_mcp_dev-7.8.1.dev717}/src/ha_mcp/auth/consent_form.py +0 -0
- {ha_mcp_dev-7.8.1.dev715 → ha_mcp_dev-7.8.1.dev717}/src/ha_mcp/auth/provider.py +0 -0
- {ha_mcp_dev-7.8.1.dev715 → ha_mcp_dev-7.8.1.dev717}/src/ha_mcp/backup_manager.py +0 -0
- {ha_mcp_dev-7.8.1.dev715 → ha_mcp_dev-7.8.1.dev717}/src/ha_mcp/client/__init__.py +0 -0
- {ha_mcp_dev-7.8.1.dev715 → ha_mcp_dev-7.8.1.dev717}/src/ha_mcp/client/rest_client.py +0 -0
- {ha_mcp_dev-7.8.1.dev715 → ha_mcp_dev-7.8.1.dev717}/src/ha_mcp/client/supervisor_client.py +0 -0
- {ha_mcp_dev-7.8.1.dev715 → ha_mcp_dev-7.8.1.dev717}/src/ha_mcp/client/websocket_client.py +0 -0
- {ha_mcp_dev-7.8.1.dev715 → ha_mcp_dev-7.8.1.dev717}/src/ha_mcp/client/websocket_listener.py +0 -0
- {ha_mcp_dev-7.8.1.dev715 → ha_mcp_dev-7.8.1.dev717}/src/ha_mcp/config.py +0 -0
- {ha_mcp_dev-7.8.1.dev715 → ha_mcp_dev-7.8.1.dev717}/src/ha_mcp/dashboard_screenshot/__init__.py +0 -0
- {ha_mcp_dev-7.8.1.dev715 → ha_mcp_dev-7.8.1.dev717}/src/ha_mcp/dashboard_screenshot/capture.py +0 -0
- {ha_mcp_dev-7.8.1.dev715 → ha_mcp_dev-7.8.1.dev717}/src/ha_mcp/dashboard_screenshot/provision.py +0 -0
- {ha_mcp_dev-7.8.1.dev715 → ha_mcp_dev-7.8.1.dev717}/src/ha_mcp/errors.py +0 -0
- {ha_mcp_dev-7.8.1.dev715 → ha_mcp_dev-7.8.1.dev717}/src/ha_mcp/policy/__init__.py +0 -0
- {ha_mcp_dev-7.8.1.dev715 → ha_mcp_dev-7.8.1.dev717}/src/ha_mcp/policy/approval_queue.py +0 -0
- {ha_mcp_dev-7.8.1.dev715 → ha_mcp_dev-7.8.1.dev717}/src/ha_mcp/policy/evaluator.py +0 -0
- {ha_mcp_dev-7.8.1.dev715 → ha_mcp_dev-7.8.1.dev717}/src/ha_mcp/policy/handlers.py +0 -0
- {ha_mcp_dev-7.8.1.dev715 → ha_mcp_dev-7.8.1.dev717}/src/ha_mcp/policy/middleware.py +0 -0
- {ha_mcp_dev-7.8.1.dev715 → ha_mcp_dev-7.8.1.dev717}/src/ha_mcp/policy/model.py +0 -0
- {ha_mcp_dev-7.8.1.dev715 → ha_mcp_dev-7.8.1.dev717}/src/ha_mcp/policy/persistence.py +0 -0
- {ha_mcp_dev-7.8.1.dev715 → ha_mcp_dev-7.8.1.dev717}/src/ha_mcp/policy/value_sources.py +0 -0
- {ha_mcp_dev-7.8.1.dev715 → ha_mcp_dev-7.8.1.dev717}/src/ha_mcp/py.typed +0 -0
- {ha_mcp_dev-7.8.1.dev715 → ha_mcp_dev-7.8.1.dev717}/src/ha_mcp/resources/skills-vendor/.claude/settings.json +0 -0
- {ha_mcp_dev-7.8.1.dev715 → ha_mcp_dev-7.8.1.dev717}/src/ha_mcp/resources/skills-vendor/.claude-plugin/marketplace.json +0 -0
- {ha_mcp_dev-7.8.1.dev715 → ha_mcp_dev-7.8.1.dev717}/src/ha_mcp/resources/skills-vendor/.claude-plugin/plugin.json +0 -0
- {ha_mcp_dev-7.8.1.dev715 → ha_mcp_dev-7.8.1.dev717}/src/ha_mcp/resources/skills-vendor/.github/ISSUE_TEMPLATE/skill-rca.md +0 -0
- {ha_mcp_dev-7.8.1.dev715 → ha_mcp_dev-7.8.1.dev717}/src/ha_mcp/resources/skills-vendor/.github/pull_request_template.md +0 -0
- {ha_mcp_dev-7.8.1.dev715 → ha_mcp_dev-7.8.1.dev717}/src/ha_mcp/resources/skills-vendor/AGENTS.md +0 -0
- {ha_mcp_dev-7.8.1.dev715 → ha_mcp_dev-7.8.1.dev717}/src/ha_mcp/resources/skills-vendor/CLAUDE.md +0 -0
- {ha_mcp_dev-7.8.1.dev715 → ha_mcp_dev-7.8.1.dev717}/src/ha_mcp/resources/skills-vendor/CONTRIBUTING.md +0 -0
- {ha_mcp_dev-7.8.1.dev715 → ha_mcp_dev-7.8.1.dev717}/src/ha_mcp/resources/skills-vendor/LICENSE +0 -0
- {ha_mcp_dev-7.8.1.dev715 → ha_mcp_dev-7.8.1.dev717}/src/ha_mcp/resources/skills-vendor/README.md +0 -0
- {ha_mcp_dev-7.8.1.dev715 → ha_mcp_dev-7.8.1.dev717}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/SKILL.md +0 -0
- {ha_mcp_dev-7.8.1.dev715 → ha_mcp_dev-7.8.1.dev717}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/evals/evals.json +0 -0
- {ha_mcp_dev-7.8.1.dev715 → ha_mcp_dev-7.8.1.dev717}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/automation-patterns.md +0 -0
- {ha_mcp_dev-7.8.1.dev715 → ha_mcp_dev-7.8.1.dev717}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-cards.md +0 -0
- {ha_mcp_dev-7.8.1.dev715 → ha_mcp_dev-7.8.1.dev717}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-guide.md +0 -0
- {ha_mcp_dev-7.8.1.dev715 → ha_mcp_dev-7.8.1.dev717}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/device-control.md +0 -0
- {ha_mcp_dev-7.8.1.dev715 → ha_mcp_dev-7.8.1.dev717}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/domain-docs.md +0 -0
- {ha_mcp_dev-7.8.1.dev715 → ha_mcp_dev-7.8.1.dev717}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/examples.yaml +0 -0
- {ha_mcp_dev-7.8.1.dev715 → ha_mcp_dev-7.8.1.dev717}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/helper-selection.md +0 -0
- {ha_mcp_dev-7.8.1.dev715 → ha_mcp_dev-7.8.1.dev717}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/safe-refactoring.md +0 -0
- {ha_mcp_dev-7.8.1.dev715 → ha_mcp_dev-7.8.1.dev717}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/template-guidelines.md +0 -0
- {ha_mcp_dev-7.8.1.dev715 → ha_mcp_dev-7.8.1.dev717}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/yaml-only-integrations.md +0 -0
- {ha_mcp_dev-7.8.1.dev715 → ha_mcp_dev-7.8.1.dev717}/src/ha_mcp/server.py +0 -0
- {ha_mcp_dev-7.8.1.dev715 → ha_mcp_dev-7.8.1.dev717}/src/ha_mcp/settings.css +0 -0
- {ha_mcp_dev-7.8.1.dev715 → ha_mcp_dev-7.8.1.dev717}/src/ha_mcp/settings.js +0 -0
- {ha_mcp_dev-7.8.1.dev715 → ha_mcp_dev-7.8.1.dev717}/src/ha_mcp/settings_ui.py +0 -0
- {ha_mcp_dev-7.8.1.dev715 → ha_mcp_dev-7.8.1.dev717}/src/ha_mcp/smoke_test.py +0 -0
- {ha_mcp_dev-7.8.1.dev715 → ha_mcp_dev-7.8.1.dev717}/src/ha_mcp/stdio_settings_sidecar.py +0 -0
- {ha_mcp_dev-7.8.1.dev715 → ha_mcp_dev-7.8.1.dev717}/src/ha_mcp/tools/__init__.py +0 -0
- {ha_mcp_dev-7.8.1.dev715 → ha_mcp_dev-7.8.1.dev717}/src/ha_mcp/tools/auto_backup.py +0 -0
- {ha_mcp_dev-7.8.1.dev715 → ha_mcp_dev-7.8.1.dev717}/src/ha_mcp/tools/backup.py +0 -0
- {ha_mcp_dev-7.8.1.dev715 → ha_mcp_dev-7.8.1.dev717}/src/ha_mcp/tools/best_practice_checker.py +0 -0
- {ha_mcp_dev-7.8.1.dev715 → ha_mcp_dev-7.8.1.dev717}/src/ha_mcp/tools/config_entry_flow.py +0 -0
- {ha_mcp_dev-7.8.1.dev715 → ha_mcp_dev-7.8.1.dev717}/src/ha_mcp/tools/device_control.py +0 -0
- {ha_mcp_dev-7.8.1.dev715 → ha_mcp_dev-7.8.1.dev717}/src/ha_mcp/tools/enhanced.py +0 -0
- {ha_mcp_dev-7.8.1.dev715 → ha_mcp_dev-7.8.1.dev717}/src/ha_mcp/tools/helpers.py +0 -0
- {ha_mcp_dev-7.8.1.dev715 → ha_mcp_dev-7.8.1.dev717}/src/ha_mcp/tools/reference_validator.py +0 -0
- {ha_mcp_dev-7.8.1.dev715 → ha_mcp_dev-7.8.1.dev717}/src/ha_mcp/tools/registry.py +0 -0
- {ha_mcp_dev-7.8.1.dev715 → ha_mcp_dev-7.8.1.dev717}/src/ha_mcp/tools/smart_search/__init__.py +0 -0
- {ha_mcp_dev-7.8.1.dev715 → ha_mcp_dev-7.8.1.dev717}/src/ha_mcp/tools/smart_search/_base.py +0 -0
- {ha_mcp_dev-7.8.1.dev715 → ha_mcp_dev-7.8.1.dev717}/src/ha_mcp/tools/smart_search/_config.py +0 -0
- {ha_mcp_dev-7.8.1.dev715 → ha_mcp_dev-7.8.1.dev717}/src/ha_mcp/tools/smart_search/_deep.py +0 -0
- {ha_mcp_dev-7.8.1.dev715 → ha_mcp_dev-7.8.1.dev717}/src/ha_mcp/tools/smart_search/_entities.py +0 -0
- {ha_mcp_dev-7.8.1.dev715 → ha_mcp_dev-7.8.1.dev717}/src/ha_mcp/tools/smart_search/_fetch.py +0 -0
- {ha_mcp_dev-7.8.1.dev715 → ha_mcp_dev-7.8.1.dev717}/src/ha_mcp/tools/smart_search/_overview.py +0 -0
- {ha_mcp_dev-7.8.1.dev715 → ha_mcp_dev-7.8.1.dev717}/src/ha_mcp/tools/smart_search/_scenes.py +0 -0
- {ha_mcp_dev-7.8.1.dev715 → ha_mcp_dev-7.8.1.dev717}/src/ha_mcp/tools/smart_search/_scoring.py +0 -0
- {ha_mcp_dev-7.8.1.dev715 → ha_mcp_dev-7.8.1.dev717}/src/ha_mcp/tools/tool_search_hint_middleware.py +0 -0
- {ha_mcp_dev-7.8.1.dev715 → ha_mcp_dev-7.8.1.dev717}/src/ha_mcp/tools/tools_areas.py +0 -0
- {ha_mcp_dev-7.8.1.dev715 → ha_mcp_dev-7.8.1.dev717}/src/ha_mcp/tools/tools_blueprints.py +0 -0
- {ha_mcp_dev-7.8.1.dev715 → ha_mcp_dev-7.8.1.dev717}/src/ha_mcp/tools/tools_bug_report.py +0 -0
- {ha_mcp_dev-7.8.1.dev715 → ha_mcp_dev-7.8.1.dev717}/src/ha_mcp/tools/tools_calendar.py +0 -0
- {ha_mcp_dev-7.8.1.dev715 → ha_mcp_dev-7.8.1.dev717}/src/ha_mcp/tools/tools_camera.py +0 -0
- {ha_mcp_dev-7.8.1.dev715 → ha_mcp_dev-7.8.1.dev717}/src/ha_mcp/tools/tools_categories.py +0 -0
- {ha_mcp_dev-7.8.1.dev715 → ha_mcp_dev-7.8.1.dev717}/src/ha_mcp/tools/tools_code.py +0 -0
- {ha_mcp_dev-7.8.1.dev715 → ha_mcp_dev-7.8.1.dev717}/src/ha_mcp/tools/tools_config_automations.py +0 -0
- {ha_mcp_dev-7.8.1.dev715 → ha_mcp_dev-7.8.1.dev717}/src/ha_mcp/tools/tools_config_dashboards.py +0 -0
- {ha_mcp_dev-7.8.1.dev715 → ha_mcp_dev-7.8.1.dev717}/src/ha_mcp/tools/tools_config_helpers.py +0 -0
- {ha_mcp_dev-7.8.1.dev715 → ha_mcp_dev-7.8.1.dev717}/src/ha_mcp/tools/tools_config_scenes.py +0 -0
- {ha_mcp_dev-7.8.1.dev715 → ha_mcp_dev-7.8.1.dev717}/src/ha_mcp/tools/tools_config_scripts.py +0 -0
- {ha_mcp_dev-7.8.1.dev715 → ha_mcp_dev-7.8.1.dev717}/src/ha_mcp/tools/tools_dashboard_screenshot.py +0 -0
- {ha_mcp_dev-7.8.1.dev715 → ha_mcp_dev-7.8.1.dev717}/src/ha_mcp/tools/tools_energy.py +0 -0
- {ha_mcp_dev-7.8.1.dev715 → ha_mcp_dev-7.8.1.dev717}/src/ha_mcp/tools/tools_entities.py +0 -0
- {ha_mcp_dev-7.8.1.dev715 → ha_mcp_dev-7.8.1.dev717}/src/ha_mcp/tools/tools_filesystem.py +0 -0
- {ha_mcp_dev-7.8.1.dev715 → ha_mcp_dev-7.8.1.dev717}/src/ha_mcp/tools/tools_groups.py +0 -0
- {ha_mcp_dev-7.8.1.dev715 → ha_mcp_dev-7.8.1.dev717}/src/ha_mcp/tools/tools_hacs.py +0 -0
- {ha_mcp_dev-7.8.1.dev715 → ha_mcp_dev-7.8.1.dev717}/src/ha_mcp/tools/tools_history.py +0 -0
- {ha_mcp_dev-7.8.1.dev715 → ha_mcp_dev-7.8.1.dev717}/src/ha_mcp/tools/tools_integrations.py +0 -0
- {ha_mcp_dev-7.8.1.dev715 → ha_mcp_dev-7.8.1.dev717}/src/ha_mcp/tools/tools_labels.py +0 -0
- {ha_mcp_dev-7.8.1.dev715 → ha_mcp_dev-7.8.1.dev717}/src/ha_mcp/tools/tools_mcp_component.py +0 -0
- {ha_mcp_dev-7.8.1.dev715 → ha_mcp_dev-7.8.1.dev717}/src/ha_mcp/tools/tools_registry.py +0 -0
- {ha_mcp_dev-7.8.1.dev715 → ha_mcp_dev-7.8.1.dev717}/src/ha_mcp/tools/tools_resources.py +0 -0
- {ha_mcp_dev-7.8.1.dev715 → ha_mcp_dev-7.8.1.dev717}/src/ha_mcp/tools/tools_service.py +0 -0
- {ha_mcp_dev-7.8.1.dev715 → ha_mcp_dev-7.8.1.dev717}/src/ha_mcp/tools/tools_services.py +0 -0
- {ha_mcp_dev-7.8.1.dev715 → ha_mcp_dev-7.8.1.dev717}/src/ha_mcp/tools/tools_themes.py +0 -0
- {ha_mcp_dev-7.8.1.dev715 → ha_mcp_dev-7.8.1.dev717}/src/ha_mcp/tools/tools_todo.py +0 -0
- {ha_mcp_dev-7.8.1.dev715 → ha_mcp_dev-7.8.1.dev717}/src/ha_mcp/tools/tools_traces.py +0 -0
- {ha_mcp_dev-7.8.1.dev715 → ha_mcp_dev-7.8.1.dev717}/src/ha_mcp/tools/tools_utility.py +0 -0
- {ha_mcp_dev-7.8.1.dev715 → ha_mcp_dev-7.8.1.dev717}/src/ha_mcp/tools/tools_voice_assistant.py +0 -0
- {ha_mcp_dev-7.8.1.dev715 → ha_mcp_dev-7.8.1.dev717}/src/ha_mcp/tools/tools_yaml_config.py +0 -0
- {ha_mcp_dev-7.8.1.dev715 → ha_mcp_dev-7.8.1.dev717}/src/ha_mcp/tools/tools_zones.py +0 -0
- {ha_mcp_dev-7.8.1.dev715 → ha_mcp_dev-7.8.1.dev717}/src/ha_mcp/tools/util_helpers.py +0 -0
- {ha_mcp_dev-7.8.1.dev715 → ha_mcp_dev-7.8.1.dev717}/src/ha_mcp/tools/validation_middleware.py +0 -0
- {ha_mcp_dev-7.8.1.dev715 → ha_mcp_dev-7.8.1.dev717}/src/ha_mcp/transforms/__init__.py +0 -0
- {ha_mcp_dev-7.8.1.dev715 → ha_mcp_dev-7.8.1.dev717}/src/ha_mcp/transforms/categorized_search.py +0 -0
- {ha_mcp_dev-7.8.1.dev715 → ha_mcp_dev-7.8.1.dev717}/src/ha_mcp/transforms/lite_docstrings.py +0 -0
- {ha_mcp_dev-7.8.1.dev715 → ha_mcp_dev-7.8.1.dev717}/src/ha_mcp/utils/__init__.py +0 -0
- {ha_mcp_dev-7.8.1.dev715 → ha_mcp_dev-7.8.1.dev717}/src/ha_mcp/utils/config_hash.py +0 -0
- {ha_mcp_dev-7.8.1.dev715 → ha_mcp_dev-7.8.1.dev717}/src/ha_mcp/utils/data_paths.py +0 -0
- {ha_mcp_dev-7.8.1.dev715 → ha_mcp_dev-7.8.1.dev717}/src/ha_mcp/utils/domain_handlers.py +0 -0
- {ha_mcp_dev-7.8.1.dev715 → ha_mcp_dev-7.8.1.dev717}/src/ha_mcp/utils/fuzzy_search.py +0 -0
- {ha_mcp_dev-7.8.1.dev715 → ha_mcp_dev-7.8.1.dev717}/src/ha_mcp/utils/kill_signal_diagnostics.py +0 -0
- {ha_mcp_dev-7.8.1.dev715 → ha_mcp_dev-7.8.1.dev717}/src/ha_mcp/utils/operation_manager.py +0 -0
- {ha_mcp_dev-7.8.1.dev715 → ha_mcp_dev-7.8.1.dev717}/src/ha_mcp/utils/python_sandbox.py +0 -0
- {ha_mcp_dev-7.8.1.dev715 → ha_mcp_dev-7.8.1.dev717}/src/ha_mcp/utils/skill_loader.py +0 -0
- {ha_mcp_dev-7.8.1.dev715 → ha_mcp_dev-7.8.1.dev717}/src/ha_mcp/utils/usage_logger.py +0 -0
- {ha_mcp_dev-7.8.1.dev715 → ha_mcp_dev-7.8.1.dev717}/src/ha_mcp_dev.egg-info/dependency_links.txt +0 -0
- {ha_mcp_dev-7.8.1.dev715 → ha_mcp_dev-7.8.1.dev717}/src/ha_mcp_dev.egg-info/entry_points.txt +0 -0
- {ha_mcp_dev-7.8.1.dev715 → ha_mcp_dev-7.8.1.dev717}/src/ha_mcp_dev.egg-info/top_level.txt +0 -0
- {ha_mcp_dev-7.8.1.dev715 → ha_mcp_dev-7.8.1.dev717}/tests/__init__.py +0 -0
- {ha_mcp_dev-7.8.1.dev715 → ha_mcp_dev-7.8.1.dev717}/tests/test_constants.py +0 -0
- {ha_mcp_dev-7.8.1.dev715 → ha_mcp_dev-7.8.1.dev717}/tests/test_env_manager.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ha-mcp-dev
|
|
3
|
-
Version: 7.8.1.
|
|
3
|
+
Version: 7.8.1.dev717
|
|
4
4
|
Summary: Home Assistant MCP Server - Complete control of Home Assistant through MCP
|
|
5
5
|
Author-email: Julien <github@qc-h.net>
|
|
6
6
|
License: MIT
|
|
@@ -27,6 +27,7 @@ Requires-Dist: websockets==16.0
|
|
|
27
27
|
Requires-Dist: cryptography==49.0.0
|
|
28
28
|
Requires-Dist: pydantic-monty==0.0.18
|
|
29
29
|
Requires-Dist: tzdata>=2024.1
|
|
30
|
+
Requires-Dist: packaging>=24.0
|
|
30
31
|
Dynamic: license-file
|
|
31
32
|
|
|
32
33
|
> **Breaking change (v7.3.0):** `ha_config_set_yaml` has been moved to [beta](docs/beta.md).
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "ha-mcp-dev"
|
|
7
|
-
version = "7.8.1.
|
|
7
|
+
version = "7.8.1.dev717"
|
|
8
8
|
description = "Home Assistant MCP Server - Complete control of Home Assistant through MCP"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.13,<3.14"
|
|
@@ -33,6 +33,7 @@ dependencies = [
|
|
|
33
33
|
"cryptography==49.0.0",
|
|
34
34
|
"pydantic-monty==0.0.18",
|
|
35
35
|
"tzdata>=2024.1",
|
|
36
|
+
"packaging>=24.0",
|
|
36
37
|
]
|
|
37
38
|
|
|
38
39
|
[project.urls]
|
|
@@ -479,16 +479,24 @@ def _setup_logging(log_level_str: str, force: bool = False) -> None:
|
|
|
479
479
|
|
|
480
480
|
|
|
481
481
|
def _log_startup_version() -> None:
|
|
482
|
-
"""Log ha-mcp version at startup, plus
|
|
482
|
+
"""Log ha-mcp version at startup, plus dev-channel / update banners.
|
|
483
483
|
|
|
484
484
|
The dev banner only fires for standalone dev installs (Docker ``:dev`` /
|
|
485
485
|
``:latest``, or ``pip install ha-mcp-dev``). It is suppressed under the HA
|
|
486
486
|
Supervisor because add-on users already pick dev vs stable in the HAOS UI.
|
|
487
|
+
|
|
488
|
+
The update banner fires on every startup when a newer release is available,
|
|
489
|
+
on every deployment — pip/Docker/stdio compare against PyPI, the HA add-on
|
|
490
|
+
(stable AND dev) against the Supervisor add-on store — mirroring FastMCP's
|
|
491
|
+
``log_server_banner``. ``get_update_info`` is a no-op for the ``unknown``
|
|
492
|
+
version (PyPI path) and the ``HA_MCP_DISABLE_UPDATE_CHECK`` opt-out, and
|
|
493
|
+
never raises.
|
|
487
494
|
"""
|
|
488
495
|
from ha_mcp._version import get_version, is_dev_version, is_running_in_addon
|
|
489
496
|
|
|
490
497
|
version = get_version()
|
|
491
498
|
logger.info(f"ha-mcp {version}")
|
|
499
|
+
|
|
492
500
|
if is_dev_version(version) and not is_running_in_addon():
|
|
493
501
|
logger.warning(
|
|
494
502
|
"This is the dev channel. For the stable release use the "
|
|
@@ -496,6 +504,17 @@ def _log_startup_version() -> None:
|
|
|
496
504
|
"(or 'pip install ha-mcp' on PyPI)."
|
|
497
505
|
)
|
|
498
506
|
|
|
507
|
+
from ha_mcp.update_check import get_update_info, update_command_hint
|
|
508
|
+
|
|
509
|
+
info = get_update_info()
|
|
510
|
+
if info is not None and info.update_available:
|
|
511
|
+
logger.warning(
|
|
512
|
+
"A newer ha-mcp release is available: %s (you have %s). %s",
|
|
513
|
+
info.latest,
|
|
514
|
+
info.current,
|
|
515
|
+
update_command_hint(info.current),
|
|
516
|
+
)
|
|
517
|
+
|
|
499
518
|
|
|
500
519
|
def _get_timestamped_uvicorn_log_config() -> dict:
|
|
501
520
|
"""Return a Uvicorn log config with human-readable timestamps added."""
|
|
@@ -93,8 +93,8 @@ def _addon_write(args: dict[str, Any]) -> str | None:
|
|
|
93
93
|
return "array_patch modification"
|
|
94
94
|
if args.get("websocket"):
|
|
95
95
|
# A WebSocket session's initial message can command mutations
|
|
96
|
-
# (e.g. ESPHome /compile
|
|
97
|
-
# as a read — fail closed.
|
|
96
|
+
# (e.g. an ESPHome firmware/compile or devices/update_config command),
|
|
97
|
+
# so it is not statically classifiable as a read — fail closed.
|
|
98
98
|
return "WebSocket proxy session"
|
|
99
99
|
method = str(args.get("method") or "GET").strip().upper()
|
|
100
100
|
if method != "GET":
|
|
@@ -958,7 +958,7 @@ async def _call_addon_ws(
|
|
|
958
958
|
Args:
|
|
959
959
|
client: Home Assistant REST client
|
|
960
960
|
slug: Add-on slug (e.g., "<prefix>_esphome")
|
|
961
|
-
path: WebSocket endpoint path (e.g., "/
|
|
961
|
+
path: WebSocket endpoint path (e.g., "/ws" for the ESPHome dashboard's command channel)
|
|
962
962
|
body: Message to send after connecting (JSON-encoded if dict, raw if string)
|
|
963
963
|
timeout: Max seconds to wait for messages (default 60)
|
|
964
964
|
debug: Include diagnostic info
|
|
@@ -2622,7 +2622,7 @@ def register_addon_tools(mcp: Any, client: HomeAssistantClient, **kwargs: Any) -
|
|
|
2622
2622
|
body: Annotated[
|
|
2623
2623
|
dict[str, Any] | str | None,
|
|
2624
2624
|
Field(
|
|
2625
|
-
description="Proxy mode only. Request body for POST/PUT/PATCH. Pass a JSON object or JSON string.",
|
|
2625
|
+
description="Proxy mode only. Request body for POST/PUT/PATCH — or, with websocket=True, the initial WebSocket message. Pass a JSON object or JSON string.",
|
|
2626
2626
|
default=None,
|
|
2627
2627
|
),
|
|
2628
2628
|
] = None,
|
|
@@ -2658,17 +2658,20 @@ def register_addon_tools(mcp: Any, client: HomeAssistantClient, **kwargs: Any) -
|
|
|
2658
2658
|
websocket: Annotated[
|
|
2659
2659
|
bool,
|
|
2660
2660
|
Field(
|
|
2661
|
-
description="Proxy mode only. Use WebSocket instead of HTTP
|
|
2662
|
-
"(e.g
|
|
2663
|
-
"
|
|
2661
|
+
description="Proxy mode only. Use WebSocket instead of HTTP — for an add-on's "
|
|
2662
|
+
"WebSocket API (e.g. the ESPHome dashboard's '/ws' command channel; see the "
|
|
2663
|
+
"docstring's ESPHome section). Sends 'body' as the initial message, collects "
|
|
2664
|
+
"responses. Default: false.",
|
|
2664
2665
|
default=False,
|
|
2665
2666
|
),
|
|
2666
2667
|
] = False,
|
|
2667
2668
|
wait_for_close: Annotated[
|
|
2668
2669
|
bool,
|
|
2669
2670
|
Field(
|
|
2670
|
-
description="Proxy mode only. WebSocket: True: wait for server to close
|
|
2671
|
-
"
|
|
2671
|
+
description="Proxy mode only. WebSocket: True: wait for the server to close the stream "
|
|
2672
|
+
"(run-to-completion ops like an ESPHome compile/validate). False: return after the first "
|
|
2673
|
+
"response batch — use for a one-shot command/response or a bounded log capture on a channel "
|
|
2674
|
+
"that stays open (e.g. ESPHome '/ws'). Default: true.",
|
|
2672
2675
|
default=True,
|
|
2673
2676
|
),
|
|
2674
2677
|
] = True,
|
|
@@ -2839,6 +2842,29 @@ def register_addon_tools(mcp: Any, client: HomeAssistantClient, **kwargs: Any) -
|
|
|
2839
2842
|
share Home Assistant's container network (i.e. only the HAOS addon).
|
|
2840
2843
|
Use ha_get_addon(slug="...") to discover available ports and endpoints.
|
|
2841
2844
|
|
|
2845
|
+
**ESPHome Device Builder dashboard (current rewrite):** config and log
|
|
2846
|
+
access is a WebSocket JSON-command API, NOT REST. The legacy endpoints
|
|
2847
|
+
are gone — `GET /edit?configuration=` now returns the dashboard SPA, and
|
|
2848
|
+
the old `/compile` `/validate` `/logs` WebSocket paths (which took
|
|
2849
|
+
`{"type": "spawn", ...}` bodies) reject the upgrade (HTTP 200). Use
|
|
2850
|
+
instead:
|
|
2851
|
+
- HTTP `GET /devices` → JSON list of configured devices; each entry's
|
|
2852
|
+
`configuration` field is the YAML filename to pass below.
|
|
2853
|
+
- WebSocket `path="/ws"` with body
|
|
2854
|
+
`{"command": "<cmd>", "message_id": "1", "args": {...}}`. The server
|
|
2855
|
+
sends a `server_info` message first, then one reply per `message_id`.
|
|
2856
|
+
Wire-confirmed commands: `devices/get_config` `{configuration}` → raw
|
|
2857
|
+
YAML (in the reply's `result`); `devices/logs` (stream)
|
|
2858
|
+
`{configuration, port: "OTA"}` → live device logs. Also exposed by the
|
|
2859
|
+
dashboard frontend (command/arg names not wire-tested here):
|
|
2860
|
+
`devices/update_config` `{configuration, content}` → save,
|
|
2861
|
+
`devices/validate`, `firmware/compile`.
|
|
2862
|
+
- The `/ws` channel stays open, so for a one-shot read or a bounded log
|
|
2863
|
+
capture pass `wait_for_close=False` with `message_limit` (and
|
|
2864
|
+
`message_offset` to skip the server_info / config-banner preamble).
|
|
2865
|
+
Reach the dashboard through Ingress — omit `port`; direct `port=` does
|
|
2866
|
+
not route to it.
|
|
2867
|
+
|
|
2842
2868
|
**Array-patch mode** (when path AND array_patch are provided):
|
|
2843
2869
|
Atomic "GET array, mutate, POST array" workflow for addon APIs whose write
|
|
2844
2870
|
contract is "send the whole resource collection back". Operations are applied
|
|
@@ -2847,8 +2873,8 @@ def register_addon_tools(mcp: Any, client: HomeAssistantClient, **kwargs: Any) -
|
|
|
2847
2873
|
full array. Designed for Node-RED /flows and similar endpoints.
|
|
2848
2874
|
|
|
2849
2875
|
**Response shaping (proxy mode):**
|
|
2850
|
-
- WebSocket streams can be noisy (
|
|
2851
|
-
config
|
|
2876
|
+
- WebSocket streams can be noisy (e.g. the ESPHome dashboard's devices/logs
|
|
2877
|
+
dumps the device's full config banner on connect). By default, `summarize=True` collapses long runs of
|
|
2852
2878
|
non-signal messages into short elision markers; INFO/WARNING/ERROR/exit
|
|
2853
2879
|
lines always pass through. Pagination via `message_offset` / `message_limit`
|
|
2854
2880
|
works on the raw collected list before summarize runs.
|
|
@@ -2881,9 +2907,10 @@ def register_addon_tools(mcp: Any, client: HomeAssistantClient, **kwargs: Any) -
|
|
|
2881
2907
|
- Set boot mode: ha_manage_addon(slug="...", boot="manual")
|
|
2882
2908
|
- Call HTTP API: ha_manage_addon(slug="...", path="/api/events")
|
|
2883
2909
|
- Direct port: ha_manage_addon(slug="...", path="/flows", port=1880)
|
|
2884
|
-
-
|
|
2885
|
-
-
|
|
2886
|
-
-
|
|
2910
|
+
- ESPHome list devices (HTTP): ha_manage_addon(slug="<prefix>_esphome", path="/devices")
|
|
2911
|
+
- ESPHome read a device's YAML (WS one-shot): ha_manage_addon(slug="<prefix>_esphome", path="/ws", websocket=True, wait_for_close=False, message_limit=2, body={"command": "devices/get_config", "message_id": "1", "args": {"configuration": "device.yaml"}})
|
|
2912
|
+
- ESPHome live logs (WS, bounded): ha_manage_addon(slug="<prefix>_esphome", path="/ws", websocket=True, wait_for_close=False, message_limit=60, body={"command": "devices/logs", "message_id": "1", "args": {"configuration": "device.yaml", "port": "OTA"}})
|
|
2913
|
+
- Filter WS errors only: ha_manage_addon(slug="...", path="/ws", websocket=True, python_transform="response = [m for m in response if 'ERROR' in str(m) or 'WARN' in str(m)]")
|
|
2887
2914
|
- HTTP subset: ha_manage_addon(slug="...", path="/flows", python_transform="response = [f['id'] for f in response]")
|
|
2888
2915
|
- Array-patch (Node-RED, rename a node):
|
|
2889
2916
|
ha_manage_addon(
|
|
@@ -1896,13 +1896,14 @@ def register_search_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
|
|
|
1896
1896
|
"notification_count, notifications, repair_count, "
|
|
1897
1897
|
"dismissed_repair_count, repairs, repairs_error, "
|
|
1898
1898
|
"tool_discovery, settings_url, settings_url_hint, "
|
|
1899
|
-
"read_only_mode, read_only_mode_hint. Note: "
|
|
1899
|
+
"read_only_mode, read_only_mode_hint, ha_mcp_update. Note: "
|
|
1900
1900
|
"``settings_url`` (stdio mode), ``settings_url_hint`` "
|
|
1901
|
-
"(HTTP/Docker/OAuth mode),
|
|
1901
|
+
"(HTTP/Docker/OAuth mode), the ``read_only_mode`` / "
|
|
1902
1902
|
"``read_only_mode_hint`` pair (only while Read Only Mode "
|
|
1903
|
-
"is on)
|
|
1904
|
-
"
|
|
1905
|
-
"
|
|
1903
|
+
"is on), and ``ha_mcp_update`` (when an update check applies) "
|
|
1904
|
+
"are emitted regardless of ``fields=`` projection so the "
|
|
1905
|
+
"settings page, the active mode, and a newer ha-mcp release "
|
|
1906
|
+
"stay discoverable; see the tool description."
|
|
1906
1907
|
),
|
|
1907
1908
|
),
|
|
1908
1909
|
] = None,
|
|
@@ -1936,6 +1937,13 @@ def register_search_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
|
|
|
1936
1937
|
instead carries a ``settings_url_hint`` string telling the user where
|
|
1937
1938
|
the page is mounted and to read the full URL from the startup logs.
|
|
1938
1939
|
Hand whichever of the two fields is present to the user.
|
|
1940
|
+
|
|
1941
|
+
The response also carries an ``ha_mcp_update`` object
|
|
1942
|
+
``{current, latest, update_available}`` reporting whether a newer ha-mcp
|
|
1943
|
+
release is available (PyPI for pip/Docker, the Supervisor add-on store
|
|
1944
|
+
for the add-on) — proactively tell the user when ``update_available`` is
|
|
1945
|
+
true. Emitted regardless of ``fields=``; omitted only for the
|
|
1946
|
+
``unknown`` version and when ``HA_MCP_DISABLE_UPDATE_CHECK`` is set.
|
|
1939
1947
|
"""
|
|
1940
1948
|
# Validate fields= early so a malformed value returns VALIDATION_FAILED
|
|
1941
1949
|
# with parameter="fields" (ha_get_overview has no outer try/except, so
|
|
@@ -2172,6 +2180,17 @@ def register_search_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
|
|
|
2172
2180
|
"configuration."
|
|
2173
2181
|
)
|
|
2174
2182
|
|
|
2183
|
+
# Surface the MCP server's own update status after projection (like
|
|
2184
|
+
# settings_url / read_only_mode) so it survives any fields= filter — the
|
|
2185
|
+
# model should learn about a newer ha-mcp release even from a minimal
|
|
2186
|
+
# overview, the most common session-start call. Best-effort and never
|
|
2187
|
+
# raises; see ha_mcp.update_check.
|
|
2188
|
+
from ..update_check import get_update_field
|
|
2189
|
+
|
|
2190
|
+
mcp_update = await get_update_field()
|
|
2191
|
+
if mcp_update is not None:
|
|
2192
|
+
projected["ha_mcp_update"] = mcp_update
|
|
2193
|
+
|
|
2175
2194
|
return projected
|
|
2176
2195
|
|
|
2177
2196
|
async def ha_deep_search(
|
|
@@ -375,6 +375,15 @@ class SystemTools:
|
|
|
375
375
|
Returns health check results from integrations, system resources, and connectivity.
|
|
376
376
|
Available information varies by installation type and loaded integrations.
|
|
377
377
|
|
|
378
|
+
The result also carries an ``ha_mcp_update`` object —
|
|
379
|
+
``{current, latest, update_available}`` — reporting whether a newer
|
|
380
|
+
ha-mcp release is available (from PyPI for pip/Docker, or the Supervisor
|
|
381
|
+
add-on store for the add-on), so you can proactively tell the user to
|
|
382
|
+
upgrade. Present on every install type including the HA add-on (so a user
|
|
383
|
+
who missed the Supervisor's update prompt still hears about it); omitted
|
|
384
|
+
only for the ``unknown`` version and when ``HA_MCP_DISABLE_UPDATE_CHECK``
|
|
385
|
+
is set.
|
|
386
|
+
|
|
378
387
|
**Parameters:**
|
|
379
388
|
- include: Optional comma-separated list of additional data to include.
|
|
380
389
|
- "repairs": Repair items from Settings > System > Repairs (active only by default; pass `include_dismissed_repairs=True` for all)
|
|
@@ -700,6 +709,16 @@ class SystemTools:
|
|
|
700
709
|
result.setdefault("warnings", []).extend(section_warnings)
|
|
701
710
|
result["dead_entities"] = dead_section
|
|
702
711
|
|
|
712
|
+
# Surface the MCP server's own update status so the model can relay
|
|
713
|
+
# it in chat. ``get_update_field`` is best-effort, thread-offloaded,
|
|
714
|
+
# and never raises (omits the field on any hiccup); see
|
|
715
|
+
# ha_mcp.update_check for gating/throttle details.
|
|
716
|
+
from ..update_check import get_update_field
|
|
717
|
+
|
|
718
|
+
mcp_update = await get_update_field()
|
|
719
|
+
if mcp_update is not None:
|
|
720
|
+
result["ha_mcp_update"] = mcp_update
|
|
721
|
+
|
|
703
722
|
return result
|
|
704
723
|
|
|
705
724
|
except ToolError:
|
|
@@ -524,7 +524,7 @@ class UpdateTools:
|
|
|
524
524
|
|
|
525
525
|
categories = {k: v for k, v in categories.items() if v}
|
|
526
526
|
|
|
527
|
-
|
|
527
|
+
result: dict[str, Any] = {
|
|
528
528
|
"success": True,
|
|
529
529
|
"updates_available": len(available_updates),
|
|
530
530
|
"skipped_count": len(skipped_updates),
|
|
@@ -533,6 +533,19 @@ class UpdateTools:
|
|
|
533
533
|
"include_skipped": include_skipped,
|
|
534
534
|
}
|
|
535
535
|
|
|
536
|
+
# Surface the MCP server's OWN update status alongside HA's updates so the
|
|
537
|
+
# model is more likely to mention "you're on X, but Y is out" — this tool
|
|
538
|
+
# is HA-centric, but spreading the ha-mcp self-update signal across the
|
|
539
|
+
# status surfaces raises the odds an AI relays it. ``get_update_field`` is
|
|
540
|
+
# best-effort and never raises; see ha_mcp.update_check for details.
|
|
541
|
+
from ..update_check import get_update_field
|
|
542
|
+
|
|
543
|
+
mcp_update = await get_update_field()
|
|
544
|
+
if mcp_update is not None:
|
|
545
|
+
result["ha_mcp_update"] = mcp_update
|
|
546
|
+
|
|
547
|
+
return result
|
|
548
|
+
|
|
536
549
|
async def _get_update_details(
|
|
537
550
|
self, entity_id: str, include_release_notes: bool = False
|
|
538
551
|
) -> dict[str, Any]:
|
|
@@ -702,6 +715,11 @@ class UpdateTools:
|
|
|
702
715
|
- updates_available: Count of available updates
|
|
703
716
|
- updates: List of update entities with version info
|
|
704
717
|
- categories: Updates grouped by category (core, addons, devices, hacs, os)
|
|
718
|
+
- ha_mcp_update: This MCP server's own update status
|
|
719
|
+
{current, latest, update_available} — so you can flag a newer ha-mcp
|
|
720
|
+
release (from PyPI for pip/Docker, the Supervisor add-on store for the
|
|
721
|
+
add-on). Present on all install types; omitted only for the unknown
|
|
722
|
+
version and when HA_MCP_DISABLE_UPDATE_CHECK is set.
|
|
705
723
|
|
|
706
724
|
RETURNS (when getting specific update):
|
|
707
725
|
- Update details including installed/latest versions
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
"""Self-update notifier for ha-mcp.
|
|
2
|
+
|
|
3
|
+
An operator can sit on an old build without ever knowing. This does a
|
|
4
|
+
fail-silent check, holds the result in memory for the process, and surfaces a
|
|
5
|
+
newer release via a startup log banner and the ``ha_get_overview`` /
|
|
6
|
+
``ha_get_system_health`` / ``ha_get_updates`` tool fields.
|
|
7
|
+
|
|
8
|
+
The comparison reference depends on how ha-mcp is deployed, because the running
|
|
9
|
+
version only means something against the matching source:
|
|
10
|
+
|
|
11
|
+
* **pip / Docker / stdio** — the running version IS a PyPI version, so it is
|
|
12
|
+
compared against PyPI: stable installs against ``ha-mcp``, dev installs
|
|
13
|
+
(``.dev``) against ``ha-mcp-dev``.
|
|
14
|
+
* **HA add-on (stable AND dev)** — the add-on is built from source and updated
|
|
15
|
+
through the Supervisor add-on store, so its ``HA_MCP_BUILD_VERSION`` is on the
|
|
16
|
+
add-on's own counter, NOT PyPI's. The reference is therefore the Supervisor
|
|
17
|
+
add-on store (``GET /addons/self/info`` → ``version`` / ``version_latest`` /
|
|
18
|
+
``update_available``), which is the same counter — so the dev add-on, like
|
|
19
|
+
every other deployment, correctly says "you're on X, Y is out."
|
|
20
|
+
|
|
21
|
+
The check runs once per process — memoized in memory, no disk, no throttle. For
|
|
22
|
+
stdio that is once per session (the server is spawned per conversation); for a
|
|
23
|
+
long-running Docker/web server or the add-on, once at boot. It is a no-op for the
|
|
24
|
+
``unknown`` version (PyPI path) and the ``HA_MCP_DISABLE_UPDATE_CHECK`` opt-out.
|
|
25
|
+
Every network call is best-effort: any failure yields "no update info" rather
|
|
26
|
+
than raising, so callers never need to guard it.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
from __future__ import annotations
|
|
30
|
+
|
|
31
|
+
import asyncio
|
|
32
|
+
import functools
|
|
33
|
+
import logging
|
|
34
|
+
import os
|
|
35
|
+
from dataclasses import dataclass
|
|
36
|
+
from typing import Any, TypedDict
|
|
37
|
+
|
|
38
|
+
import httpx
|
|
39
|
+
from packaging.version import InvalidVersion, Version
|
|
40
|
+
|
|
41
|
+
from ._version import (
|
|
42
|
+
get_supervisor_base_url,
|
|
43
|
+
get_version,
|
|
44
|
+
is_dev_version,
|
|
45
|
+
is_running_in_addon,
|
|
46
|
+
)
|
|
47
|
+
from .stdio_settings_sidecar import _TRUTHY # shared HA_MCP_DISABLE_* truthy set
|
|
48
|
+
|
|
49
|
+
logger = logging.getLogger(__name__)
|
|
50
|
+
|
|
51
|
+
DISABLE_ENV = "HA_MCP_DISABLE_UPDATE_CHECK"
|
|
52
|
+
_PYPI_JSON_URL = "https://pypi.org/pypi/{package}/json"
|
|
53
|
+
_STABLE_PACKAGE = "ha-mcp"
|
|
54
|
+
_DEV_PACKAGE = "ha-mcp-dev"
|
|
55
|
+
# Tight timeout so the once-per-process check can never add more than a blip of
|
|
56
|
+
# latency to a cold stdio spawn.
|
|
57
|
+
_HTTP_TIMEOUT_SECONDS = 2.0
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@dataclass(frozen=True)
|
|
61
|
+
class UpdateInfo:
|
|
62
|
+
"""Result of a self-update check.
|
|
63
|
+
|
|
64
|
+
``update_available`` is the field callers branch on; ``current`` and
|
|
65
|
+
``latest`` are carried so a banner or tool field can show both versions.
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
current: str
|
|
69
|
+
latest: str
|
|
70
|
+
update_available: bool
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _is_disabled() -> bool:
|
|
74
|
+
# Reuse the shared _TRUTHY set so HA_MCP_DISABLE_UPDATE_CHECK parses
|
|
75
|
+
# identically to the sibling HA_MCP_DISABLE_SETTINGS_UI flag: only a truthy
|
|
76
|
+
# value disables; 0/false/no/off/blank (and anything unrecognized) keep the
|
|
77
|
+
# check enabled, so a user who sets =0 to "keep it on" isn't surprised.
|
|
78
|
+
return os.environ.get(DISABLE_ENV, "").strip().lower() in _TRUTHY
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _is_newer(latest: str, current: str) -> bool:
|
|
82
|
+
"""Return True only when ``latest`` is a strictly higher PEP 440 release.
|
|
83
|
+
|
|
84
|
+
``packaging.version`` orders dev/pre/post correctly — e.g.
|
|
85
|
+
``7.8.0.dev714 < 7.8.0.dev720 < 7.8.0`` and ``7.8.0 < 7.9.0`` — so this works
|
|
86
|
+
for both the stable and dev channels. An unparseable version on either side
|
|
87
|
+
reads as "can't compare" → not newer (never a bogus banner).
|
|
88
|
+
"""
|
|
89
|
+
try:
|
|
90
|
+
return Version(latest) > Version(current)
|
|
91
|
+
except InvalidVersion:
|
|
92
|
+
return False
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _fetch_latest_from_pypi(package: str) -> str | None:
|
|
96
|
+
"""Fetch ``package``'s latest published version from PyPI, or None on failure."""
|
|
97
|
+
try:
|
|
98
|
+
# follow_redirects=True mirrors the sibling fetch in tools_updates.py and
|
|
99
|
+
# survives any future PyPI redirect (the JSON API serves 200 directly today).
|
|
100
|
+
resp = httpx.get(
|
|
101
|
+
_PYPI_JSON_URL.format(package=package),
|
|
102
|
+
timeout=_HTTP_TIMEOUT_SECONDS,
|
|
103
|
+
follow_redirects=True,
|
|
104
|
+
)
|
|
105
|
+
resp.raise_for_status()
|
|
106
|
+
version = resp.json()["info"]["version"]
|
|
107
|
+
return version if isinstance(version, str) else None
|
|
108
|
+
except (httpx.HTTPError, KeyError, ValueError, TypeError) as err:
|
|
109
|
+
logger.debug("ha-mcp update check skipped (PyPI fetch failed: %s)", err)
|
|
110
|
+
return None
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _fetch_supervisor_addon_info() -> dict[str, Any] | None:
|
|
114
|
+
"""Return this add-on's Supervisor info dict, or None on any failure.
|
|
115
|
+
|
|
116
|
+
GET ``/addons/self/info`` carries ``version`` (installed), ``version_latest``
|
|
117
|
+
(the add-on store's latest), and ``update_available`` — all on the add-on's
|
|
118
|
+
own version counter. Sync + fail-silent, mirroring ``_fetch_latest_from_pypi``.
|
|
119
|
+
"""
|
|
120
|
+
token = os.environ.get("SUPERVISOR_TOKEN")
|
|
121
|
+
if not token:
|
|
122
|
+
return None
|
|
123
|
+
try:
|
|
124
|
+
resp = httpx.get(
|
|
125
|
+
f"{get_supervisor_base_url()}/addons/self/info",
|
|
126
|
+
headers={"Authorization": f"Bearer {token}"},
|
|
127
|
+
timeout=_HTTP_TIMEOUT_SECONDS,
|
|
128
|
+
)
|
|
129
|
+
resp.raise_for_status()
|
|
130
|
+
body = resp.json()
|
|
131
|
+
# Supervisor REST envelope is {"result": "ok", "data": {...}}; some mocks
|
|
132
|
+
# return the data dict directly — handle both (mirrors settings_ui).
|
|
133
|
+
data = body.get("data") if isinstance(body, dict) and "data" in body else body
|
|
134
|
+
return data if isinstance(data, dict) else None
|
|
135
|
+
except (httpx.HTTPError, ValueError, TypeError) as err:
|
|
136
|
+
logger.debug("ha-mcp update check skipped (Supervisor fetch failed: %s)", err)
|
|
137
|
+
return None
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _resolve_from_supervisor() -> UpdateInfo | None:
|
|
141
|
+
"""Build UpdateInfo for the add-on from the Supervisor add-on store."""
|
|
142
|
+
info = _fetch_supervisor_addon_info()
|
|
143
|
+
if info is None:
|
|
144
|
+
return None
|
|
145
|
+
current = info.get("version")
|
|
146
|
+
latest = info.get("version_latest")
|
|
147
|
+
if not isinstance(current, str) or not isinstance(latest, str):
|
|
148
|
+
return None
|
|
149
|
+
return UpdateInfo(
|
|
150
|
+
current=current,
|
|
151
|
+
latest=latest,
|
|
152
|
+
update_available=bool(info.get("update_available")),
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _resolve_update_info() -> UpdateInfo | None:
|
|
157
|
+
"""Update-check logic; see ``get_update_info`` for the memo + never-raises wrapper."""
|
|
158
|
+
if _is_disabled():
|
|
159
|
+
return None
|
|
160
|
+
# In the add-on (stable OR dev), the authoritative reference is the
|
|
161
|
+
# Supervisor add-on store, which tracks the SAME version counter as the
|
|
162
|
+
# installed add-on. PyPI is the wrong reference there: the add-on builds
|
|
163
|
+
# ha-mcp from source on its own cadence (HA_MCP_BUILD_VERSION), unrelated to
|
|
164
|
+
# the ha-mcp/ha-mcp-dev PyPI counters — comparing them yields false
|
|
165
|
+
# positives/negatives. Non-add-on deployments (pip / Docker / stdio) report a
|
|
166
|
+
# real PyPI version and take the PyPI path below.
|
|
167
|
+
if is_running_in_addon():
|
|
168
|
+
return _resolve_from_supervisor()
|
|
169
|
+
current = get_version()
|
|
170
|
+
if current == "unknown":
|
|
171
|
+
return None
|
|
172
|
+
# A dev install (``.dev`` version) tracks the renamed ``ha-mcp-dev`` package;
|
|
173
|
+
# a stable install tracks ``ha-mcp``.
|
|
174
|
+
package = _DEV_PACKAGE if is_dev_version(current) else _STABLE_PACKAGE
|
|
175
|
+
latest = _fetch_latest_from_pypi(package)
|
|
176
|
+
if latest is None:
|
|
177
|
+
return None
|
|
178
|
+
return UpdateInfo(
|
|
179
|
+
current=current,
|
|
180
|
+
latest=latest,
|
|
181
|
+
update_available=_is_newer(latest, current),
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
@functools.lru_cache(maxsize=1)
|
|
186
|
+
def get_update_info() -> UpdateInfo | None:
|
|
187
|
+
"""Return self-update info for this process, or None when no check applies.
|
|
188
|
+
|
|
189
|
+
Memoized in memory (``lru_cache``) so the network check runs once per process
|
|
190
|
+
— the startup banner warms it and the tool fields reuse it without re-hitting
|
|
191
|
+
PyPI. No disk, no throttle: a long-running server reflects its boot-time check
|
|
192
|
+
until restart, which is fine (those operators aren't watching startup logs).
|
|
193
|
+
Never raises — an unexpected failure degrades to None so the unguarded
|
|
194
|
+
startup-banner call can't break startup. Tests reset via
|
|
195
|
+
``get_update_info.cache_clear()``.
|
|
196
|
+
"""
|
|
197
|
+
try:
|
|
198
|
+
return _resolve_update_info()
|
|
199
|
+
except Exception as err: # pragma: no cover - contract backstop
|
|
200
|
+
logger.debug("ha-mcp update check skipped (unexpected error: %s)", err)
|
|
201
|
+
return None
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
class UpdateField(TypedDict):
|
|
205
|
+
"""The ``ha_mcp_update`` object embedded in status-tool responses."""
|
|
206
|
+
|
|
207
|
+
current: str
|
|
208
|
+
latest: str
|
|
209
|
+
update_available: bool
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
async def get_update_field() -> UpdateField | None:
|
|
213
|
+
"""Return an embeddable self-update dict for tool responses, or None.
|
|
214
|
+
|
|
215
|
+
Off-loads the (memoized, networks-at-most-once-per-process) check to a thread
|
|
216
|
+
so it never blocks the event loop, and shapes the result for embedding under
|
|
217
|
+
an ``ha_mcp_update`` key. Never raises — a hiccup yields None (the tool omits
|
|
218
|
+
the field) and is logged at debug. Shared by every status tool that surfaces
|
|
219
|
+
the notice (``ha_get_overview`` / ``ha_get_system_health`` / ``ha_get_updates``)
|
|
220
|
+
so the shaping and event-loop offload live in one place.
|
|
221
|
+
"""
|
|
222
|
+
try:
|
|
223
|
+
# Once the lru_cache is warm (normally at startup), the result is a
|
|
224
|
+
# sub-microsecond cache hit, so call it directly. Only the cold first
|
|
225
|
+
# call — which may hit PyPI — is offloaded to a thread so it can't block
|
|
226
|
+
# the event loop.
|
|
227
|
+
if get_update_info.cache_info().currsize > 0:
|
|
228
|
+
info = get_update_info()
|
|
229
|
+
else:
|
|
230
|
+
info = await asyncio.to_thread(get_update_info)
|
|
231
|
+
except Exception as err: # pragma: no cover - defensive
|
|
232
|
+
logger.debug("ha-mcp self-update check skipped: %s", err)
|
|
233
|
+
return None
|
|
234
|
+
if info is None:
|
|
235
|
+
return None
|
|
236
|
+
return {
|
|
237
|
+
"current": info.current,
|
|
238
|
+
"latest": info.latest,
|
|
239
|
+
"update_available": info.update_available,
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def _running_in_docker() -> bool:
|
|
244
|
+
return os.path.exists("/.dockerenv") or os.path.exists("/run/.containerenv")
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def update_command_hint(current: str) -> str:
|
|
248
|
+
"""Return a deployment- and channel-aware one-liner for how to upgrade."""
|
|
249
|
+
if is_running_in_addon():
|
|
250
|
+
# Add-on users update through the Supervisor UI, not pip/docker.
|
|
251
|
+
return "Update from Settings -> Add-ons -> Home Assistant MCP Server."
|
|
252
|
+
dev = is_dev_version(current)
|
|
253
|
+
if _running_in_docker():
|
|
254
|
+
# Dev images are tagged :dev (rolling); :stable is the stable channel.
|
|
255
|
+
tag = "dev" if dev else "stable"
|
|
256
|
+
return (
|
|
257
|
+
f"Pull the new image: docker pull "
|
|
258
|
+
f"ghcr.io/homeassistant-ai/ha-mcp:{tag} (then restart)."
|
|
259
|
+
)
|
|
260
|
+
package = _DEV_PACKAGE if dev else _STABLE_PACKAGE
|
|
261
|
+
return f"Upgrade with: pip install -U {package}."
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ha-mcp-dev
|
|
3
|
-
Version: 7.8.1.
|
|
3
|
+
Version: 7.8.1.dev717
|
|
4
4
|
Summary: Home Assistant MCP Server - Complete control of Home Assistant through MCP
|
|
5
5
|
Author-email: Julien <github@qc-h.net>
|
|
6
6
|
License: MIT
|
|
@@ -27,6 +27,7 @@ Requires-Dist: websockets==16.0
|
|
|
27
27
|
Requires-Dist: cryptography==49.0.0
|
|
28
28
|
Requires-Dist: pydantic-monty==0.0.18
|
|
29
29
|
Requires-Dist: tzdata>=2024.1
|
|
30
|
+
Requires-Dist: packaging>=24.0
|
|
30
31
|
Dynamic: license-file
|
|
31
32
|
|
|
32
33
|
> **Breaking change (v7.3.0):** `ha_config_set_yaml` has been moved to [beta](docs/beta.md).
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ha_mcp_dev-7.8.1.dev715 → ha_mcp_dev-7.8.1.dev717}/src/ha_mcp/dashboard_screenshot/__init__.py
RENAMED
|
File without changes
|
{ha_mcp_dev-7.8.1.dev715 → ha_mcp_dev-7.8.1.dev717}/src/ha_mcp/dashboard_screenshot/capture.py
RENAMED
|
File without changes
|
{ha_mcp_dev-7.8.1.dev715 → ha_mcp_dev-7.8.1.dev717}/src/ha_mcp/dashboard_screenshot/provision.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ha_mcp_dev-7.8.1.dev715 → ha_mcp_dev-7.8.1.dev717}/src/ha_mcp/resources/skills-vendor/AGENTS.md
RENAMED
|
File without changes
|
{ha_mcp_dev-7.8.1.dev715 → ha_mcp_dev-7.8.1.dev717}/src/ha_mcp/resources/skills-vendor/CLAUDE.md
RENAMED
|
File without changes
|
|
File without changes
|
{ha_mcp_dev-7.8.1.dev715 → ha_mcp_dev-7.8.1.dev717}/src/ha_mcp/resources/skills-vendor/LICENSE
RENAMED
|
File without changes
|
{ha_mcp_dev-7.8.1.dev715 → ha_mcp_dev-7.8.1.dev717}/src/ha_mcp/resources/skills-vendor/README.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ha_mcp_dev-7.8.1.dev715 → ha_mcp_dev-7.8.1.dev717}/src/ha_mcp/tools/best_practice_checker.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ha_mcp_dev-7.8.1.dev715 → ha_mcp_dev-7.8.1.dev717}/src/ha_mcp/tools/smart_search/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{ha_mcp_dev-7.8.1.dev715 → ha_mcp_dev-7.8.1.dev717}/src/ha_mcp/tools/smart_search/_config.py
RENAMED
|
File without changes
|
|
File without changes
|
{ha_mcp_dev-7.8.1.dev715 → ha_mcp_dev-7.8.1.dev717}/src/ha_mcp/tools/smart_search/_entities.py
RENAMED
|
File without changes
|
|
File without changes
|
{ha_mcp_dev-7.8.1.dev715 → ha_mcp_dev-7.8.1.dev717}/src/ha_mcp/tools/smart_search/_overview.py
RENAMED
|
File without changes
|
{ha_mcp_dev-7.8.1.dev715 → ha_mcp_dev-7.8.1.dev717}/src/ha_mcp/tools/smart_search/_scenes.py
RENAMED
|
File without changes
|
{ha_mcp_dev-7.8.1.dev715 → ha_mcp_dev-7.8.1.dev717}/src/ha_mcp/tools/smart_search/_scoring.py
RENAMED
|
File without changes
|
{ha_mcp_dev-7.8.1.dev715 → ha_mcp_dev-7.8.1.dev717}/src/ha_mcp/tools/tool_search_hint_middleware.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ha_mcp_dev-7.8.1.dev715 → ha_mcp_dev-7.8.1.dev717}/src/ha_mcp/tools/tools_config_automations.py
RENAMED
|
File without changes
|
{ha_mcp_dev-7.8.1.dev715 → ha_mcp_dev-7.8.1.dev717}/src/ha_mcp/tools/tools_config_dashboards.py
RENAMED
|
File without changes
|
{ha_mcp_dev-7.8.1.dev715 → ha_mcp_dev-7.8.1.dev717}/src/ha_mcp/tools/tools_config_helpers.py
RENAMED
|
File without changes
|
|
File without changes
|
{ha_mcp_dev-7.8.1.dev715 → ha_mcp_dev-7.8.1.dev717}/src/ha_mcp/tools/tools_config_scripts.py
RENAMED
|
File without changes
|
{ha_mcp_dev-7.8.1.dev715 → ha_mcp_dev-7.8.1.dev717}/src/ha_mcp/tools/tools_dashboard_screenshot.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ha_mcp_dev-7.8.1.dev715 → ha_mcp_dev-7.8.1.dev717}/src/ha_mcp/tools/tools_voice_assistant.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ha_mcp_dev-7.8.1.dev715 → ha_mcp_dev-7.8.1.dev717}/src/ha_mcp/tools/validation_middleware.py
RENAMED
|
File without changes
|
|
File without changes
|
{ha_mcp_dev-7.8.1.dev715 → ha_mcp_dev-7.8.1.dev717}/src/ha_mcp/transforms/categorized_search.py
RENAMED
|
File without changes
|
{ha_mcp_dev-7.8.1.dev715 → ha_mcp_dev-7.8.1.dev717}/src/ha_mcp/transforms/lite_docstrings.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ha_mcp_dev-7.8.1.dev715 → ha_mcp_dev-7.8.1.dev717}/src/ha_mcp/utils/kill_signal_diagnostics.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ha_mcp_dev-7.8.1.dev715 → ha_mcp_dev-7.8.1.dev717}/src/ha_mcp_dev.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{ha_mcp_dev-7.8.1.dev715 → ha_mcp_dev-7.8.1.dev717}/src/ha_mcp_dev.egg-info/entry_points.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|