ha-mcp-dev 7.8.0.dev703__tar.gz → 7.8.0.dev705__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.0.dev703/src/ha_mcp_dev.egg-info → ha_mcp_dev-7.8.0.dev705}/PKG-INFO +1 -1
- {ha_mcp_dev-7.8.0.dev703 → ha_mcp_dev-7.8.0.dev705}/pyproject.toml +1 -1
- {ha_mcp_dev-7.8.0.dev703 → ha_mcp_dev-7.8.0.dev705}/src/ha_mcp/__main__.py +22 -11
- {ha_mcp_dev-7.8.0.dev703 → ha_mcp_dev-7.8.0.dev705}/src/ha_mcp/tools/tools_system.py +364 -22
- {ha_mcp_dev-7.8.0.dev703 → ha_mcp_dev-7.8.0.dev705/src/ha_mcp_dev.egg-info}/PKG-INFO +1 -1
- {ha_mcp_dev-7.8.0.dev703 → ha_mcp_dev-7.8.0.dev705}/LICENSE +0 -0
- {ha_mcp_dev-7.8.0.dev703 → ha_mcp_dev-7.8.0.dev705}/MANIFEST.in +0 -0
- {ha_mcp_dev-7.8.0.dev703 → ha_mcp_dev-7.8.0.dev705}/README.md +0 -0
- {ha_mcp_dev-7.8.0.dev703 → ha_mcp_dev-7.8.0.dev705}/setup.cfg +0 -0
- {ha_mcp_dev-7.8.0.dev703 → ha_mcp_dev-7.8.0.dev705}/src/ha_mcp/__init__.py +0 -0
- {ha_mcp_dev-7.8.0.dev703 → ha_mcp_dev-7.8.0.dev705}/src/ha_mcp/_pypi_marker +0 -0
- {ha_mcp_dev-7.8.0.dev703 → ha_mcp_dev-7.8.0.dev705}/src/ha_mcp/_version.py +0 -0
- {ha_mcp_dev-7.8.0.dev703 → ha_mcp_dev-7.8.0.dev705}/src/ha_mcp/auth/__init__.py +0 -0
- {ha_mcp_dev-7.8.0.dev703 → ha_mcp_dev-7.8.0.dev705}/src/ha_mcp/auth/consent_form.py +0 -0
- {ha_mcp_dev-7.8.0.dev703 → ha_mcp_dev-7.8.0.dev705}/src/ha_mcp/auth/provider.py +0 -0
- {ha_mcp_dev-7.8.0.dev703 → ha_mcp_dev-7.8.0.dev705}/src/ha_mcp/backup_manager.py +0 -0
- {ha_mcp_dev-7.8.0.dev703 → ha_mcp_dev-7.8.0.dev705}/src/ha_mcp/client/__init__.py +0 -0
- {ha_mcp_dev-7.8.0.dev703 → ha_mcp_dev-7.8.0.dev705}/src/ha_mcp/client/rest_client.py +0 -0
- {ha_mcp_dev-7.8.0.dev703 → ha_mcp_dev-7.8.0.dev705}/src/ha_mcp/client/supervisor_client.py +0 -0
- {ha_mcp_dev-7.8.0.dev703 → ha_mcp_dev-7.8.0.dev705}/src/ha_mcp/client/websocket_client.py +0 -0
- {ha_mcp_dev-7.8.0.dev703 → ha_mcp_dev-7.8.0.dev705}/src/ha_mcp/client/websocket_listener.py +0 -0
- {ha_mcp_dev-7.8.0.dev703 → ha_mcp_dev-7.8.0.dev705}/src/ha_mcp/config.py +0 -0
- {ha_mcp_dev-7.8.0.dev703 → ha_mcp_dev-7.8.0.dev705}/src/ha_mcp/dashboard_screenshot/__init__.py +0 -0
- {ha_mcp_dev-7.8.0.dev703 → ha_mcp_dev-7.8.0.dev705}/src/ha_mcp/dashboard_screenshot/capture.py +0 -0
- {ha_mcp_dev-7.8.0.dev703 → ha_mcp_dev-7.8.0.dev705}/src/ha_mcp/dashboard_screenshot/provision.py +0 -0
- {ha_mcp_dev-7.8.0.dev703 → ha_mcp_dev-7.8.0.dev705}/src/ha_mcp/errors.py +0 -0
- {ha_mcp_dev-7.8.0.dev703 → ha_mcp_dev-7.8.0.dev705}/src/ha_mcp/policy/__init__.py +0 -0
- {ha_mcp_dev-7.8.0.dev703 → ha_mcp_dev-7.8.0.dev705}/src/ha_mcp/policy/approval_queue.py +0 -0
- {ha_mcp_dev-7.8.0.dev703 → ha_mcp_dev-7.8.0.dev705}/src/ha_mcp/policy/evaluator.py +0 -0
- {ha_mcp_dev-7.8.0.dev703 → ha_mcp_dev-7.8.0.dev705}/src/ha_mcp/policy/handlers.py +0 -0
- {ha_mcp_dev-7.8.0.dev703 → ha_mcp_dev-7.8.0.dev705}/src/ha_mcp/policy/middleware.py +0 -0
- {ha_mcp_dev-7.8.0.dev703 → ha_mcp_dev-7.8.0.dev705}/src/ha_mcp/policy/model.py +0 -0
- {ha_mcp_dev-7.8.0.dev703 → ha_mcp_dev-7.8.0.dev705}/src/ha_mcp/policy/persistence.py +0 -0
- {ha_mcp_dev-7.8.0.dev703 → ha_mcp_dev-7.8.0.dev705}/src/ha_mcp/policy/value_sources.py +0 -0
- {ha_mcp_dev-7.8.0.dev703 → ha_mcp_dev-7.8.0.dev705}/src/ha_mcp/py.typed +0 -0
- {ha_mcp_dev-7.8.0.dev703 → ha_mcp_dev-7.8.0.dev705}/src/ha_mcp/read_only.py +0 -0
- {ha_mcp_dev-7.8.0.dev703 → ha_mcp_dev-7.8.0.dev705}/src/ha_mcp/resources/skills-vendor/.claude/settings.json +0 -0
- {ha_mcp_dev-7.8.0.dev703 → ha_mcp_dev-7.8.0.dev705}/src/ha_mcp/resources/skills-vendor/.claude-plugin/marketplace.json +0 -0
- {ha_mcp_dev-7.8.0.dev703 → ha_mcp_dev-7.8.0.dev705}/src/ha_mcp/resources/skills-vendor/.claude-plugin/plugin.json +0 -0
- {ha_mcp_dev-7.8.0.dev703 → ha_mcp_dev-7.8.0.dev705}/src/ha_mcp/resources/skills-vendor/.github/ISSUE_TEMPLATE/skill-rca.md +0 -0
- {ha_mcp_dev-7.8.0.dev703 → ha_mcp_dev-7.8.0.dev705}/src/ha_mcp/resources/skills-vendor/.github/pull_request_template.md +0 -0
- {ha_mcp_dev-7.8.0.dev703 → ha_mcp_dev-7.8.0.dev705}/src/ha_mcp/resources/skills-vendor/AGENTS.md +0 -0
- {ha_mcp_dev-7.8.0.dev703 → ha_mcp_dev-7.8.0.dev705}/src/ha_mcp/resources/skills-vendor/CLAUDE.md +0 -0
- {ha_mcp_dev-7.8.0.dev703 → ha_mcp_dev-7.8.0.dev705}/src/ha_mcp/resources/skills-vendor/CONTRIBUTING.md +0 -0
- {ha_mcp_dev-7.8.0.dev703 → ha_mcp_dev-7.8.0.dev705}/src/ha_mcp/resources/skills-vendor/LICENSE +0 -0
- {ha_mcp_dev-7.8.0.dev703 → ha_mcp_dev-7.8.0.dev705}/src/ha_mcp/resources/skills-vendor/README.md +0 -0
- {ha_mcp_dev-7.8.0.dev703 → ha_mcp_dev-7.8.0.dev705}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/SKILL.md +0 -0
- {ha_mcp_dev-7.8.0.dev703 → ha_mcp_dev-7.8.0.dev705}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/evals/evals.json +0 -0
- {ha_mcp_dev-7.8.0.dev703 → ha_mcp_dev-7.8.0.dev705}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/automation-patterns.md +0 -0
- {ha_mcp_dev-7.8.0.dev703 → ha_mcp_dev-7.8.0.dev705}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-cards.md +0 -0
- {ha_mcp_dev-7.8.0.dev703 → ha_mcp_dev-7.8.0.dev705}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-guide.md +0 -0
- {ha_mcp_dev-7.8.0.dev703 → ha_mcp_dev-7.8.0.dev705}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/device-control.md +0 -0
- {ha_mcp_dev-7.8.0.dev703 → ha_mcp_dev-7.8.0.dev705}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/domain-docs.md +0 -0
- {ha_mcp_dev-7.8.0.dev703 → ha_mcp_dev-7.8.0.dev705}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/examples.yaml +0 -0
- {ha_mcp_dev-7.8.0.dev703 → ha_mcp_dev-7.8.0.dev705}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/helper-selection.md +0 -0
- {ha_mcp_dev-7.8.0.dev703 → ha_mcp_dev-7.8.0.dev705}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/safe-refactoring.md +0 -0
- {ha_mcp_dev-7.8.0.dev703 → ha_mcp_dev-7.8.0.dev705}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/template-guidelines.md +0 -0
- {ha_mcp_dev-7.8.0.dev703 → ha_mcp_dev-7.8.0.dev705}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/yaml-only-integrations.md +0 -0
- {ha_mcp_dev-7.8.0.dev703 → ha_mcp_dev-7.8.0.dev705}/src/ha_mcp/server.py +0 -0
- {ha_mcp_dev-7.8.0.dev703 → ha_mcp_dev-7.8.0.dev705}/src/ha_mcp/settings.css +0 -0
- {ha_mcp_dev-7.8.0.dev703 → ha_mcp_dev-7.8.0.dev705}/src/ha_mcp/settings.js +0 -0
- {ha_mcp_dev-7.8.0.dev703 → ha_mcp_dev-7.8.0.dev705}/src/ha_mcp/settings_ui.py +0 -0
- {ha_mcp_dev-7.8.0.dev703 → ha_mcp_dev-7.8.0.dev705}/src/ha_mcp/smoke_test.py +0 -0
- {ha_mcp_dev-7.8.0.dev703 → ha_mcp_dev-7.8.0.dev705}/src/ha_mcp/stdio_settings_sidecar.py +0 -0
- {ha_mcp_dev-7.8.0.dev703 → ha_mcp_dev-7.8.0.dev705}/src/ha_mcp/tools/__init__.py +0 -0
- {ha_mcp_dev-7.8.0.dev703 → ha_mcp_dev-7.8.0.dev705}/src/ha_mcp/tools/auto_backup.py +0 -0
- {ha_mcp_dev-7.8.0.dev703 → ha_mcp_dev-7.8.0.dev705}/src/ha_mcp/tools/backup.py +0 -0
- {ha_mcp_dev-7.8.0.dev703 → ha_mcp_dev-7.8.0.dev705}/src/ha_mcp/tools/best_practice_checker.py +0 -0
- {ha_mcp_dev-7.8.0.dev703 → ha_mcp_dev-7.8.0.dev705}/src/ha_mcp/tools/config_entry_flow.py +0 -0
- {ha_mcp_dev-7.8.0.dev703 → ha_mcp_dev-7.8.0.dev705}/src/ha_mcp/tools/device_control.py +0 -0
- {ha_mcp_dev-7.8.0.dev703 → ha_mcp_dev-7.8.0.dev705}/src/ha_mcp/tools/enhanced.py +0 -0
- {ha_mcp_dev-7.8.0.dev703 → ha_mcp_dev-7.8.0.dev705}/src/ha_mcp/tools/helpers.py +0 -0
- {ha_mcp_dev-7.8.0.dev703 → ha_mcp_dev-7.8.0.dev705}/src/ha_mcp/tools/reference_validator.py +0 -0
- {ha_mcp_dev-7.8.0.dev703 → ha_mcp_dev-7.8.0.dev705}/src/ha_mcp/tools/registry.py +0 -0
- {ha_mcp_dev-7.8.0.dev703 → ha_mcp_dev-7.8.0.dev705}/src/ha_mcp/tools/smart_search/__init__.py +0 -0
- {ha_mcp_dev-7.8.0.dev703 → ha_mcp_dev-7.8.0.dev705}/src/ha_mcp/tools/smart_search/_base.py +0 -0
- {ha_mcp_dev-7.8.0.dev703 → ha_mcp_dev-7.8.0.dev705}/src/ha_mcp/tools/smart_search/_config.py +0 -0
- {ha_mcp_dev-7.8.0.dev703 → ha_mcp_dev-7.8.0.dev705}/src/ha_mcp/tools/smart_search/_deep.py +0 -0
- {ha_mcp_dev-7.8.0.dev703 → ha_mcp_dev-7.8.0.dev705}/src/ha_mcp/tools/smart_search/_entities.py +0 -0
- {ha_mcp_dev-7.8.0.dev703 → ha_mcp_dev-7.8.0.dev705}/src/ha_mcp/tools/smart_search/_fetch.py +0 -0
- {ha_mcp_dev-7.8.0.dev703 → ha_mcp_dev-7.8.0.dev705}/src/ha_mcp/tools/smart_search/_overview.py +0 -0
- {ha_mcp_dev-7.8.0.dev703 → ha_mcp_dev-7.8.0.dev705}/src/ha_mcp/tools/smart_search/_scenes.py +0 -0
- {ha_mcp_dev-7.8.0.dev703 → ha_mcp_dev-7.8.0.dev705}/src/ha_mcp/tools/smart_search/_scoring.py +0 -0
- {ha_mcp_dev-7.8.0.dev703 → ha_mcp_dev-7.8.0.dev705}/src/ha_mcp/tools/tool_search_hint_middleware.py +0 -0
- {ha_mcp_dev-7.8.0.dev703 → ha_mcp_dev-7.8.0.dev705}/src/ha_mcp/tools/tools_addons.py +0 -0
- {ha_mcp_dev-7.8.0.dev703 → ha_mcp_dev-7.8.0.dev705}/src/ha_mcp/tools/tools_areas.py +0 -0
- {ha_mcp_dev-7.8.0.dev703 → ha_mcp_dev-7.8.0.dev705}/src/ha_mcp/tools/tools_blueprints.py +0 -0
- {ha_mcp_dev-7.8.0.dev703 → ha_mcp_dev-7.8.0.dev705}/src/ha_mcp/tools/tools_bug_report.py +0 -0
- {ha_mcp_dev-7.8.0.dev703 → ha_mcp_dev-7.8.0.dev705}/src/ha_mcp/tools/tools_calendar.py +0 -0
- {ha_mcp_dev-7.8.0.dev703 → ha_mcp_dev-7.8.0.dev705}/src/ha_mcp/tools/tools_camera.py +0 -0
- {ha_mcp_dev-7.8.0.dev703 → ha_mcp_dev-7.8.0.dev705}/src/ha_mcp/tools/tools_categories.py +0 -0
- {ha_mcp_dev-7.8.0.dev703 → ha_mcp_dev-7.8.0.dev705}/src/ha_mcp/tools/tools_code.py +0 -0
- {ha_mcp_dev-7.8.0.dev703 → ha_mcp_dev-7.8.0.dev705}/src/ha_mcp/tools/tools_config_automations.py +0 -0
- {ha_mcp_dev-7.8.0.dev703 → ha_mcp_dev-7.8.0.dev705}/src/ha_mcp/tools/tools_config_dashboards.py +0 -0
- {ha_mcp_dev-7.8.0.dev703 → ha_mcp_dev-7.8.0.dev705}/src/ha_mcp/tools/tools_config_helpers.py +0 -0
- {ha_mcp_dev-7.8.0.dev703 → ha_mcp_dev-7.8.0.dev705}/src/ha_mcp/tools/tools_config_scenes.py +0 -0
- {ha_mcp_dev-7.8.0.dev703 → ha_mcp_dev-7.8.0.dev705}/src/ha_mcp/tools/tools_config_scripts.py +0 -0
- {ha_mcp_dev-7.8.0.dev703 → ha_mcp_dev-7.8.0.dev705}/src/ha_mcp/tools/tools_dashboard_screenshot.py +0 -0
- {ha_mcp_dev-7.8.0.dev703 → ha_mcp_dev-7.8.0.dev705}/src/ha_mcp/tools/tools_energy.py +0 -0
- {ha_mcp_dev-7.8.0.dev703 → ha_mcp_dev-7.8.0.dev705}/src/ha_mcp/tools/tools_entities.py +0 -0
- {ha_mcp_dev-7.8.0.dev703 → ha_mcp_dev-7.8.0.dev705}/src/ha_mcp/tools/tools_filesystem.py +0 -0
- {ha_mcp_dev-7.8.0.dev703 → ha_mcp_dev-7.8.0.dev705}/src/ha_mcp/tools/tools_groups.py +0 -0
- {ha_mcp_dev-7.8.0.dev703 → ha_mcp_dev-7.8.0.dev705}/src/ha_mcp/tools/tools_hacs.py +0 -0
- {ha_mcp_dev-7.8.0.dev703 → ha_mcp_dev-7.8.0.dev705}/src/ha_mcp/tools/tools_history.py +0 -0
- {ha_mcp_dev-7.8.0.dev703 → ha_mcp_dev-7.8.0.dev705}/src/ha_mcp/tools/tools_integrations.py +0 -0
- {ha_mcp_dev-7.8.0.dev703 → ha_mcp_dev-7.8.0.dev705}/src/ha_mcp/tools/tools_labels.py +0 -0
- {ha_mcp_dev-7.8.0.dev703 → ha_mcp_dev-7.8.0.dev705}/src/ha_mcp/tools/tools_mcp_component.py +0 -0
- {ha_mcp_dev-7.8.0.dev703 → ha_mcp_dev-7.8.0.dev705}/src/ha_mcp/tools/tools_registry.py +0 -0
- {ha_mcp_dev-7.8.0.dev703 → ha_mcp_dev-7.8.0.dev705}/src/ha_mcp/tools/tools_resources.py +0 -0
- {ha_mcp_dev-7.8.0.dev703 → ha_mcp_dev-7.8.0.dev705}/src/ha_mcp/tools/tools_search.py +0 -0
- {ha_mcp_dev-7.8.0.dev703 → ha_mcp_dev-7.8.0.dev705}/src/ha_mcp/tools/tools_service.py +0 -0
- {ha_mcp_dev-7.8.0.dev703 → ha_mcp_dev-7.8.0.dev705}/src/ha_mcp/tools/tools_services.py +0 -0
- {ha_mcp_dev-7.8.0.dev703 → ha_mcp_dev-7.8.0.dev705}/src/ha_mcp/tools/tools_themes.py +0 -0
- {ha_mcp_dev-7.8.0.dev703 → ha_mcp_dev-7.8.0.dev705}/src/ha_mcp/tools/tools_todo.py +0 -0
- {ha_mcp_dev-7.8.0.dev703 → ha_mcp_dev-7.8.0.dev705}/src/ha_mcp/tools/tools_traces.py +0 -0
- {ha_mcp_dev-7.8.0.dev703 → ha_mcp_dev-7.8.0.dev705}/src/ha_mcp/tools/tools_updates.py +0 -0
- {ha_mcp_dev-7.8.0.dev703 → ha_mcp_dev-7.8.0.dev705}/src/ha_mcp/tools/tools_utility.py +0 -0
- {ha_mcp_dev-7.8.0.dev703 → ha_mcp_dev-7.8.0.dev705}/src/ha_mcp/tools/tools_voice_assistant.py +0 -0
- {ha_mcp_dev-7.8.0.dev703 → ha_mcp_dev-7.8.0.dev705}/src/ha_mcp/tools/tools_yaml_config.py +0 -0
- {ha_mcp_dev-7.8.0.dev703 → ha_mcp_dev-7.8.0.dev705}/src/ha_mcp/tools/tools_zones.py +0 -0
- {ha_mcp_dev-7.8.0.dev703 → ha_mcp_dev-7.8.0.dev705}/src/ha_mcp/tools/util_helpers.py +0 -0
- {ha_mcp_dev-7.8.0.dev703 → ha_mcp_dev-7.8.0.dev705}/src/ha_mcp/tools/validation_middleware.py +0 -0
- {ha_mcp_dev-7.8.0.dev703 → ha_mcp_dev-7.8.0.dev705}/src/ha_mcp/transforms/__init__.py +0 -0
- {ha_mcp_dev-7.8.0.dev703 → ha_mcp_dev-7.8.0.dev705}/src/ha_mcp/transforms/categorized_search.py +0 -0
- {ha_mcp_dev-7.8.0.dev703 → ha_mcp_dev-7.8.0.dev705}/src/ha_mcp/transforms/lite_docstrings.py +0 -0
- {ha_mcp_dev-7.8.0.dev703 → ha_mcp_dev-7.8.0.dev705}/src/ha_mcp/utils/__init__.py +0 -0
- {ha_mcp_dev-7.8.0.dev703 → ha_mcp_dev-7.8.0.dev705}/src/ha_mcp/utils/config_hash.py +0 -0
- {ha_mcp_dev-7.8.0.dev703 → ha_mcp_dev-7.8.0.dev705}/src/ha_mcp/utils/data_paths.py +0 -0
- {ha_mcp_dev-7.8.0.dev703 → ha_mcp_dev-7.8.0.dev705}/src/ha_mcp/utils/domain_handlers.py +0 -0
- {ha_mcp_dev-7.8.0.dev703 → ha_mcp_dev-7.8.0.dev705}/src/ha_mcp/utils/fuzzy_search.py +0 -0
- {ha_mcp_dev-7.8.0.dev703 → ha_mcp_dev-7.8.0.dev705}/src/ha_mcp/utils/kill_signal_diagnostics.py +0 -0
- {ha_mcp_dev-7.8.0.dev703 → ha_mcp_dev-7.8.0.dev705}/src/ha_mcp/utils/operation_manager.py +0 -0
- {ha_mcp_dev-7.8.0.dev703 → ha_mcp_dev-7.8.0.dev705}/src/ha_mcp/utils/python_sandbox.py +0 -0
- {ha_mcp_dev-7.8.0.dev703 → ha_mcp_dev-7.8.0.dev705}/src/ha_mcp/utils/skill_loader.py +0 -0
- {ha_mcp_dev-7.8.0.dev703 → ha_mcp_dev-7.8.0.dev705}/src/ha_mcp/utils/usage_logger.py +0 -0
- {ha_mcp_dev-7.8.0.dev703 → ha_mcp_dev-7.8.0.dev705}/src/ha_mcp_dev.egg-info/SOURCES.txt +0 -0
- {ha_mcp_dev-7.8.0.dev703 → ha_mcp_dev-7.8.0.dev705}/src/ha_mcp_dev.egg-info/dependency_links.txt +0 -0
- {ha_mcp_dev-7.8.0.dev703 → ha_mcp_dev-7.8.0.dev705}/src/ha_mcp_dev.egg-info/entry_points.txt +0 -0
- {ha_mcp_dev-7.8.0.dev703 → ha_mcp_dev-7.8.0.dev705}/src/ha_mcp_dev.egg-info/requires.txt +0 -0
- {ha_mcp_dev-7.8.0.dev703 → ha_mcp_dev-7.8.0.dev705}/src/ha_mcp_dev.egg-info/top_level.txt +0 -0
- {ha_mcp_dev-7.8.0.dev703 → ha_mcp_dev-7.8.0.dev705}/tests/__init__.py +0 -0
- {ha_mcp_dev-7.8.0.dev703 → ha_mcp_dev-7.8.0.dev705}/tests/test_constants.py +0 -0
- {ha_mcp_dev-7.8.0.dev703 → ha_mcp_dev-7.8.0.dev705}/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.8.0.
|
|
7
|
+
version = "7.8.0.dev705"
|
|
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"
|
|
@@ -363,24 +363,35 @@ _LOG_DATE_FORMAT = "%Y-%m-%d %H:%M:%S"
|
|
|
363
363
|
|
|
364
364
|
|
|
365
365
|
class StatelessSessionLogFilter(logging.Filter):
|
|
366
|
-
"""
|
|
366
|
+
"""Suppress the routine 'Terminating session: None' log from the MCP SDK.
|
|
367
367
|
|
|
368
368
|
In stateless HTTP mode every request creates and tears down a temporary
|
|
369
|
-
session
|
|
370
|
-
|
|
371
|
-
|
|
369
|
+
session whose id is ``None``, so the SDK emits an INFO
|
|
370
|
+
``Terminating session: None`` (mcp/server/streamable_http.py) on *every*
|
|
371
|
+
request. The line is routine but looks alarming and has repeatedly
|
|
372
|
+
confused users into thinking the connection is broken.
|
|
373
|
+
|
|
374
|
+
Returning ``False`` drops the record at this logger before it reaches any
|
|
375
|
+
handler. (Merely downgrading the level to DEBUG did not work: the level
|
|
376
|
+
gate is applied before the filter runs, so the record was already admitted
|
|
377
|
+
and still emitted -- just relabelled.) Real session terminations carry an
|
|
378
|
+
actual id and are not matched, so they still log.
|
|
372
379
|
|
|
373
380
|
# TODO: remove when modelcontextprotocol/python-sdk#2329 is resolved
|
|
374
381
|
"""
|
|
375
382
|
|
|
376
383
|
def filter(self, record: logging.LogRecord) -> bool:
|
|
377
|
-
if
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
record
|
|
383
|
-
|
|
384
|
+
if record.name != "mcp.server.streamable_http":
|
|
385
|
+
return True
|
|
386
|
+
try:
|
|
387
|
+
message = record.getMessage()
|
|
388
|
+
except (ValueError, TypeError):
|
|
389
|
+
# A malformed %-format record on this logger is not our target, and
|
|
390
|
+
# a filter must not raise: filters run in Logger.handle() with no
|
|
391
|
+
# exception handling, so a raise would crash the logging call.
|
|
392
|
+
return True
|
|
393
|
+
# Drop the stateless teardown noise; keep everything else.
|
|
394
|
+
return "Terminating session: None" not in message
|
|
384
395
|
|
|
385
396
|
|
|
386
397
|
class ToolValidationLogFilter(logging.Filter):
|
|
@@ -34,6 +34,43 @@ from .util_helpers import (
|
|
|
34
34
|
|
|
35
35
|
logger = logging.getLogger(__name__)
|
|
36
36
|
|
|
37
|
+
|
|
38
|
+
def _reraise_if_fatal(exc: BaseException) -> None:
|
|
39
|
+
"""Re-raise exceptions that must unwind rather than be demoted to an
|
|
40
|
+
embedded section error:
|
|
41
|
+
|
|
42
|
+
- ``CancelledError`` / ``KeyboardInterrupt`` / ``SystemExit`` (all
|
|
43
|
+
``BaseException`` but not ``Exception``) — task cancellation and
|
|
44
|
+
interpreter exit.
|
|
45
|
+
- ``ToolError`` — carries the MCP ``isError`` contract.
|
|
46
|
+
- ``HomeAssistantConnectionError`` — once the HA transport is dead the
|
|
47
|
+
remaining section fetches will fail anyway, so propagate the root
|
|
48
|
+
cause as ``isError=true`` rather than embed N per-section connection
|
|
49
|
+
errors. The codebase's ``rest_client._request`` already wraps
|
|
50
|
+
``OSError`` / timeout / transport failures into this class, so it is
|
|
51
|
+
the single fatal class to gate on.
|
|
52
|
+
|
|
53
|
+
This implements the cross-section policy proposed in #1624: a dead
|
|
54
|
+
connection fails loud rather than degrade per-section. Every section
|
|
55
|
+
helper in ``ha_get_system_health`` routes its ``except Exception`` block
|
|
56
|
+
through this gate as its first line, so the policy is consistent across
|
|
57
|
+
the ws ``sections`` gather pre-pass and every inline section. Recoverable
|
|
58
|
+
``Exception``-level failures still fall through to the caller's
|
|
59
|
+
embed-as-error handling.
|
|
60
|
+
"""
|
|
61
|
+
# Local import: ``rest_client`` imports from tool helpers transitively,
|
|
62
|
+
# so a module-level import here would risk a circular import in the
|
|
63
|
+
# tools package.
|
|
64
|
+
from ..client.rest_client import HomeAssistantConnectionError
|
|
65
|
+
|
|
66
|
+
if (
|
|
67
|
+
isinstance(exc, ToolError)
|
|
68
|
+
or not isinstance(exc, Exception)
|
|
69
|
+
or isinstance(exc, HomeAssistantConnectionError)
|
|
70
|
+
):
|
|
71
|
+
raise exc
|
|
72
|
+
|
|
73
|
+
|
|
37
74
|
# Mapping of reload targets to their service domains and services
|
|
38
75
|
RELOAD_TARGETS = {
|
|
39
76
|
"all": None, # Special case - reload all
|
|
@@ -357,6 +394,15 @@ class SystemTools:
|
|
|
357
394
|
- "config_check": Validate HA configuration via POST /config/core/check_config
|
|
358
395
|
(the pre-restart safety check; ha_restart runs it automatically). Returns
|
|
359
396
|
{result: valid|invalid, is_valid, errors}; read-only/idempotent, takes no args.
|
|
397
|
+
- "dead_entities": Surface orphaned/stale entity-registry entries by diffing
|
|
398
|
+
the registry against the state machine and the live config-entries set.
|
|
399
|
+
Returns confidence-tiered buckets — ``config_entry_orphans`` (owning
|
|
400
|
+
integration instance gone; definitively dead) and ``stale_restored`` (HA
|
|
401
|
+
restored the entity from the registry on startup but the loaded integration
|
|
402
|
+
no longer provides it). Each item carries entity_id + platform so a client
|
|
403
|
+
can propose cleanup with ha_remove_entity. Deliberately excludes
|
|
404
|
+
``unknown``-state entities and merely-offline devices to keep false positives
|
|
405
|
+
low. Read-only; takes no args.
|
|
360
406
|
- Example: include="repairs,zha_network,zwave_network,config_check"
|
|
361
407
|
- Example: include="diagnostics", config_entry_id="abc123..."
|
|
362
408
|
- include_dismissed_repairs: Include user-dismissed/ignored repairs (default: False). Only meaningful when "repairs" is in `include`.
|
|
@@ -419,16 +465,19 @@ class SystemTools:
|
|
|
419
465
|
# catch cannot swallow an unrelated ToolError.
|
|
420
466
|
#
|
|
421
467
|
# Degrade gracefully ONLY when the caller asked for a REST-based
|
|
422
|
-
# section (config_check / diagnostics) that can
|
|
423
|
-
# without the WebSocket. config_check is the
|
|
424
|
-
# replacement for the removed ha_check_config tool, so
|
|
425
|
-
# not depend on the health WebSocket (the
|
|
426
|
-
# command carries its own 10s timeout and can
|
|
427
|
-
# some installs).
|
|
428
|
-
#
|
|
429
|
-
#
|
|
430
|
-
#
|
|
431
|
-
|
|
468
|
+
# section (config_check / diagnostics / dead_entities) that can
|
|
469
|
+
# still be served without the WebSocket. config_check is the
|
|
470
|
+
# pure-REST replacement for the removed ha_check_config tool, so
|
|
471
|
+
# it must not depend on the health WebSocket (the
|
|
472
|
+
# system_health/info command carries its own 10s timeout and can
|
|
473
|
+
# hang/be absent on some installs). dead_entities uses the REST
|
|
474
|
+
# client's own per-client WebSocket bridge for the registry +
|
|
475
|
+
# config-entries (not this health ws_client), so it is likewise
|
|
476
|
+
# independent of the baseline. If the caller asked for nothing
|
|
477
|
+
# (the health baseline itself) or only WS-backed sections, the
|
|
478
|
+
# baseline WAS the deliverable: re-raise so the failure surfaces
|
|
479
|
+
# as isError=true, exactly as before this change.
|
|
480
|
+
if not (includes & {"config_check", "diagnostics", "dead_entities"}):
|
|
432
481
|
raise
|
|
433
482
|
logger.warning("system_health baseline unavailable: %s", health_err)
|
|
434
483
|
ws_client = None
|
|
@@ -453,6 +502,7 @@ class SystemTools:
|
|
|
453
502
|
"diagnostics",
|
|
454
503
|
"config_check",
|
|
455
504
|
"themes",
|
|
505
|
+
"dead_entities",
|
|
456
506
|
}
|
|
457
507
|
unknown = includes - VALID_INCLUDES
|
|
458
508
|
if unknown:
|
|
@@ -531,17 +581,13 @@ class SystemTools:
|
|
|
531
581
|
# inside a helper would otherwise be silently demoted to
|
|
532
582
|
# ``{"error": "ToolError: …"}`` and break the MCP
|
|
533
583
|
# ``isError=true`` contract for the whole tool.
|
|
584
|
+
# ``_reraise_if_fatal`` encapsulates the policy (cancellation,
|
|
585
|
+
# interpreter-exit, ``ToolError``, and the codebase's transport
|
|
586
|
+
# ``HomeAssistantConnectionError``) — the single source of
|
|
587
|
+
# truth shared with each section helper's ``except`` chain.
|
|
534
588
|
for section_result in gathered:
|
|
535
|
-
if isinstance(section_result,
|
|
536
|
-
|
|
537
|
-
if isinstance(section_result, ToolError):
|
|
538
|
-
raise section_result
|
|
539
|
-
if isinstance(section_result, BaseException) and not isinstance(
|
|
540
|
-
section_result, Exception
|
|
541
|
-
):
|
|
542
|
-
# ``KeyboardInterrupt`` / ``SystemExit`` — never demote
|
|
543
|
-
# these to a section-level error string.
|
|
544
|
-
raise section_result
|
|
589
|
+
if isinstance(section_result, BaseException):
|
|
590
|
+
_reraise_if_fatal(section_result)
|
|
545
591
|
for (section_name, _), section_result in zip(
|
|
546
592
|
sections, gathered, strict=True
|
|
547
593
|
):
|
|
@@ -638,6 +684,22 @@ class SystemTools:
|
|
|
638
684
|
# in one call.
|
|
639
685
|
result["config_check"] = await self._fetch_config_check()
|
|
640
686
|
|
|
687
|
+
if "dead_entities" in includes:
|
|
688
|
+
# REST + the REST client's own per-client WebSocket bridge
|
|
689
|
+
# (states via /api/states, registry + config-entries via the
|
|
690
|
+
# bridge), not the health ws_client — so it runs inline like
|
|
691
|
+
# config_check and survives a baseline-WS-down install.
|
|
692
|
+
dead_section = await self._fetch_dead_entities()
|
|
693
|
+
# Pop the ``_warnings`` sentinel and bubble it to the top-level
|
|
694
|
+
# ``result["warnings"]`` (the documented contract location).
|
|
695
|
+
# The section helper uses this sentinel so its return signature
|
|
696
|
+
# stays uniform with every other section (a plain dict) while
|
|
697
|
+
# avoiding a collision with the reserved ``warnings`` term.
|
|
698
|
+
section_warnings = dead_section.pop("_warnings", None)
|
|
699
|
+
if section_warnings:
|
|
700
|
+
result.setdefault("warnings", []).extend(section_warnings)
|
|
701
|
+
result["dead_entities"] = dead_section
|
|
702
|
+
|
|
641
703
|
return result
|
|
642
704
|
|
|
643
705
|
except ToolError:
|
|
@@ -765,6 +827,7 @@ class SystemTools:
|
|
|
765
827
|
)
|
|
766
828
|
repairs["error"] = f"Repairs data not available: {err_msg}"
|
|
767
829
|
except Exception as e:
|
|
830
|
+
_reraise_if_fatal(e)
|
|
768
831
|
logger.warning("Failed to fetch repairs: %s", e)
|
|
769
832
|
repairs["error"] = f"Repairs data not available: {e}"
|
|
770
833
|
return repairs
|
|
@@ -810,6 +873,7 @@ class SystemTools:
|
|
|
810
873
|
"Use ha_get_device(integration='zha') for full device list."
|
|
811
874
|
)
|
|
812
875
|
except Exception as e:
|
|
876
|
+
_reraise_if_fatal(e)
|
|
813
877
|
logger.warning("Failed to fetch ZHA network data: %s", e)
|
|
814
878
|
zha_network["error"] = f"ZHA integration not available or error: {e}"
|
|
815
879
|
return zha_network
|
|
@@ -825,8 +889,11 @@ class SystemTools:
|
|
|
825
889
|
"total_count": 0,
|
|
826
890
|
}
|
|
827
891
|
try:
|
|
828
|
-
# Get all zwave_js config entries to find entry_id
|
|
829
|
-
|
|
892
|
+
# Get all zwave_js config entries to find entry_id. The HA command
|
|
893
|
+
# is ``config_entries/get`` (underscore); the slash form is rejected
|
|
894
|
+
# as "Unknown command", which the outer except would mask as
|
|
895
|
+
# "Z-Wave JS integration not available".
|
|
896
|
+
entries_result = await ws_client.send_command("config_entries/get")
|
|
830
897
|
zwave_entry_id = None
|
|
831
898
|
if entries_result.get("success"):
|
|
832
899
|
for entry in entries_result.get("result", []):
|
|
@@ -870,6 +937,7 @@ class SystemTools:
|
|
|
870
937
|
"Use ha_get_device(integration='zwave_js') for full device list."
|
|
871
938
|
)
|
|
872
939
|
except Exception as e:
|
|
940
|
+
_reraise_if_fatal(e)
|
|
873
941
|
logger.warning("Failed to fetch Z-Wave network data: %s", e)
|
|
874
942
|
zwave_network["error"] = (
|
|
875
943
|
f"Z-Wave JS integration not available or error: {e}"
|
|
@@ -904,10 +972,283 @@ class SystemTools:
|
|
|
904
972
|
)
|
|
905
973
|
themes_data["error"] = f"Themes data not available: {err_msg}"
|
|
906
974
|
except Exception as e:
|
|
975
|
+
_reraise_if_fatal(e)
|
|
907
976
|
logger.warning("Failed to fetch themes: %s", e)
|
|
908
977
|
themes_data["error"] = f"Themes data not available: {e}"
|
|
909
978
|
return themes_data
|
|
910
979
|
|
|
980
|
+
@staticmethod
|
|
981
|
+
def _ws_result_list(
|
|
982
|
+
resp: Any,
|
|
983
|
+
) -> tuple[list[dict[str, Any]] | None, str | None]:
|
|
984
|
+
"""Unwrap a ``send_websocket_message`` response into ``(list, None)``
|
|
985
|
+
on success or ``(None, error_str)`` on failure.
|
|
986
|
+
|
|
987
|
+
``send_websocket_message`` returns the HA WebSocket envelope
|
|
988
|
+
(``{"success": bool, "result": [...]}``) on success and
|
|
989
|
+
``{"success": False, "error": ...}`` on failure; ``return_exceptions``
|
|
990
|
+
in the caller's ``gather`` can also hand back a raw exception.
|
|
991
|
+
Preserves the underlying cause string (envelope error message,
|
|
992
|
+
exception type, or wrong-shape description) so the caller can
|
|
993
|
+
attribute the failure rather than substitute a fixed "unavailable"
|
|
994
|
+
message that hides the root cause (auth vs command error vs
|
|
995
|
+
malformed envelope). Fatal exceptions (per ``_reraise_if_fatal``)
|
|
996
|
+
unwind instead of being returned as an error string.
|
|
997
|
+
"""
|
|
998
|
+
if isinstance(resp, BaseException):
|
|
999
|
+
# gather(return_exceptions=True) hands back the raw exception; let
|
|
1000
|
+
# truly-fatal ones unwind instead of masking them as "unavailable".
|
|
1001
|
+
_reraise_if_fatal(resp)
|
|
1002
|
+
return None, f"{type(resp).__name__}: {resp}"
|
|
1003
|
+
if not isinstance(resp, dict):
|
|
1004
|
+
return None, f"unexpected response type: {type(resp).__name__}"
|
|
1005
|
+
# Require success truthy before trusting ``result`` — matches the
|
|
1006
|
+
# ``if result.get("success")`` convention used by the other WS handlers
|
|
1007
|
+
# in this file (and treats a malformed envelope missing the key as a
|
|
1008
|
+
# failure rather than reading a half-built result).
|
|
1009
|
+
if not resp.get("success"):
|
|
1010
|
+
err = resp.get("error")
|
|
1011
|
+
if isinstance(err, dict):
|
|
1012
|
+
err_msg = err.get("message") or err.get("code") or str(err)
|
|
1013
|
+
elif err:
|
|
1014
|
+
err_msg = str(err)
|
|
1015
|
+
else:
|
|
1016
|
+
err_msg = "unknown error"
|
|
1017
|
+
return None, str(err_msg)
|
|
1018
|
+
result = resp.get("result")
|
|
1019
|
+
if isinstance(result, list):
|
|
1020
|
+
return result, None
|
|
1021
|
+
return None, f"unexpected result shape: {type(result).__name__}"
|
|
1022
|
+
|
|
1023
|
+
async def _fetch_dead_entities(self) -> dict[str, Any]:
|
|
1024
|
+
"""Surface orphaned/stale entity-registry entries.
|
|
1025
|
+
|
|
1026
|
+
Diffs the entity registry against the state machine and the live
|
|
1027
|
+
config-entries set, classifying findings into confidence tiers:
|
|
1028
|
+
|
|
1029
|
+
- ``config_entry_orphans`` (definitive): registry entries whose
|
|
1030
|
+
``config_entry_id`` is no longer present in ``config_entries/get`` —
|
|
1031
|
+
the owning integration instance was removed, leaving the registry
|
|
1032
|
+
entry behind.
|
|
1033
|
+
- ``stale_restored`` (likely): entries HA recreated from the registry on
|
|
1034
|
+
startup — state ``unavailable`` with ``restored: true`` — whose owning
|
|
1035
|
+
config entry still exists. The integration is loaded but no longer
|
|
1036
|
+
provides the entity (renamed/removed device, re-paired Zigbee).
|
|
1037
|
+
|
|
1038
|
+
Deliberately NEVER flagged, to keep false positives low: ``unknown``
|
|
1039
|
+
state (alive, just no current value — e.g. weather/disaster-alert
|
|
1040
|
+
sensors), bare ``unavailable`` without ``restored`` (a loaded
|
|
1041
|
+
integration reporting a device merely offline right now), and entries
|
|
1042
|
+
disabled via ``disabled_by`` (intentional, unless their config entry is
|
|
1043
|
+
also gone — those still surface as orphans). The ``restored`` flag is
|
|
1044
|
+
what HA sets when it rebuilds a state object from the registry's cached
|
|
1045
|
+
last state because no live platform currently provides the entity; it is
|
|
1046
|
+
the discriminator between "dead" and "temporarily offline". (This tracks
|
|
1047
|
+
HA Core's state-restoration behaviour; re-verify if classification drifts
|
|
1048
|
+
after an HA upgrade.)
|
|
1049
|
+
|
|
1050
|
+
Entities can appear under ``stale_restored`` transiently right after a
|
|
1051
|
+
restart, before integrations finish loading; ``note`` flags this.
|
|
1052
|
+
|
|
1053
|
+
Instance method (not @staticmethod): uses the REST client
|
|
1054
|
+
(``self._client``) for states plus its per-client WebSocket bridge for
|
|
1055
|
+
the registry + config entries, so it needs no system_health ws_client
|
|
1056
|
+
and runs even when the health baseline is unavailable.
|
|
1057
|
+
"""
|
|
1058
|
+
DEAD_ENTITIES_LIMIT = 50
|
|
1059
|
+
dead: dict[str, Any] = {
|
|
1060
|
+
"config_entry_orphans": {"items": [], "count": 0, "total_count": 0},
|
|
1061
|
+
"stale_restored": {"items": [], "count": 0, "total_count": 0},
|
|
1062
|
+
"summary": {"candidate_total": 0, "registry_total": 0},
|
|
1063
|
+
}
|
|
1064
|
+
# Warnings collected here are bubbled to the top-level
|
|
1065
|
+
# ``result["warnings"]`` by the aggregator. The section returns a
|
|
1066
|
+
# plain dict (like every other section helper, keeping the return
|
|
1067
|
+
# signature uniform) with a ``_warnings`` sentinel that
|
|
1068
|
+
# ``ha_get_system_health`` pops and extends onto ``result["warnings"]``
|
|
1069
|
+
# — the documented contract location, which a section-local
|
|
1070
|
+
# ``warnings`` key would collide with.
|
|
1071
|
+
bubble_warnings: list[str] = []
|
|
1072
|
+
try:
|
|
1073
|
+
# Index the gather result (rather than tuple-unpack) so mypy can
|
|
1074
|
+
# type each element through the return_exceptions=True overload;
|
|
1075
|
+
# mirrors smart_search/_entities.py::_fetch_search_entities.
|
|
1076
|
+
results = await asyncio.gather(
|
|
1077
|
+
self._client.get_states(),
|
|
1078
|
+
self._client.send_websocket_message(
|
|
1079
|
+
{"type": "config/entity_registry/list"}
|
|
1080
|
+
),
|
|
1081
|
+
self._client.send_websocket_message({"type": "config_entries/get"}),
|
|
1082
|
+
return_exceptions=True,
|
|
1083
|
+
)
|
|
1084
|
+
states = results[0]
|
|
1085
|
+
|
|
1086
|
+
if isinstance(states, BaseException):
|
|
1087
|
+
# Truly-fatal errors must propagate, not demote to a section
|
|
1088
|
+
# error string (mirrors the ws sections gather pre-pass).
|
|
1089
|
+
_reraise_if_fatal(states)
|
|
1090
|
+
dead["error"] = f"Could not fetch entity states: {states}"
|
|
1091
|
+
return dead
|
|
1092
|
+
if not isinstance(states, list):
|
|
1093
|
+
dead["error"] = (
|
|
1094
|
+
"Could not fetch entity states: expected list, got "
|
|
1095
|
+
f"{type(states).__name__}"
|
|
1096
|
+
)
|
|
1097
|
+
return dead
|
|
1098
|
+
registry, registry_err = self._ws_result_list(results[1])
|
|
1099
|
+
if registry is None:
|
|
1100
|
+
# Preserve the underlying cause (envelope error message,
|
|
1101
|
+
# exception type, or wrong-shape description) so the client
|
|
1102
|
+
# can distinguish auth vs command vs malformed envelope
|
|
1103
|
+
# rather than see a fixed "unavailable" substitute.
|
|
1104
|
+
dead["error"] = (
|
|
1105
|
+
f"Could not fetch entity registry "
|
|
1106
|
+
f"(config/entity_registry/list: {registry_err})"
|
|
1107
|
+
)
|
|
1108
|
+
return dead
|
|
1109
|
+
|
|
1110
|
+
# config-entries is the only optional source: without it the
|
|
1111
|
+
# definitive orphan tier can't be computed, but stale_restored still
|
|
1112
|
+
# can — so degrade rather than fail the whole section.
|
|
1113
|
+
entries, entries_err = self._ws_result_list(results[2])
|
|
1114
|
+
live_entry_ids: set[str] | None = None
|
|
1115
|
+
if entries is None:
|
|
1116
|
+
# Genuine fetch failure — preserve the cause so a backend
|
|
1117
|
+
# failure isn't reported as "no entries".
|
|
1118
|
+
dead["config_entries_checked"] = False
|
|
1119
|
+
bubble_warnings.append(
|
|
1120
|
+
f"config_entries/get failed ({entries_err}); "
|
|
1121
|
+
"config_entry_orphans tier skipped (cannot distinguish a "
|
|
1122
|
+
"removed integration from a failed fetch). stale_restored "
|
|
1123
|
+
"still computed."
|
|
1124
|
+
)
|
|
1125
|
+
elif not entries:
|
|
1126
|
+
# Real empty list — HA reports no config entries configured.
|
|
1127
|
+
# Distinct from a fetch failure: the message names the actual
|
|
1128
|
+
# state. The orphan tier still skips since there is no live
|
|
1129
|
+
# set to diff against; stale_restored still computed.
|
|
1130
|
+
dead["config_entries_checked"] = False
|
|
1131
|
+
bubble_warnings.append(
|
|
1132
|
+
"config_entries/get returned an empty list (no "
|
|
1133
|
+
"integrations configured); config_entry_orphans tier "
|
|
1134
|
+
"skipped. stale_restored still computed."
|
|
1135
|
+
)
|
|
1136
|
+
else:
|
|
1137
|
+
live_entry_ids = {
|
|
1138
|
+
e["entry_id"]
|
|
1139
|
+
for e in entries
|
|
1140
|
+
if isinstance(e, dict) and e.get("entry_id")
|
|
1141
|
+
}
|
|
1142
|
+
dead["config_entries_checked"] = True
|
|
1143
|
+
|
|
1144
|
+
state_by_id = {
|
|
1145
|
+
s["entity_id"]: s
|
|
1146
|
+
for s in states
|
|
1147
|
+
if isinstance(s, dict) and s.get("entity_id")
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
orphans: list[dict[str, Any]] = []
|
|
1151
|
+
stale: list[dict[str, Any]] = []
|
|
1152
|
+
for entry in registry:
|
|
1153
|
+
if not isinstance(entry, dict):
|
|
1154
|
+
continue
|
|
1155
|
+
eid = entry.get("entity_id")
|
|
1156
|
+
if not eid:
|
|
1157
|
+
continue
|
|
1158
|
+
cfg = entry.get("config_entry_id")
|
|
1159
|
+
disabled_by = entry.get("disabled_by")
|
|
1160
|
+
|
|
1161
|
+
# Tier 1 — config-entry orphan (only when the live set loaded).
|
|
1162
|
+
# Covers disabled leftovers too: a disabled entity whose config
|
|
1163
|
+
# entry is gone is still dead cruft (disabled_by is surfaced on
|
|
1164
|
+
# the item so the client sees why it lingered).
|
|
1165
|
+
if live_entry_ids is not None and cfg and cfg not in live_entry_ids:
|
|
1166
|
+
orphans.append(
|
|
1167
|
+
{
|
|
1168
|
+
"entity_id": eid,
|
|
1169
|
+
"platform": entry.get("platform"),
|
|
1170
|
+
"config_entry_id": cfg,
|
|
1171
|
+
"disabled_by": disabled_by,
|
|
1172
|
+
"has_state": eid in state_by_id,
|
|
1173
|
+
}
|
|
1174
|
+
)
|
|
1175
|
+
continue
|
|
1176
|
+
|
|
1177
|
+
# Tier 2 — stale restored. Skip intentionally-disabled entries
|
|
1178
|
+
# (they normally have no state object anyway).
|
|
1179
|
+
if disabled_by is not None:
|
|
1180
|
+
continue
|
|
1181
|
+
state_obj = state_by_id.get(eid)
|
|
1182
|
+
if state_obj is None:
|
|
1183
|
+
continue
|
|
1184
|
+
attrs = state_obj.get("attributes")
|
|
1185
|
+
if (
|
|
1186
|
+
state_obj.get("state") == "unavailable"
|
|
1187
|
+
and isinstance(attrs, dict)
|
|
1188
|
+
and attrs.get("restored")
|
|
1189
|
+
):
|
|
1190
|
+
stale.append(
|
|
1191
|
+
{
|
|
1192
|
+
"entity_id": eid,
|
|
1193
|
+
"platform": entry.get("platform"),
|
|
1194
|
+
"config_entry_id": cfg,
|
|
1195
|
+
}
|
|
1196
|
+
)
|
|
1197
|
+
|
|
1198
|
+
def _bucket(items: list[dict[str, Any]]) -> dict[str, Any]:
|
|
1199
|
+
total = len(items)
|
|
1200
|
+
capped = items[:DEAD_ENTITIES_LIMIT]
|
|
1201
|
+
bucket: dict[str, Any] = {
|
|
1202
|
+
"items": capped,
|
|
1203
|
+
"count": len(capped),
|
|
1204
|
+
"total_count": total,
|
|
1205
|
+
}
|
|
1206
|
+
if total > DEAD_ENTITIES_LIMIT:
|
|
1207
|
+
bucket["truncated"] = True
|
|
1208
|
+
bucket["hint"] = (
|
|
1209
|
+
f"Showing {DEAD_ENTITIES_LIMIT} of {total}; "
|
|
1210
|
+
"remove cleanup candidates in batches and re-run."
|
|
1211
|
+
)
|
|
1212
|
+
return bucket
|
|
1213
|
+
|
|
1214
|
+
candidate_total = len(orphans) + len(stale)
|
|
1215
|
+
dead["config_entry_orphans"] = _bucket(orphans)
|
|
1216
|
+
dead["stale_restored"] = _bucket(stale)
|
|
1217
|
+
dead["summary"] = {
|
|
1218
|
+
"candidate_total": candidate_total,
|
|
1219
|
+
"registry_total": len(registry),
|
|
1220
|
+
}
|
|
1221
|
+
# Only attach the guidance note when there is something to act on —
|
|
1222
|
+
# no point spending tokens on cleanup advice for an empty result.
|
|
1223
|
+
if candidate_total:
|
|
1224
|
+
dead["note"] = (
|
|
1225
|
+
"Excludes 'unknown'-state entities and merely-offline "
|
|
1226
|
+
"devices (bare 'unavailable' without 'restored'). Entries "
|
|
1227
|
+
"can appear under stale_restored transiently right after a "
|
|
1228
|
+
"restart; re-run if HA restarted recently. Remove a "
|
|
1229
|
+
"confirmed-dead entity with ha_remove_entity(entity_id)."
|
|
1230
|
+
)
|
|
1231
|
+
except ToolError:
|
|
1232
|
+
# A ToolError (incl. one re-raised by _reraise_if_fatal) carries the
|
|
1233
|
+
# MCP isError contract — let it reach ha_get_system_health's own
|
|
1234
|
+
# ``except ToolError: raise`` instead of being demoted to a section
|
|
1235
|
+
# error string here (AGENTS.md error-handling guard pattern).
|
|
1236
|
+
raise
|
|
1237
|
+
except Exception as e:
|
|
1238
|
+
_reraise_if_fatal(e)
|
|
1239
|
+
# ``logger.exception`` so an unexpected diff bug gets a full
|
|
1240
|
+
# traceback rather than a one-line warning that hides the
|
|
1241
|
+
# site of the regression.
|
|
1242
|
+
logger.exception("Failed to compute dead entities")
|
|
1243
|
+
dead["error"] = f"Dead-entities diff not available: {e}"
|
|
1244
|
+
# ``_warnings`` is a sentinel that ``ha_get_system_health`` pops to
|
|
1245
|
+
# ``result["warnings"]``; it never reaches the client. Attaching it
|
|
1246
|
+
# outside the try/except keeps it correct on both happy and
|
|
1247
|
+
# embed-as-error paths.
|
|
1248
|
+
if bubble_warnings:
|
|
1249
|
+
dead["_warnings"] = bubble_warnings
|
|
1250
|
+
return dead
|
|
1251
|
+
|
|
911
1252
|
async def _fetch_config_check(self) -> dict[str, Any]:
|
|
912
1253
|
"""Validate HA configuration via POST /config/core/check_config.
|
|
913
1254
|
|
|
@@ -936,6 +1277,7 @@ class SystemTools:
|
|
|
936
1277
|
"errors": errors,
|
|
937
1278
|
}
|
|
938
1279
|
except Exception as e:
|
|
1280
|
+
_reraise_if_fatal(e)
|
|
939
1281
|
logger.warning("Failed to check config: %s", e)
|
|
940
1282
|
config_check["error"] = f"Config check not available: {e}"
|
|
941
1283
|
return config_check
|
|
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.0.dev703 → ha_mcp_dev-7.8.0.dev705}/src/ha_mcp/dashboard_screenshot/__init__.py
RENAMED
|
File without changes
|
{ha_mcp_dev-7.8.0.dev703 → ha_mcp_dev-7.8.0.dev705}/src/ha_mcp/dashboard_screenshot/capture.py
RENAMED
|
File without changes
|
{ha_mcp_dev-7.8.0.dev703 → ha_mcp_dev-7.8.0.dev705}/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
|
|
File without changes
|
{ha_mcp_dev-7.8.0.dev703 → ha_mcp_dev-7.8.0.dev705}/src/ha_mcp/resources/skills-vendor/AGENTS.md
RENAMED
|
File without changes
|
{ha_mcp_dev-7.8.0.dev703 → ha_mcp_dev-7.8.0.dev705}/src/ha_mcp/resources/skills-vendor/CLAUDE.md
RENAMED
|
File without changes
|
|
File without changes
|
{ha_mcp_dev-7.8.0.dev703 → ha_mcp_dev-7.8.0.dev705}/src/ha_mcp/resources/skills-vendor/LICENSE
RENAMED
|
File without changes
|
{ha_mcp_dev-7.8.0.dev703 → ha_mcp_dev-7.8.0.dev705}/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.0.dev703 → ha_mcp_dev-7.8.0.dev705}/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.0.dev703 → ha_mcp_dev-7.8.0.dev705}/src/ha_mcp/tools/smart_search/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{ha_mcp_dev-7.8.0.dev703 → ha_mcp_dev-7.8.0.dev705}/src/ha_mcp/tools/smart_search/_config.py
RENAMED
|
File without changes
|
|
File without changes
|
{ha_mcp_dev-7.8.0.dev703 → ha_mcp_dev-7.8.0.dev705}/src/ha_mcp/tools/smart_search/_entities.py
RENAMED
|
File without changes
|
|
File without changes
|
{ha_mcp_dev-7.8.0.dev703 → ha_mcp_dev-7.8.0.dev705}/src/ha_mcp/tools/smart_search/_overview.py
RENAMED
|
File without changes
|
{ha_mcp_dev-7.8.0.dev703 → ha_mcp_dev-7.8.0.dev705}/src/ha_mcp/tools/smart_search/_scenes.py
RENAMED
|
File without changes
|
{ha_mcp_dev-7.8.0.dev703 → ha_mcp_dev-7.8.0.dev705}/src/ha_mcp/tools/smart_search/_scoring.py
RENAMED
|
File without changes
|
{ha_mcp_dev-7.8.0.dev703 → ha_mcp_dev-7.8.0.dev705}/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
|
|
File without changes
|
{ha_mcp_dev-7.8.0.dev703 → ha_mcp_dev-7.8.0.dev705}/src/ha_mcp/tools/tools_config_automations.py
RENAMED
|
File without changes
|
{ha_mcp_dev-7.8.0.dev703 → ha_mcp_dev-7.8.0.dev705}/src/ha_mcp/tools/tools_config_dashboards.py
RENAMED
|
File without changes
|
{ha_mcp_dev-7.8.0.dev703 → ha_mcp_dev-7.8.0.dev705}/src/ha_mcp/tools/tools_config_helpers.py
RENAMED
|
File without changes
|
|
File without changes
|
{ha_mcp_dev-7.8.0.dev703 → ha_mcp_dev-7.8.0.dev705}/src/ha_mcp/tools/tools_config_scripts.py
RENAMED
|
File without changes
|
{ha_mcp_dev-7.8.0.dev703 → ha_mcp_dev-7.8.0.dev705}/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
|
|
File without changes
|
|
File without changes
|
{ha_mcp_dev-7.8.0.dev703 → ha_mcp_dev-7.8.0.dev705}/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.0.dev703 → ha_mcp_dev-7.8.0.dev705}/src/ha_mcp/tools/validation_middleware.py
RENAMED
|
File without changes
|
|
File without changes
|
{ha_mcp_dev-7.8.0.dev703 → ha_mcp_dev-7.8.0.dev705}/src/ha_mcp/transforms/categorized_search.py
RENAMED
|
File without changes
|
{ha_mcp_dev-7.8.0.dev703 → ha_mcp_dev-7.8.0.dev705}/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.0.dev703 → ha_mcp_dev-7.8.0.dev705}/src/ha_mcp/utils/kill_signal_diagnostics.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.0.dev703 → ha_mcp_dev-7.8.0.dev705}/src/ha_mcp_dev.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{ha_mcp_dev-7.8.0.dev703 → ha_mcp_dev-7.8.0.dev705}/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
|
|
File without changes
|