ha-mcp-dev 7.8.0.dev709__tar.gz → 7.8.0.dev711__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.dev709/src/ha_mcp_dev.egg-info → ha_mcp_dev-7.8.0.dev711}/PKG-INFO +1 -1
- {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/pyproject.toml +1 -1
- {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/backup_manager.py +108 -9
- {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/tools/auto_backup.py +14 -11
- {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/tools/tools_entities.py +1 -0
- {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/tools/tools_registry.py +3 -0
- {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/tools/tools_utility.py +105 -10
- {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711/src/ha_mcp_dev.egg-info}/PKG-INFO +1 -1
- {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/LICENSE +0 -0
- {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/MANIFEST.in +0 -0
- {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/README.md +0 -0
- {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/setup.cfg +0 -0
- {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/__init__.py +0 -0
- {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/__main__.py +0 -0
- {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/_pypi_marker +0 -0
- {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/_version.py +0 -0
- {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/auth/__init__.py +0 -0
- {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/auth/consent_form.py +0 -0
- {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/auth/provider.py +0 -0
- {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/client/__init__.py +0 -0
- {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/client/rest_client.py +0 -0
- {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/client/supervisor_client.py +0 -0
- {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/client/websocket_client.py +0 -0
- {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/client/websocket_listener.py +0 -0
- {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/config.py +0 -0
- {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/dashboard_screenshot/__init__.py +0 -0
- {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/dashboard_screenshot/capture.py +0 -0
- {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/dashboard_screenshot/provision.py +0 -0
- {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/errors.py +0 -0
- {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/policy/__init__.py +0 -0
- {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/policy/approval_queue.py +0 -0
- {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/policy/evaluator.py +0 -0
- {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/policy/handlers.py +0 -0
- {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/policy/middleware.py +0 -0
- {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/policy/model.py +0 -0
- {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/policy/persistence.py +0 -0
- {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/policy/value_sources.py +0 -0
- {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/py.typed +0 -0
- {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/read_only.py +0 -0
- {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/resources/skills-vendor/.claude/settings.json +0 -0
- {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/resources/skills-vendor/.claude-plugin/marketplace.json +0 -0
- {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/resources/skills-vendor/.claude-plugin/plugin.json +0 -0
- {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/resources/skills-vendor/.github/ISSUE_TEMPLATE/skill-rca.md +0 -0
- {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/resources/skills-vendor/.github/pull_request_template.md +0 -0
- {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/resources/skills-vendor/AGENTS.md +0 -0
- {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/resources/skills-vendor/CLAUDE.md +0 -0
- {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/resources/skills-vendor/CONTRIBUTING.md +0 -0
- {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/resources/skills-vendor/LICENSE +0 -0
- {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/resources/skills-vendor/README.md +0 -0
- {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/SKILL.md +0 -0
- {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/evals/evals.json +0 -0
- {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/automation-patterns.md +0 -0
- {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-cards.md +0 -0
- {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-guide.md +0 -0
- {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/device-control.md +0 -0
- {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/domain-docs.md +0 -0
- {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/examples.yaml +0 -0
- {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/helper-selection.md +0 -0
- {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/safe-refactoring.md +0 -0
- {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/template-guidelines.md +0 -0
- {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/yaml-only-integrations.md +0 -0
- {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/server.py +0 -0
- {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/settings.css +0 -0
- {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/settings.js +0 -0
- {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/settings_ui.py +0 -0
- {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/smoke_test.py +0 -0
- {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/stdio_settings_sidecar.py +0 -0
- {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/tools/__init__.py +0 -0
- {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/tools/backup.py +0 -0
- {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/tools/best_practice_checker.py +0 -0
- {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/tools/config_entry_flow.py +0 -0
- {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/tools/device_control.py +0 -0
- {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/tools/enhanced.py +0 -0
- {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/tools/helpers.py +0 -0
- {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/tools/reference_validator.py +0 -0
- {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/tools/registry.py +0 -0
- {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/tools/smart_search/__init__.py +0 -0
- {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/tools/smart_search/_base.py +0 -0
- {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/tools/smart_search/_config.py +0 -0
- {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/tools/smart_search/_deep.py +0 -0
- {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/tools/smart_search/_entities.py +0 -0
- {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/tools/smart_search/_fetch.py +0 -0
- {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/tools/smart_search/_overview.py +0 -0
- {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/tools/smart_search/_scenes.py +0 -0
- {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/tools/smart_search/_scoring.py +0 -0
- {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/tools/tool_search_hint_middleware.py +0 -0
- {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/tools/tools_addons.py +0 -0
- {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/tools/tools_areas.py +0 -0
- {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/tools/tools_blueprints.py +0 -0
- {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/tools/tools_bug_report.py +0 -0
- {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/tools/tools_calendar.py +0 -0
- {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/tools/tools_camera.py +0 -0
- {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/tools/tools_categories.py +0 -0
- {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/tools/tools_code.py +0 -0
- {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/tools/tools_config_automations.py +0 -0
- {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/tools/tools_config_dashboards.py +0 -0
- {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/tools/tools_config_helpers.py +0 -0
- {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/tools/tools_config_scenes.py +0 -0
- {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/tools/tools_config_scripts.py +0 -0
- {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/tools/tools_dashboard_screenshot.py +0 -0
- {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/tools/tools_energy.py +0 -0
- {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/tools/tools_filesystem.py +0 -0
- {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/tools/tools_groups.py +0 -0
- {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/tools/tools_hacs.py +0 -0
- {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/tools/tools_history.py +0 -0
- {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/tools/tools_integrations.py +0 -0
- {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/tools/tools_labels.py +0 -0
- {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/tools/tools_mcp_component.py +0 -0
- {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/tools/tools_resources.py +0 -0
- {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/tools/tools_search.py +0 -0
- {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/tools/tools_service.py +0 -0
- {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/tools/tools_services.py +0 -0
- {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/tools/tools_system.py +0 -0
- {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/tools/tools_themes.py +0 -0
- {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/tools/tools_todo.py +0 -0
- {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/tools/tools_traces.py +0 -0
- {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/tools/tools_updates.py +0 -0
- {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/tools/tools_voice_assistant.py +0 -0
- {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/tools/tools_yaml_config.py +0 -0
- {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/tools/tools_zones.py +0 -0
- {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/tools/util_helpers.py +0 -0
- {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/tools/validation_middleware.py +0 -0
- {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/transforms/__init__.py +0 -0
- {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/transforms/categorized_search.py +0 -0
- {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/transforms/lite_docstrings.py +0 -0
- {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/utils/__init__.py +0 -0
- {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/utils/config_hash.py +0 -0
- {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/utils/data_paths.py +0 -0
- {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/utils/domain_handlers.py +0 -0
- {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/utils/fuzzy_search.py +0 -0
- {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/utils/kill_signal_diagnostics.py +0 -0
- {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/utils/operation_manager.py +0 -0
- {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/utils/python_sandbox.py +0 -0
- {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/utils/skill_loader.py +0 -0
- {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/utils/usage_logger.py +0 -0
- {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp_dev.egg-info/SOURCES.txt +0 -0
- {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp_dev.egg-info/dependency_links.txt +0 -0
- {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp_dev.egg-info/entry_points.txt +0 -0
- {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp_dev.egg-info/requires.txt +0 -0
- {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp_dev.egg-info/top_level.txt +0 -0
- {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/tests/__init__.py +0 -0
- {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/tests/test_constants.py +0 -0
- {ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/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.dev711"
|
|
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"
|
|
@@ -991,19 +991,46 @@ async def _fetch_dashboard(client: Any, entity_id: str) -> Any:
|
|
|
991
991
|
"""
|
|
992
992
|
from fastmcp.exceptions import ToolError
|
|
993
993
|
|
|
994
|
-
from .tools.tools_config_dashboards import
|
|
994
|
+
from .tools.tools_config_dashboards import (
|
|
995
|
+
_get_dashboard_config_internal,
|
|
996
|
+
_resolve_dashboard,
|
|
997
|
+
)
|
|
998
|
+
|
|
999
|
+
# The set/delete tools accept BOTH the canonical hyphenated url_path
|
|
1000
|
+
# AND HA's internal (underscored) dashboard id, eagerly resolving the
|
|
1001
|
+
# latter before writing. ``_get_dashboard_config_internal`` does NOT
|
|
1002
|
+
# lazy-resolve, so an internal-id identifier 404s with "Unknown config
|
|
1003
|
+
# specified" and the pre-write snapshot is silently skipped. Pre-resolve
|
|
1004
|
+
# to the canonical url_path so capture works for whichever form the
|
|
1005
|
+
# caller passed (matching the form the write tool ultimately targets).
|
|
1006
|
+
fetch_path = entity_id
|
|
1007
|
+
try:
|
|
1008
|
+
match, _ = await _resolve_dashboard(client, entity_id)
|
|
1009
|
+
if match and match.get("url_path"):
|
|
1010
|
+
fetch_path = match["url_path"]
|
|
1011
|
+
except (HomeAssistantError, ToolError) as err:
|
|
1012
|
+
# Resolver failure (transport/shape) — fall through with the
|
|
1013
|
+
# original identifier; the canonical form is often already correct.
|
|
1014
|
+
logger.debug(
|
|
1015
|
+
"Auto-backup: dashboard resolve failed for %r: %s — using as-is",
|
|
1016
|
+
entity_id,
|
|
1017
|
+
err,
|
|
1018
|
+
)
|
|
995
1019
|
|
|
996
1020
|
try:
|
|
997
|
-
config, _config_hash = await _get_dashboard_config_internal(client,
|
|
1021
|
+
config, _config_hash = await _get_dashboard_config_internal(client, fetch_path)
|
|
998
1022
|
except ToolError as err:
|
|
999
|
-
# ToolError carries the structured failure payload; treat
|
|
1000
|
-
# missing
|
|
1023
|
+
# ToolError carries the structured failure payload; treat a
|
|
1024
|
+
# missing/unknown dashboard as "nothing to back up" (also covers a
|
|
1025
|
+
# brand-new dashboard on the create path). "Unknown config
|
|
1026
|
+
# specified" is HA's message for an unresolved url_path.
|
|
1001
1027
|
msg = str(err).lower()
|
|
1002
|
-
if "not_found" in msg or "config_not_found" in msg:
|
|
1028
|
+
if "not_found" in msg or "config_not_found" in msg or "unknown config" in msg:
|
|
1003
1029
|
return None
|
|
1004
1030
|
raise
|
|
1005
1031
|
except HomeAssistantError as err:
|
|
1006
|
-
|
|
1032
|
+
msg = str(err).lower()
|
|
1033
|
+
if "not_found" in msg or "config_not_found" in msg or "unknown config" in msg:
|
|
1007
1034
|
return None
|
|
1008
1035
|
raise
|
|
1009
1036
|
return config
|
|
@@ -1305,8 +1332,11 @@ async def _restore_area_or_floor(client: Any, entity_id: str, config: Any) -> An
|
|
|
1305
1332
|
|
|
1306
1333
|
|
|
1307
1334
|
async def _fetch_todo_item(client: Any, entity_id: str) -> Any:
|
|
1308
|
-
|
|
1309
|
-
|
|
1335
|
+
# The second segment is whatever the tool's ``item`` param carried.
|
|
1336
|
+
# ha_set_todo_item / ha_remove_todo_item accept EITHER the item uid OR
|
|
1337
|
+
# its exact summary/name, so this can be either form.
|
|
1338
|
+
cal, _, item_ref = entity_id.partition("::")
|
|
1339
|
+
if not cal or not item_ref:
|
|
1310
1340
|
return None
|
|
1311
1341
|
payload = {
|
|
1312
1342
|
"type": "execute_script",
|
|
@@ -1332,7 +1362,11 @@ async def _fetch_todo_item(client: Any, entity_id: str) -> Any:
|
|
|
1332
1362
|
result = _require_dict(result, "execute_script")
|
|
1333
1363
|
items = result.get("response", {}).get("items", {}).get(cal, {}).get("items", [])
|
|
1334
1364
|
for item in items:
|
|
1335
|
-
|
|
1365
|
+
# Match either form. Matching only on uid silently skipped the
|
|
1366
|
+
# snapshot whenever the caller passed the human-readable summary
|
|
1367
|
+
# (the documented/common case, e.g. ha_remove_todo_item(list, "Buy
|
|
1368
|
+
# milk")) — uid != summary, so the loop found nothing -> None.
|
|
1369
|
+
if item.get("uid") == item_ref or item.get("summary") == item_ref:
|
|
1336
1370
|
return {"todo_entity_id": cal, **item}
|
|
1337
1371
|
return None
|
|
1338
1372
|
|
|
@@ -1366,6 +1400,42 @@ async def _restore_entity_state(client: Any, entity_id: str, config: Any) -> Any
|
|
|
1366
1400
|
return await _rest_post(client, f"states/{entity_id}", payload)
|
|
1367
1401
|
|
|
1368
1402
|
|
|
1403
|
+
# Devices — config/device_registry/{list,update}. ``ha_set_device`` mutates
|
|
1404
|
+
# the user-editable registry fields (name_by_user / area_id / disabled_by /
|
|
1405
|
+
# labels); restore re-applies exactly those. A device deleted by
|
|
1406
|
+
# ``ha_remove_device`` cannot be recreated through the registry, so for that
|
|
1407
|
+
# path the snapshot is an informational pre-delete record and restore is
|
|
1408
|
+
# best-effort.
|
|
1409
|
+
|
|
1410
|
+
|
|
1411
|
+
async def _fetch_device(client: Any, entity_id: str) -> Any:
|
|
1412
|
+
items = await _ws_send(client, {"type": "config/device_registry/list"})
|
|
1413
|
+
if not isinstance(items, list):
|
|
1414
|
+
return None
|
|
1415
|
+
for item in items:
|
|
1416
|
+
if item.get("id") == entity_id:
|
|
1417
|
+
return item
|
|
1418
|
+
return None
|
|
1419
|
+
|
|
1420
|
+
|
|
1421
|
+
async def _restore_device(client: Any, entity_id: str, config: Any) -> Any:
|
|
1422
|
+
# Re-apply the captured registry state. Uses the same field NAMES as
|
|
1423
|
+
# ``_update_device_internal`` but, unlike that partial-update path, always
|
|
1424
|
+
# sends all four — restore reverts the device to the snapshot, so a
|
|
1425
|
+
# captured ``None`` area/name is intentionally re-applied (cleared).
|
|
1426
|
+
return await _ws_send(
|
|
1427
|
+
client,
|
|
1428
|
+
{
|
|
1429
|
+
"type": "config/device_registry/update",
|
|
1430
|
+
"device_id": entity_id,
|
|
1431
|
+
"name_by_user": config.get("name_by_user"),
|
|
1432
|
+
"area_id": config.get("area_id"),
|
|
1433
|
+
"disabled_by": config.get("disabled_by"),
|
|
1434
|
+
"labels": config.get("labels", []),
|
|
1435
|
+
},
|
|
1436
|
+
)
|
|
1437
|
+
|
|
1438
|
+
|
|
1369
1439
|
# Integration enable/disable — restore re-applies the disabled flag.
|
|
1370
1440
|
|
|
1371
1441
|
|
|
@@ -1435,6 +1505,34 @@ async def _fetch_helper(client: Any, entity_id: str, helper_type: str) -> Any:
|
|
|
1435
1505
|
for item in items:
|
|
1436
1506
|
if item.get("id") == object_id or item.get("id") == entity_id:
|
|
1437
1507
|
return item
|
|
1508
|
+
# Fallback for renamed helpers: after an entity_id rename the object_id
|
|
1509
|
+
# no longer equals the storage collection id (which stays the original
|
|
1510
|
+
# create-time id == the registry unique_id), so the direct match above
|
|
1511
|
+
# misses and the snapshot was silently skipped. Resolve the unique_id
|
|
1512
|
+
# via the entity registry and match on that — the same key the helper
|
|
1513
|
+
# update tool itself resolves to.
|
|
1514
|
+
eid = entity_id if "." in entity_id else f"{helper_type}.{entity_id}"
|
|
1515
|
+
try:
|
|
1516
|
+
entry = await _ws_send(
|
|
1517
|
+
client, {"type": "config/entity_registry/get", "entity_id": eid}
|
|
1518
|
+
)
|
|
1519
|
+
except HomeAssistantError as err:
|
|
1520
|
+
# Only a genuine "entity not found" means there's nothing to back up;
|
|
1521
|
+
# transport/auth/5xx errors must propagate so maybe_snapshot logs a
|
|
1522
|
+
# WARNING rather than silently skipping. Same POLICY as _fetch_automation,
|
|
1523
|
+
# but matched on the message substring because config/entity_registry/get
|
|
1524
|
+
# failures arrive as a WS command error with no status_code to switch on.
|
|
1525
|
+
# Best-effort: if HA's not-found wording ever changes, a real miss
|
|
1526
|
+
# degrades to a WARNING + skip (never a swallowed fatal error).
|
|
1527
|
+
msg = str(err).lower()
|
|
1528
|
+
if "not_found" in msg or "not found" in msg:
|
|
1529
|
+
return None
|
|
1530
|
+
raise
|
|
1531
|
+
unique_id = entry.get("unique_id") if isinstance(entry, dict) else None
|
|
1532
|
+
if unique_id:
|
|
1533
|
+
for item in items:
|
|
1534
|
+
if str(item.get("id")) == str(unique_id):
|
|
1535
|
+
return item
|
|
1438
1536
|
return None
|
|
1439
1537
|
|
|
1440
1538
|
|
|
@@ -1505,6 +1603,7 @@ def register_default_handlers(mgr: BackupManager, _client: Any) -> None:
|
|
|
1505
1603
|
)
|
|
1506
1604
|
mgr.register(DomainHandler("todo_item", _fetch_todo_item, _restore_todo_item))
|
|
1507
1605
|
mgr.register(DomainHandler("entity", _fetch_entity_state, _restore_entity_state))
|
|
1606
|
+
mgr.register(DomainHandler("device", _fetch_device, _restore_device))
|
|
1508
1607
|
mgr.register(DomainHandler("integration", _fetch_integration, _restore_integration))
|
|
1509
1608
|
for helper_type in _KNOWN_HELPER_TYPES:
|
|
1510
1609
|
mgr.register(_make_helper_handler(helper_type))
|
|
@@ -75,17 +75,20 @@ def automation_backup_target(kw: dict[str, Any]) -> str:
|
|
|
75
75
|
config_id = config.get("id")
|
|
76
76
|
if config_id:
|
|
77
77
|
return str(config_id)
|
|
78
|
-
identifier
|
|
79
|
-
#
|
|
80
|
-
#
|
|
81
|
-
#
|
|
82
|
-
#
|
|
83
|
-
#
|
|
84
|
-
#
|
|
85
|
-
#
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
78
|
+
# Return the identifier UNCHANGED — do NOT strip the ``automation.``
|
|
79
|
+
# prefix. Capture and restore resolve the target through
|
|
80
|
+
# ``client.get_automation_config`` -> ``_resolve_automation_id``, which
|
|
81
|
+
# converts an entity_id ("automation.<slug>") to the real numeric
|
|
82
|
+
# ``unique_id`` via a state lookup ONLY when the prefix is present;
|
|
83
|
+
# otherwise it assumes the string already IS a unique_id. Stripping the
|
|
84
|
+
# prefix produced a bare object_id slug that the resolver mis-treats as
|
|
85
|
+
# a unique_id -> GET /config/automation/config/<slug> 404s -> the
|
|
86
|
+
# pre-write snapshot is silently skipped (and, had it resolved, restore
|
|
87
|
+
# would POST to the wrong key and create a stray automation). The
|
|
88
|
+
# doubled domain segment in the snapshot filename
|
|
89
|
+
# ("automation.automation.<slug>.<ts>.yaml") is purely cosmetic and is
|
|
90
|
+
# exactly what the remove path (id_param="identifier") already produces.
|
|
91
|
+
return _resolve_str(kw.get("identifier"))
|
|
89
92
|
|
|
90
93
|
|
|
91
94
|
def with_auto_backup(
|
|
@@ -1316,6 +1316,7 @@ def register_entity_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
|
|
|
1316
1316
|
"title": "Remove Entity",
|
|
1317
1317
|
},
|
|
1318
1318
|
)
|
|
1319
|
+
@with_auto_backup(domain="entity", id_param="entity_id", client=client)
|
|
1319
1320
|
@log_tool_usage
|
|
1320
1321
|
async def ha_remove_entity(
|
|
1321
1322
|
entity_id: Annotated[
|
|
@@ -14,6 +14,7 @@ from pydantic import Field
|
|
|
14
14
|
|
|
15
15
|
from ..client.rest_client import HomeAssistantAPIError, HomeAssistantConnectionError
|
|
16
16
|
from ..errors import ErrorCode, create_error_response
|
|
17
|
+
from .auto_backup import with_auto_backup
|
|
17
18
|
from .helpers import (
|
|
18
19
|
exception_to_structured_error,
|
|
19
20
|
log_tool_usage,
|
|
@@ -599,6 +600,7 @@ def register_registry_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
|
|
|
599
600
|
tags={"Device Registry"},
|
|
600
601
|
annotations={"destructiveHint": True, "title": "Set Device"},
|
|
601
602
|
)
|
|
603
|
+
@with_auto_backup(domain="device", id_param="device_id", client=client)
|
|
602
604
|
@log_tool_usage
|
|
603
605
|
async def ha_set_device(
|
|
604
606
|
device_id: Annotated[
|
|
@@ -700,6 +702,7 @@ def register_registry_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
|
|
|
700
702
|
"title": "Remove Device",
|
|
701
703
|
},
|
|
702
704
|
)
|
|
705
|
+
@with_auto_backup(domain="device", id_param="device_id", client=client)
|
|
703
706
|
@log_tool_usage
|
|
704
707
|
async def ha_remove_device(
|
|
705
708
|
device_id: Annotated[
|
|
@@ -122,8 +122,14 @@ class UtilityTools:
|
|
|
122
122
|
entity_id: str | None,
|
|
123
123
|
end_time: str | None,
|
|
124
124
|
slug: str | None,
|
|
125
|
+
order: Literal["newest", "oldest"],
|
|
125
126
|
) -> list[str]:
|
|
126
127
|
warnings: list[str] = []
|
|
128
|
+
if source == "logger" and order != "newest":
|
|
129
|
+
warnings.append(
|
|
130
|
+
"Parameter 'order' does not apply to source='logger' "
|
|
131
|
+
"(entries are sorted by integration name); ignored"
|
|
132
|
+
)
|
|
127
133
|
if source != "logbook" and any(p is not None for p in [entity_id, end_time]):
|
|
128
134
|
ignored = [
|
|
129
135
|
p
|
|
@@ -200,6 +206,7 @@ class UtilityTools:
|
|
|
200
206
|
compact: bool,
|
|
201
207
|
level: str | None,
|
|
202
208
|
slug: str | None,
|
|
209
|
+
order: Literal["newest", "oldest"],
|
|
203
210
|
) -> dict[str, Any]:
|
|
204
211
|
if source == "logbook":
|
|
205
212
|
return await self._get_logbook(
|
|
@@ -210,20 +217,29 @@ class UtilityTools:
|
|
|
210
217
|
offset=offset,
|
|
211
218
|
search=search,
|
|
212
219
|
compact=compact,
|
|
220
|
+
order=order,
|
|
213
221
|
)
|
|
214
222
|
if source == "system":
|
|
215
|
-
return await self._get_system_log(
|
|
223
|
+
return await self._get_system_log(
|
|
224
|
+
limit=limit, search=search, level=level, order=order
|
|
225
|
+
)
|
|
216
226
|
if source == "error_log":
|
|
217
|
-
return await self._get_error_log(
|
|
227
|
+
return await self._get_error_log(
|
|
228
|
+
limit=limit, search=search, level=level, order=order
|
|
229
|
+
)
|
|
218
230
|
if source == "logger":
|
|
231
|
+
# logger reports per-integration levels, not time-ordered events;
|
|
232
|
+
# 'order' does not apply (a warning is emitted upstream).
|
|
219
233
|
return await self._get_logger_info(limit=limit, search=search)
|
|
220
234
|
if source == "system_service":
|
|
221
235
|
assert slug is not None # guaranteed by _validate_log_slug
|
|
222
236
|
return await self._get_system_service_log(
|
|
223
|
-
service=slug, limit=limit, search=search
|
|
237
|
+
service=slug, limit=limit, search=search, order=order
|
|
224
238
|
)
|
|
225
239
|
assert slug is not None # guaranteed by _validate_log_slug
|
|
226
|
-
return await self._get_supervisor_log(
|
|
240
|
+
return await self._get_supervisor_log(
|
|
241
|
+
slug=slug, limit=limit, search=search, order=order
|
|
242
|
+
)
|
|
227
243
|
|
|
228
244
|
async def get_logs(
|
|
229
245
|
self,
|
|
@@ -237,9 +253,12 @@ class UtilityTools:
|
|
|
237
253
|
compact: bool,
|
|
238
254
|
level: str | None,
|
|
239
255
|
slug: str | None,
|
|
256
|
+
order: Literal["newest", "oldest"] = "newest",
|
|
240
257
|
) -> dict[str, Any]:
|
|
241
258
|
level = self._validate_log_level(level)
|
|
242
|
-
warnings = self._collect_log_warnings(
|
|
259
|
+
warnings = self._collect_log_warnings(
|
|
260
|
+
source, level, entity_id, end_time, slug, order
|
|
261
|
+
)
|
|
243
262
|
self._validate_log_slug(source, slug)
|
|
244
263
|
result = await self._fetch_log_source(
|
|
245
264
|
source,
|
|
@@ -252,6 +271,7 @@ class UtilityTools:
|
|
|
252
271
|
compact,
|
|
253
272
|
level,
|
|
254
273
|
slug,
|
|
274
|
+
order,
|
|
255
275
|
)
|
|
256
276
|
if warnings:
|
|
257
277
|
result["warnings"] = warnings
|
|
@@ -277,6 +297,7 @@ class UtilityTools:
|
|
|
277
297
|
entity_id: str | None,
|
|
278
298
|
search: str | None,
|
|
279
299
|
compact_bool: bool,
|
|
300
|
+
order: Literal["newest", "oldest"] = "newest",
|
|
280
301
|
) -> str:
|
|
281
302
|
"""Build reproducible pagination hint string for logbook results."""
|
|
282
303
|
next_offset = offset_int + effective_limit
|
|
@@ -293,6 +314,8 @@ class UtilityTools:
|
|
|
293
314
|
param_parts.append(f"search={search}")
|
|
294
315
|
if not compact_bool:
|
|
295
316
|
param_parts.append("compact=False")
|
|
317
|
+
if order != "newest":
|
|
318
|
+
param_parts.append(f"order={order}")
|
|
296
319
|
param_str = ", ".join(param_parts)
|
|
297
320
|
return (
|
|
298
321
|
f"Showing entries {offset_int + 1}-{offset_int + len(paginated_entries)} of {total_entries}. "
|
|
@@ -308,6 +331,7 @@ class UtilityTools:
|
|
|
308
331
|
offset: int = 0,
|
|
309
332
|
search: str | None = None,
|
|
310
333
|
compact: bool = True,
|
|
334
|
+
order: Literal["newest", "oldest"] = "newest",
|
|
311
335
|
) -> dict[str, Any]:
|
|
312
336
|
"""Fetch logbook entries with search and pagination."""
|
|
313
337
|
hours_back_int, effective_limit, offset_int = self._coerce_logbook_params(
|
|
@@ -342,8 +366,20 @@ class UtilityTools:
|
|
|
342
366
|
total_entries = len(response) if isinstance(response, list) else 1
|
|
343
367
|
|
|
344
368
|
if isinstance(response, list):
|
|
345
|
-
|
|
346
|
-
|
|
369
|
+
# HA's /logbook returns entries oldest-first. Take a window from
|
|
370
|
+
# the end for newest-first (default), or from the start for
|
|
371
|
+
# oldest-first, with offset paging deeper in the chosen order.
|
|
372
|
+
if order == "newest":
|
|
373
|
+
end = total_entries - offset_int
|
|
374
|
+
start = max(end - effective_limit, 0)
|
|
375
|
+
paginated_entries = (
|
|
376
|
+
list(reversed(response[start:end])) if end > 0 else []
|
|
377
|
+
)
|
|
378
|
+
else:
|
|
379
|
+
paginated_entries = response[
|
|
380
|
+
offset_int : offset_int + effective_limit
|
|
381
|
+
]
|
|
382
|
+
has_more = offset_int + len(paginated_entries) < total_entries
|
|
347
383
|
else:
|
|
348
384
|
paginated_entries = response
|
|
349
385
|
has_more = False
|
|
@@ -368,6 +404,7 @@ class UtilityTools:
|
|
|
368
404
|
else 1,
|
|
369
405
|
"limit": effective_limit,
|
|
370
406
|
"offset": offset_int,
|
|
407
|
+
"order": order,
|
|
371
408
|
"has_more": has_more,
|
|
372
409
|
}
|
|
373
410
|
if filters_applied:
|
|
@@ -383,6 +420,7 @@ class UtilityTools:
|
|
|
383
420
|
entity_id,
|
|
384
421
|
search,
|
|
385
422
|
compact,
|
|
423
|
+
order,
|
|
386
424
|
)
|
|
387
425
|
|
|
388
426
|
return await add_timezone_metadata(self._client, logbook_data)
|
|
@@ -416,11 +454,28 @@ class UtilityTools:
|
|
|
416
454
|
)
|
|
417
455
|
raise # unreachable: exception_to_structured_error always raises
|
|
418
456
|
|
|
457
|
+
@staticmethod
|
|
458
|
+
def _system_log_sort_key(entry: Any) -> float:
|
|
459
|
+
"""Total-order-safe sort key for system_log entries.
|
|
460
|
+
|
|
461
|
+
``system_log/list`` does not guarantee a numeric ``timestamp`` on every
|
|
462
|
+
record. Coerce a missing / non-numeric / non-dict entry to ``0.0`` so
|
|
463
|
+
sorting never raises a cross-type ``TypeError`` (bools are excluded so
|
|
464
|
+
a stray ``True`` doesn't read as ``1.0``).
|
|
465
|
+
"""
|
|
466
|
+
if not isinstance(entry, dict):
|
|
467
|
+
return 0.0
|
|
468
|
+
ts = entry.get("timestamp")
|
|
469
|
+
if isinstance(ts, bool) or not isinstance(ts, (int, float)):
|
|
470
|
+
return 0.0
|
|
471
|
+
return float(ts)
|
|
472
|
+
|
|
419
473
|
async def _get_system_log(
|
|
420
474
|
self,
|
|
421
475
|
limit: int | None = None,
|
|
422
476
|
search: str | None = None,
|
|
423
477
|
level: str | None = None,
|
|
478
|
+
order: Literal["newest", "oldest"] = "newest",
|
|
424
479
|
) -> dict[str, Any]:
|
|
425
480
|
"""Fetch structured system log entries via system_log/list."""
|
|
426
481
|
effective_limit = self._coerce_limit(limit)
|
|
@@ -461,6 +516,17 @@ class UtilityTools:
|
|
|
461
516
|
]
|
|
462
517
|
filters_applied["search"] = search
|
|
463
518
|
|
|
519
|
+
# system_log/list entries carry a 'timestamp' (epoch float, last
|
|
520
|
+
# occurrence), but HA does not guarantee it on every record. Sort
|
|
521
|
+
# with a total-order-safe key so 'order' is deterministic regardless
|
|
522
|
+
# of HA's native ordering (newest-first by default) and a missing /
|
|
523
|
+
# non-numeric / non-dict entry can never raise a cross-type
|
|
524
|
+
# TypeError out of this method's narrow except clause.
|
|
525
|
+
entries.sort(
|
|
526
|
+
key=self._system_log_sort_key,
|
|
527
|
+
reverse=(order == "newest"),
|
|
528
|
+
)
|
|
529
|
+
|
|
464
530
|
total_entries = len(entries)
|
|
465
531
|
entries = entries[:effective_limit]
|
|
466
532
|
|
|
@@ -471,6 +537,7 @@ class UtilityTools:
|
|
|
471
537
|
"total_entries": total_entries,
|
|
472
538
|
"returned_entries": len(entries),
|
|
473
539
|
"limit": effective_limit,
|
|
540
|
+
"order": order,
|
|
474
541
|
}
|
|
475
542
|
if filters_applied:
|
|
476
543
|
data["filters_applied"] = filters_applied
|
|
@@ -500,6 +567,7 @@ class UtilityTools:
|
|
|
500
567
|
limit: int | None = None,
|
|
501
568
|
search: str | None = None,
|
|
502
569
|
level: str | None = None,
|
|
570
|
+
order: Literal["newest", "oldest"] = "newest",
|
|
503
571
|
) -> dict[str, Any]:
|
|
504
572
|
"""Fetch raw error log text from home-assistant.log."""
|
|
505
573
|
effective_limit = self._coerce_limit(
|
|
@@ -527,8 +595,11 @@ class UtilityTools:
|
|
|
527
595
|
filters_applied["search"] = search
|
|
528
596
|
|
|
529
597
|
total_lines = len(lines)
|
|
530
|
-
#
|
|
598
|
+
# Always take the most-recent window (the tail of the chronological
|
|
599
|
+
# file); 'order' controls only the display direction of that window.
|
|
531
600
|
lines = lines[-effective_limit:]
|
|
601
|
+
if order == "newest":
|
|
602
|
+
lines = list(reversed(lines))
|
|
532
603
|
|
|
533
604
|
data: dict[str, Any] = {
|
|
534
605
|
"success": True,
|
|
@@ -537,6 +608,7 @@ class UtilityTools:
|
|
|
537
608
|
"total_lines": total_lines,
|
|
538
609
|
"returned_lines": len(lines),
|
|
539
610
|
"limit": effective_limit,
|
|
611
|
+
"order": order,
|
|
540
612
|
"note": "Returned the most recent log lines matching filters",
|
|
541
613
|
}
|
|
542
614
|
if filters_applied:
|
|
@@ -665,6 +737,7 @@ class UtilityTools:
|
|
|
665
737
|
slug: str,
|
|
666
738
|
limit: int | None = None,
|
|
667
739
|
search: str | None = None,
|
|
740
|
+
order: Literal["newest", "oldest"] = "newest",
|
|
668
741
|
) -> dict[str, Any]:
|
|
669
742
|
"""Fetch add-on container logs.
|
|
670
743
|
|
|
@@ -692,8 +765,11 @@ class UtilityTools:
|
|
|
692
765
|
filters_applied["search"] = search
|
|
693
766
|
|
|
694
767
|
total_lines = len(lines)
|
|
695
|
-
#
|
|
768
|
+
# Always take the most-recent window (the tail); 'order' controls
|
|
769
|
+
# only the display direction of that window.
|
|
696
770
|
lines = lines[-effective_limit:]
|
|
771
|
+
if order == "newest":
|
|
772
|
+
lines = list(reversed(lines))
|
|
697
773
|
|
|
698
774
|
data: dict[str, Any] = {
|
|
699
775
|
"success": True,
|
|
@@ -703,6 +779,7 @@ class UtilityTools:
|
|
|
703
779
|
"total_lines": total_lines,
|
|
704
780
|
"returned_lines": len(lines),
|
|
705
781
|
"limit": effective_limit,
|
|
782
|
+
"order": order,
|
|
706
783
|
}
|
|
707
784
|
if filters_applied:
|
|
708
785
|
data["filters_applied"] = filters_applied
|
|
@@ -797,6 +874,7 @@ class UtilityTools:
|
|
|
797
874
|
service: str,
|
|
798
875
|
limit: int | None = None,
|
|
799
876
|
search: str | None = None,
|
|
877
|
+
order: Literal["newest", "oldest"] = "newest",
|
|
800
878
|
) -> dict[str, Any]:
|
|
801
879
|
"""Fetch HA system-service logs from Supervisor's per-service endpoint.
|
|
802
880
|
|
|
@@ -826,8 +904,11 @@ class UtilityTools:
|
|
|
826
904
|
filters_applied["search"] = search
|
|
827
905
|
|
|
828
906
|
total_lines = len(lines)
|
|
829
|
-
#
|
|
907
|
+
# Always take the most-recent window (the tail); 'order' controls
|
|
908
|
+
# only the display direction of that window.
|
|
830
909
|
lines = lines[-effective_limit:]
|
|
910
|
+
if order == "newest":
|
|
911
|
+
lines = list(reversed(lines))
|
|
831
912
|
|
|
832
913
|
data: dict[str, Any] = {
|
|
833
914
|
"success": True,
|
|
@@ -837,6 +918,7 @@ class UtilityTools:
|
|
|
837
918
|
"total_lines": total_lines,
|
|
838
919
|
"returned_lines": len(lines),
|
|
839
920
|
"limit": effective_limit,
|
|
921
|
+
"order": order,
|
|
840
922
|
}
|
|
841
923
|
if filters_applied:
|
|
842
924
|
data["filters_applied"] = filters_applied
|
|
@@ -1035,6 +1117,17 @@ def register_utility_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
|
|
|
1035
1117
|
# Shared parameters
|
|
1036
1118
|
limit: int | None = None,
|
|
1037
1119
|
search: str | None = None,
|
|
1120
|
+
order: Annotated[
|
|
1121
|
+
Literal["newest", "oldest"],
|
|
1122
|
+
Field(
|
|
1123
|
+
description=(
|
|
1124
|
+
"Sort order for time-ordered sources (logbook, system, "
|
|
1125
|
+
"error_log, supervisor, system_service): 'newest' (default) "
|
|
1126
|
+
"returns most-recent first; 'oldest' returns chronological-"
|
|
1127
|
+
"first. Ignored for source='logger'."
|
|
1128
|
+
)
|
|
1129
|
+
),
|
|
1130
|
+
] = "newest",
|
|
1038
1131
|
# Logbook-specific (ignored for other sources)
|
|
1039
1132
|
hours_back: Annotated[int, Field(ge=1)] = 1,
|
|
1040
1133
|
entity_id: str | None = None,
|
|
@@ -1059,6 +1152,7 @@ def register_utility_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
|
|
|
1059
1152
|
- "logger": Effective log level per integration via logger/log_info (confirms logger.set_level changes took effect)
|
|
1060
1153
|
|
|
1061
1154
|
**Shared params:** limit, search (keyword filter on entries/lines; matches integration domain for source='logger')
|
|
1155
|
+
**Order:** order='newest' (default) returns most-recent first; order='oldest' returns chronological-first. Applies to all time-ordered sources (logbook, system, error_log, supervisor, system_service); ignored for source='logger'. For raw-text sources (error_log, supervisor, system_service) it sets the read direction of the most-recent window.
|
|
1062
1156
|
**Logbook params:** hours_back, entity_id, end_time, offset, compact (default True — strips attribute dicts to save context)
|
|
1063
1157
|
**System/error_log params:** level (ERROR, WARNING, INFO, DEBUG)
|
|
1064
1158
|
**Supervisor params:** slug = add-on slug, e.g. "core_mosquitto" (use
|
|
@@ -1078,6 +1172,7 @@ def register_utility_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
|
|
|
1078
1172
|
compact=compact,
|
|
1079
1173
|
level=level,
|
|
1080
1174
|
slug=slug,
|
|
1175
|
+
order=order,
|
|
1081
1176
|
)
|
|
1082
1177
|
|
|
1083
1178
|
@mcp.tool(
|
|
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.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/dashboard_screenshot/__init__.py
RENAMED
|
File without changes
|
{ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/dashboard_screenshot/capture.py
RENAMED
|
File without changes
|
{ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/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.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/resources/skills-vendor/AGENTS.md
RENAMED
|
File without changes
|
{ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/resources/skills-vendor/CLAUDE.md
RENAMED
|
File without changes
|
|
File without changes
|
{ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/resources/skills-vendor/LICENSE
RENAMED
|
File without changes
|
{ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/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
|
{ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/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.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/tools/smart_search/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/tools/smart_search/_config.py
RENAMED
|
File without changes
|
|
File without changes
|
{ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/tools/smart_search/_entities.py
RENAMED
|
File without changes
|
|
File without changes
|
{ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/tools/smart_search/_overview.py
RENAMED
|
File without changes
|
{ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/tools/smart_search/_scenes.py
RENAMED
|
File without changes
|
{ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/tools/smart_search/_scoring.py
RENAMED
|
File without changes
|
{ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/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.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/tools/tools_config_automations.py
RENAMED
|
File without changes
|
{ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/tools/tools_config_dashboards.py
RENAMED
|
File without changes
|
{ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/tools/tools_config_helpers.py
RENAMED
|
File without changes
|
|
File without changes
|
{ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/tools/tools_config_scripts.py
RENAMED
|
File without changes
|
{ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/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.0.dev709 → ha_mcp_dev-7.8.0.dev711}/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.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/tools/validation_middleware.py
RENAMED
|
File without changes
|
|
File without changes
|
{ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp/transforms/categorized_search.py
RENAMED
|
File without changes
|
{ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/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.dev709 → ha_mcp_dev-7.8.0.dev711}/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.dev709 → ha_mcp_dev-7.8.0.dev711}/src/ha_mcp_dev.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{ha_mcp_dev-7.8.0.dev709 → ha_mcp_dev-7.8.0.dev711}/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
|