ha-mcp-dev 7.7.0.dev690__tar.gz → 7.7.0.dev691__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.7.0.dev690/src/ha_mcp_dev.egg-info → ha_mcp_dev-7.7.0.dev691}/PKG-INFO +1 -1
- {ha_mcp_dev-7.7.0.dev690 → ha_mcp_dev-7.7.0.dev691}/pyproject.toml +1 -1
- {ha_mcp_dev-7.7.0.dev690 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/server.py +2 -1
- {ha_mcp_dev-7.7.0.dev690 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/tools/tools_config_dashboards.py +440 -66
- {ha_mcp_dev-7.7.0.dev690 → ha_mcp_dev-7.7.0.dev691/src/ha_mcp_dev.egg-info}/PKG-INFO +1 -1
- {ha_mcp_dev-7.7.0.dev690 → ha_mcp_dev-7.7.0.dev691}/LICENSE +0 -0
- {ha_mcp_dev-7.7.0.dev690 → ha_mcp_dev-7.7.0.dev691}/MANIFEST.in +0 -0
- {ha_mcp_dev-7.7.0.dev690 → ha_mcp_dev-7.7.0.dev691}/README.md +0 -0
- {ha_mcp_dev-7.7.0.dev690 → ha_mcp_dev-7.7.0.dev691}/setup.cfg +0 -0
- {ha_mcp_dev-7.7.0.dev690 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/__init__.py +0 -0
- {ha_mcp_dev-7.7.0.dev690 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/__main__.py +0 -0
- {ha_mcp_dev-7.7.0.dev690 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/_pypi_marker +0 -0
- {ha_mcp_dev-7.7.0.dev690 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/_version.py +0 -0
- {ha_mcp_dev-7.7.0.dev690 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/auth/__init__.py +0 -0
- {ha_mcp_dev-7.7.0.dev690 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/auth/consent_form.py +0 -0
- {ha_mcp_dev-7.7.0.dev690 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/auth/provider.py +0 -0
- {ha_mcp_dev-7.7.0.dev690 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/backup_manager.py +0 -0
- {ha_mcp_dev-7.7.0.dev690 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/client/__init__.py +0 -0
- {ha_mcp_dev-7.7.0.dev690 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/client/rest_client.py +0 -0
- {ha_mcp_dev-7.7.0.dev690 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/client/supervisor_client.py +0 -0
- {ha_mcp_dev-7.7.0.dev690 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/client/websocket_client.py +0 -0
- {ha_mcp_dev-7.7.0.dev690 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/client/websocket_listener.py +0 -0
- {ha_mcp_dev-7.7.0.dev690 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/config.py +0 -0
- {ha_mcp_dev-7.7.0.dev690 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/dashboard_screenshot/__init__.py +0 -0
- {ha_mcp_dev-7.7.0.dev690 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/dashboard_screenshot/capture.py +0 -0
- {ha_mcp_dev-7.7.0.dev690 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/dashboard_screenshot/provision.py +0 -0
- {ha_mcp_dev-7.7.0.dev690 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/errors.py +0 -0
- {ha_mcp_dev-7.7.0.dev690 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/policy/__init__.py +0 -0
- {ha_mcp_dev-7.7.0.dev690 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/policy/approval_queue.py +0 -0
- {ha_mcp_dev-7.7.0.dev690 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/policy/evaluator.py +0 -0
- {ha_mcp_dev-7.7.0.dev690 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/policy/handlers.py +0 -0
- {ha_mcp_dev-7.7.0.dev690 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/policy/middleware.py +0 -0
- {ha_mcp_dev-7.7.0.dev690 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/policy/model.py +0 -0
- {ha_mcp_dev-7.7.0.dev690 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/policy/persistence.py +0 -0
- {ha_mcp_dev-7.7.0.dev690 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/policy/value_sources.py +0 -0
- {ha_mcp_dev-7.7.0.dev690 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/py.typed +0 -0
- {ha_mcp_dev-7.7.0.dev690 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/read_only.py +0 -0
- {ha_mcp_dev-7.7.0.dev690 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/resources/skills-vendor/.claude/settings.json +0 -0
- {ha_mcp_dev-7.7.0.dev690 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/resources/skills-vendor/.claude-plugin/marketplace.json +0 -0
- {ha_mcp_dev-7.7.0.dev690 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/resources/skills-vendor/.claude-plugin/plugin.json +0 -0
- {ha_mcp_dev-7.7.0.dev690 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/resources/skills-vendor/.github/ISSUE_TEMPLATE/skill-rca.md +0 -0
- {ha_mcp_dev-7.7.0.dev690 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/resources/skills-vendor/.github/pull_request_template.md +0 -0
- {ha_mcp_dev-7.7.0.dev690 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/resources/skills-vendor/AGENTS.md +0 -0
- {ha_mcp_dev-7.7.0.dev690 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/resources/skills-vendor/CLAUDE.md +0 -0
- {ha_mcp_dev-7.7.0.dev690 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/resources/skills-vendor/CONTRIBUTING.md +0 -0
- {ha_mcp_dev-7.7.0.dev690 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/resources/skills-vendor/LICENSE +0 -0
- {ha_mcp_dev-7.7.0.dev690 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/resources/skills-vendor/README.md +0 -0
- {ha_mcp_dev-7.7.0.dev690 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/SKILL.md +0 -0
- {ha_mcp_dev-7.7.0.dev690 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/evals/evals.json +0 -0
- {ha_mcp_dev-7.7.0.dev690 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/automation-patterns.md +0 -0
- {ha_mcp_dev-7.7.0.dev690 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-cards.md +0 -0
- {ha_mcp_dev-7.7.0.dev690 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-guide.md +0 -0
- {ha_mcp_dev-7.7.0.dev690 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/device-control.md +0 -0
- {ha_mcp_dev-7.7.0.dev690 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/domain-docs.md +0 -0
- {ha_mcp_dev-7.7.0.dev690 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/examples.yaml +0 -0
- {ha_mcp_dev-7.7.0.dev690 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/helper-selection.md +0 -0
- {ha_mcp_dev-7.7.0.dev690 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/safe-refactoring.md +0 -0
- {ha_mcp_dev-7.7.0.dev690 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/template-guidelines.md +0 -0
- {ha_mcp_dev-7.7.0.dev690 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/yaml-only-integrations.md +0 -0
- {ha_mcp_dev-7.7.0.dev690 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/settings.css +0 -0
- {ha_mcp_dev-7.7.0.dev690 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/settings.js +0 -0
- {ha_mcp_dev-7.7.0.dev690 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/settings_ui.py +0 -0
- {ha_mcp_dev-7.7.0.dev690 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/smoke_test.py +0 -0
- {ha_mcp_dev-7.7.0.dev690 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/stdio_settings_sidecar.py +0 -0
- {ha_mcp_dev-7.7.0.dev690 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/tools/__init__.py +0 -0
- {ha_mcp_dev-7.7.0.dev690 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/tools/auto_backup.py +0 -0
- {ha_mcp_dev-7.7.0.dev690 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/tools/backup.py +0 -0
- {ha_mcp_dev-7.7.0.dev690 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/tools/best_practice_checker.py +0 -0
- {ha_mcp_dev-7.7.0.dev690 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/tools/config_entry_flow.py +0 -0
- {ha_mcp_dev-7.7.0.dev690 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/tools/device_control.py +0 -0
- {ha_mcp_dev-7.7.0.dev690 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/tools/enhanced.py +0 -0
- {ha_mcp_dev-7.7.0.dev690 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/tools/helpers.py +0 -0
- {ha_mcp_dev-7.7.0.dev690 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/tools/reference_validator.py +0 -0
- {ha_mcp_dev-7.7.0.dev690 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/tools/registry.py +0 -0
- {ha_mcp_dev-7.7.0.dev690 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/tools/smart_search/__init__.py +0 -0
- {ha_mcp_dev-7.7.0.dev690 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/tools/smart_search/_base.py +0 -0
- {ha_mcp_dev-7.7.0.dev690 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/tools/smart_search/_config.py +0 -0
- {ha_mcp_dev-7.7.0.dev690 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/tools/smart_search/_deep.py +0 -0
- {ha_mcp_dev-7.7.0.dev690 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/tools/smart_search/_entities.py +0 -0
- {ha_mcp_dev-7.7.0.dev690 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/tools/smart_search/_fetch.py +0 -0
- {ha_mcp_dev-7.7.0.dev690 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/tools/smart_search/_overview.py +0 -0
- {ha_mcp_dev-7.7.0.dev690 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/tools/smart_search/_scenes.py +0 -0
- {ha_mcp_dev-7.7.0.dev690 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/tools/smart_search/_scoring.py +0 -0
- {ha_mcp_dev-7.7.0.dev690 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/tools/tools_addons.py +0 -0
- {ha_mcp_dev-7.7.0.dev690 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/tools/tools_areas.py +0 -0
- {ha_mcp_dev-7.7.0.dev690 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/tools/tools_blueprints.py +0 -0
- {ha_mcp_dev-7.7.0.dev690 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/tools/tools_bug_report.py +0 -0
- {ha_mcp_dev-7.7.0.dev690 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/tools/tools_calendar.py +0 -0
- {ha_mcp_dev-7.7.0.dev690 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/tools/tools_camera.py +0 -0
- {ha_mcp_dev-7.7.0.dev690 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/tools/tools_categories.py +0 -0
- {ha_mcp_dev-7.7.0.dev690 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/tools/tools_code.py +0 -0
- {ha_mcp_dev-7.7.0.dev690 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/tools/tools_config_automations.py +0 -0
- {ha_mcp_dev-7.7.0.dev690 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/tools/tools_config_helpers.py +0 -0
- {ha_mcp_dev-7.7.0.dev690 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/tools/tools_config_scenes.py +0 -0
- {ha_mcp_dev-7.7.0.dev690 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/tools/tools_config_scripts.py +0 -0
- {ha_mcp_dev-7.7.0.dev690 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/tools/tools_dashboard_screenshot.py +0 -0
- {ha_mcp_dev-7.7.0.dev690 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/tools/tools_energy.py +0 -0
- {ha_mcp_dev-7.7.0.dev690 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/tools/tools_entities.py +0 -0
- {ha_mcp_dev-7.7.0.dev690 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/tools/tools_filesystem.py +0 -0
- {ha_mcp_dev-7.7.0.dev690 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/tools/tools_groups.py +0 -0
- {ha_mcp_dev-7.7.0.dev690 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/tools/tools_hacs.py +0 -0
- {ha_mcp_dev-7.7.0.dev690 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/tools/tools_history.py +0 -0
- {ha_mcp_dev-7.7.0.dev690 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/tools/tools_integrations.py +0 -0
- {ha_mcp_dev-7.7.0.dev690 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/tools/tools_labels.py +0 -0
- {ha_mcp_dev-7.7.0.dev690 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/tools/tools_mcp_component.py +0 -0
- {ha_mcp_dev-7.7.0.dev690 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/tools/tools_registry.py +0 -0
- {ha_mcp_dev-7.7.0.dev690 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/tools/tools_resources.py +0 -0
- {ha_mcp_dev-7.7.0.dev690 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/tools/tools_search.py +0 -0
- {ha_mcp_dev-7.7.0.dev690 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/tools/tools_service.py +0 -0
- {ha_mcp_dev-7.7.0.dev690 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/tools/tools_services.py +0 -0
- {ha_mcp_dev-7.7.0.dev690 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/tools/tools_system.py +0 -0
- {ha_mcp_dev-7.7.0.dev690 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/tools/tools_themes.py +0 -0
- {ha_mcp_dev-7.7.0.dev690 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/tools/tools_todo.py +0 -0
- {ha_mcp_dev-7.7.0.dev690 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/tools/tools_traces.py +0 -0
- {ha_mcp_dev-7.7.0.dev690 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/tools/tools_updates.py +0 -0
- {ha_mcp_dev-7.7.0.dev690 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/tools/tools_utility.py +0 -0
- {ha_mcp_dev-7.7.0.dev690 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/tools/tools_voice_assistant.py +0 -0
- {ha_mcp_dev-7.7.0.dev690 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/tools/tools_yaml_config.py +0 -0
- {ha_mcp_dev-7.7.0.dev690 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/tools/tools_zones.py +0 -0
- {ha_mcp_dev-7.7.0.dev690 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/tools/util_helpers.py +0 -0
- {ha_mcp_dev-7.7.0.dev690 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/tools/validation_middleware.py +0 -0
- {ha_mcp_dev-7.7.0.dev690 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/transforms/__init__.py +0 -0
- {ha_mcp_dev-7.7.0.dev690 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/transforms/categorized_search.py +0 -0
- {ha_mcp_dev-7.7.0.dev690 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/transforms/lite_docstrings.py +0 -0
- {ha_mcp_dev-7.7.0.dev690 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/utils/__init__.py +0 -0
- {ha_mcp_dev-7.7.0.dev690 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/utils/config_hash.py +0 -0
- {ha_mcp_dev-7.7.0.dev690 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/utils/data_paths.py +0 -0
- {ha_mcp_dev-7.7.0.dev690 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/utils/domain_handlers.py +0 -0
- {ha_mcp_dev-7.7.0.dev690 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/utils/fuzzy_search.py +0 -0
- {ha_mcp_dev-7.7.0.dev690 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/utils/kill_signal_diagnostics.py +0 -0
- {ha_mcp_dev-7.7.0.dev690 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/utils/operation_manager.py +0 -0
- {ha_mcp_dev-7.7.0.dev690 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/utils/python_sandbox.py +0 -0
- {ha_mcp_dev-7.7.0.dev690 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/utils/skill_loader.py +0 -0
- {ha_mcp_dev-7.7.0.dev690 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/utils/usage_logger.py +0 -0
- {ha_mcp_dev-7.7.0.dev690 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp_dev.egg-info/SOURCES.txt +0 -0
- {ha_mcp_dev-7.7.0.dev690 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp_dev.egg-info/dependency_links.txt +0 -0
- {ha_mcp_dev-7.7.0.dev690 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp_dev.egg-info/entry_points.txt +0 -0
- {ha_mcp_dev-7.7.0.dev690 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp_dev.egg-info/requires.txt +0 -0
- {ha_mcp_dev-7.7.0.dev690 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp_dev.egg-info/top_level.txt +0 -0
- {ha_mcp_dev-7.7.0.dev690 → ha_mcp_dev-7.7.0.dev691}/tests/__init__.py +0 -0
- {ha_mcp_dev-7.7.0.dev690 → ha_mcp_dev-7.7.0.dev691}/tests/test_constants.py +0 -0
- {ha_mcp_dev-7.7.0.dev690 → ha_mcp_dev-7.7.0.dev691}/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.7.0.
|
|
7
|
+
version = "7.7.0.dev691"
|
|
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"
|
|
@@ -662,7 +662,8 @@ class HomeAssistantSmartMCPServer(EnhancedToolsMixin):
|
|
|
662
662
|
"Three modes: (1) list — `list_only=True` returns all "
|
|
663
663
|
"storage-mode dashboards with metadata. (2) search — pass "
|
|
664
664
|
"any of `entity_id`, `card_type`, `heading` to find cards "
|
|
665
|
-
"(
|
|
665
|
+
"(including nested ones, with a `python_path`) inside a "
|
|
666
|
+
"specific dashboard; the "
|
|
666
667
|
"result includes a `config_hash` you can pair with "
|
|
667
668
|
"ha_config_set_dashboard(python_transform=...) to edit "
|
|
668
669
|
"matched cards surgically. (3) get — no search params "
|
{ha_mcp_dev-7.7.0.dev690 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/tools/tools_config_dashboards.py
RENAMED
|
@@ -183,18 +183,319 @@ def _badge_matches(badge: Any, entity_id: str) -> bool:
|
|
|
183
183
|
return entity_id == badge_entity
|
|
184
184
|
|
|
185
185
|
|
|
186
|
+
# Keys under which a card nests other cards, by descent rule (issue #1599):
|
|
187
|
+
# - ``cards`` (list): vertical/horizontal-stack, grid, and any custom wrapper
|
|
188
|
+
# following the stack convention.
|
|
189
|
+
# - ``card`` (dict): conditional and wrapper cards such as
|
|
190
|
+
# ``custom:auto-entities``.
|
|
191
|
+
# - ``custom_fields`` (dict of field-configs): ``custom:button-card`` embeds
|
|
192
|
+
# sub-cards under ``custom_fields.<name>.card`` (a very common pattern that
|
|
193
|
+
# wraps an entire view in one button-card). Each field-config is descended
|
|
194
|
+
# as a node, so its own ``card`` / ``cards`` are picked up by the recursion.
|
|
195
|
+
# - ``states`` (name->card map): ``custom:state-switch`` swaps a whole card per
|
|
196
|
+
# source state. Each value is itself a card, descended directly as a node.
|
|
197
|
+
# Picture-elements ``elements`` is deliberately NOT traversed: it is not one of
|
|
198
|
+
# the descent keys above, so a node carrying it is disclosed at the response
|
|
199
|
+
# boundary instead of being walked (see ``_UNTRAVERSED_NESTED_KEYS`` and the
|
|
200
|
+
# find-card warnings). A blanket "descend every dict with a ``type``" walk is
|
|
201
|
+
# intentionally avoided: tile ``features`` and view ``conditions`` also carry
|
|
202
|
+
# ``type`` and would false-match as cards.
|
|
203
|
+
_NESTED_CARDS_KEY = "cards"
|
|
204
|
+
_NESTED_CARD_KEY = "card"
|
|
205
|
+
_NESTED_CUSTOM_FIELDS_KEY = "custom_fields"
|
|
206
|
+
_NESTED_STATES_KEY = "states"
|
|
207
|
+
# Child-bearing keys recognised but deliberately NOT traversed. A walked card
|
|
208
|
+
# carrying one of these (with a truthy value) cannot be fully covered, so it is
|
|
209
|
+
# its *presence* — not the absence of matches — that the response discloses
|
|
210
|
+
# (issue #1599: disclose by presence, not by absence-inference). picture-elements
|
|
211
|
+
# ``elements`` is the canonical case.
|
|
212
|
+
_UNTRAVERSED_NESTED_KEYS = ("elements",)
|
|
213
|
+
# Defensive bound against pathological/malformed configs. Real dashboards nest
|
|
214
|
+
# only a handful of levels; this guards recursion depth far above any real use.
|
|
215
|
+
_MAX_CARD_DEPTH = 50
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def _py_key(name: str) -> str:
|
|
219
|
+
"""Render a mapping key as a Python subscript segment (``['name']``).
|
|
220
|
+
|
|
221
|
+
``repr`` quotes and escapes the key, so a name containing a quote (e.g.
|
|
222
|
+
``o'brien``) yields a valid literal; a raw ``['{name}']`` interpolation would
|
|
223
|
+
splice an unterminated string into ``python_transform``.
|
|
224
|
+
"""
|
|
225
|
+
return f"[{name!r}]"
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def _jq_key(name: str) -> str:
|
|
229
|
+
"""Render a mapping key as a jq path segment.
|
|
230
|
+
|
|
231
|
+
Identifier-safe keys use dot notation (``.name``); any other key (a dot, a
|
|
232
|
+
space, a quote) is emitted as a bracketed JSON string (``["weird.key"]``) so
|
|
233
|
+
jq does not read an embedded dot as further nesting.
|
|
234
|
+
"""
|
|
235
|
+
if re.fullmatch(r"[A-Za-z_][A-Za-z0-9_]*", name):
|
|
236
|
+
return f".{name}"
|
|
237
|
+
return f"[{json.dumps(name)}]"
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def _log_non_str_key(container_key: str, name: object, jq_prefix: str) -> None:
|
|
241
|
+
"""Breadcrumb a non-string mapping key under a card-bearing container.
|
|
242
|
+
|
|
243
|
+
Dashboard config arrives as JSON, so keys are normally strings; a non-string
|
|
244
|
+
key (from a corrupted or hand-edited config) cannot form a valid path, so the
|
|
245
|
+
entry is skipped rather than crashing the walk via ``_jq_key`` / ``_py_key``.
|
|
246
|
+
"""
|
|
247
|
+
logger.debug(
|
|
248
|
+
"Card-search skipping non-string %s key at %s (%r, %s)",
|
|
249
|
+
container_key,
|
|
250
|
+
jq_prefix,
|
|
251
|
+
name,
|
|
252
|
+
type(name).__name__,
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def _walk_card(
|
|
257
|
+
card: Any,
|
|
258
|
+
entity_id: str | None,
|
|
259
|
+
card_type: str | None,
|
|
260
|
+
heading: str | None,
|
|
261
|
+
*,
|
|
262
|
+
jq_prefix: str,
|
|
263
|
+
python_prefix: str,
|
|
264
|
+
view_index: int,
|
|
265
|
+
section_index: int | None,
|
|
266
|
+
card_index: int | None,
|
|
267
|
+
depth: int = 0,
|
|
268
|
+
truncation: list[str] | None = None,
|
|
269
|
+
uncovered: list[str] | None = None,
|
|
270
|
+
) -> list[dict[str, Any]]:
|
|
271
|
+
"""Return matches for ``card`` and every card nested beneath it.
|
|
272
|
+
|
|
273
|
+
Descends ``cards`` (list), ``card`` (dict), each ``custom_fields`` value, and
|
|
274
|
+
each ``states`` value (custom:state-switch), for nested as well as top-level
|
|
275
|
+
cards, up to ``_MAX_CARD_DEPTH``.
|
|
276
|
+
|
|
277
|
+
``jq_prefix`` / ``python_prefix`` locate ``card`` itself — the former in jq
|
|
278
|
+
dot-notation, the latter as a Python subscript chain usable (appended after
|
|
279
|
+
``config``) directly in ``ha_config_set_dashboard(python_transform=...)``.
|
|
280
|
+
Nested descendants extend both prefixes per level, so the path strings are
|
|
281
|
+
the authoritative locator for nested cards (the flat ``view_index`` /
|
|
282
|
+
``section_index`` / ``card_index`` identify the top-level container only and
|
|
283
|
+
are carried unchanged into nested matches for back-compat).
|
|
284
|
+
|
|
285
|
+
Only a dict carrying a ``type`` key is treated as a card; this keeps non-card
|
|
286
|
+
dicts reached under these keys (action targets, style blocks, entity rows)
|
|
287
|
+
from matching. If ``truncation`` is provided, the prefix of any subtree
|
|
288
|
+
skipped at the depth bound is appended to it. If ``uncovered`` is provided,
|
|
289
|
+
the path of any walked card carrying a non-traversed child-bearing key (see
|
|
290
|
+
``_UNTRAVERSED_NESTED_KEYS``) is appended to it, so the caller can disclose
|
|
291
|
+
the incompleteness regardless of whether the search matched anything.
|
|
292
|
+
"""
|
|
293
|
+
matches: list[dict[str, Any]] = []
|
|
294
|
+
if not isinstance(card, dict):
|
|
295
|
+
# Structurally-present but malformed slot (e.g. a string where a card
|
|
296
|
+
# dict is expected): skip, but breadcrumb so it is not a silent drop.
|
|
297
|
+
if card is not None:
|
|
298
|
+
logger.debug(
|
|
299
|
+
"Card-search skipping non-dict node at %s (%s)",
|
|
300
|
+
jq_prefix,
|
|
301
|
+
type(card).__name__,
|
|
302
|
+
)
|
|
303
|
+
return matches
|
|
304
|
+
if depth > _MAX_CARD_DEPTH:
|
|
305
|
+
# Stop, but make the truncation visible rather than silently dropping
|
|
306
|
+
# any cards nested below this point. Only reachable on pathological or
|
|
307
|
+
# malformed configs (real dashboards nest a handful of levels).
|
|
308
|
+
logger.warning(
|
|
309
|
+
"Card-search depth bound (%d) exceeded at %s; not descending further",
|
|
310
|
+
_MAX_CARD_DEPTH,
|
|
311
|
+
jq_prefix,
|
|
312
|
+
)
|
|
313
|
+
if truncation is not None:
|
|
314
|
+
truncation.append(jq_prefix)
|
|
315
|
+
return matches
|
|
316
|
+
|
|
317
|
+
if "type" in card:
|
|
318
|
+
if _card_matches(card, entity_id, card_type, heading):
|
|
319
|
+
matches.append(
|
|
320
|
+
{
|
|
321
|
+
"view_index": view_index,
|
|
322
|
+
"section_index": section_index,
|
|
323
|
+
"card_index": card_index,
|
|
324
|
+
"jq_path": jq_prefix,
|
|
325
|
+
"python_path": python_prefix,
|
|
326
|
+
"card_type": card.get("type"),
|
|
327
|
+
"card_config": card,
|
|
328
|
+
}
|
|
329
|
+
)
|
|
330
|
+
# Disclose un-coverable nesting by presence during the walk, not by the
|
|
331
|
+
# absence of matches: a card that carries e.g. picture-elements
|
|
332
|
+
# ``elements`` hides content this search cannot reach whether or not it
|
|
333
|
+
# (or anything else) matched.
|
|
334
|
+
if uncovered is not None:
|
|
335
|
+
for key in _UNTRAVERSED_NESTED_KEYS:
|
|
336
|
+
if card.get(key):
|
|
337
|
+
uncovered.append(f"{jq_prefix}.{key}")
|
|
338
|
+
break
|
|
339
|
+
|
|
340
|
+
nested_list = card.get(_NESTED_CARDS_KEY)
|
|
341
|
+
if isinstance(nested_list, list):
|
|
342
|
+
for i, child in enumerate(nested_list):
|
|
343
|
+
matches.extend(
|
|
344
|
+
_walk_card(
|
|
345
|
+
child,
|
|
346
|
+
entity_id,
|
|
347
|
+
card_type,
|
|
348
|
+
heading,
|
|
349
|
+
jq_prefix=f"{jq_prefix}.{_NESTED_CARDS_KEY}[{i}]",
|
|
350
|
+
python_prefix=f"{python_prefix}['{_NESTED_CARDS_KEY}'][{i}]",
|
|
351
|
+
view_index=view_index,
|
|
352
|
+
section_index=section_index,
|
|
353
|
+
card_index=card_index,
|
|
354
|
+
depth=depth + 1,
|
|
355
|
+
truncation=truncation,
|
|
356
|
+
uncovered=uncovered,
|
|
357
|
+
)
|
|
358
|
+
)
|
|
359
|
+
elif nested_list is not None:
|
|
360
|
+
# ``cards`` key present but not a list — structurally malformed slot.
|
|
361
|
+
logger.debug(
|
|
362
|
+
"Card-search skipping non-list '%s' under %s (%s)",
|
|
363
|
+
_NESTED_CARDS_KEY,
|
|
364
|
+
jq_prefix,
|
|
365
|
+
type(nested_list).__name__,
|
|
366
|
+
)
|
|
367
|
+
|
|
368
|
+
nested_card = card.get(_NESTED_CARD_KEY)
|
|
369
|
+
if isinstance(nested_card, dict):
|
|
370
|
+
matches.extend(
|
|
371
|
+
_walk_card(
|
|
372
|
+
nested_card,
|
|
373
|
+
entity_id,
|
|
374
|
+
card_type,
|
|
375
|
+
heading,
|
|
376
|
+
jq_prefix=f"{jq_prefix}.{_NESTED_CARD_KEY}",
|
|
377
|
+
python_prefix=f"{python_prefix}['{_NESTED_CARD_KEY}']",
|
|
378
|
+
view_index=view_index,
|
|
379
|
+
section_index=section_index,
|
|
380
|
+
card_index=card_index,
|
|
381
|
+
depth=depth + 1,
|
|
382
|
+
truncation=truncation,
|
|
383
|
+
uncovered=uncovered,
|
|
384
|
+
)
|
|
385
|
+
)
|
|
386
|
+
elif nested_card is not None:
|
|
387
|
+
# ``card`` key present but not a dict — structurally malformed slot.
|
|
388
|
+
logger.debug(
|
|
389
|
+
"Card-search skipping non-dict '%s' under %s (%s)",
|
|
390
|
+
_NESTED_CARD_KEY,
|
|
391
|
+
jq_prefix,
|
|
392
|
+
type(nested_card).__name__,
|
|
393
|
+
)
|
|
394
|
+
|
|
395
|
+
# custom:button-card and similar embed sub-cards under custom_fields.<name>.
|
|
396
|
+
# Descend each field-config as a node; its own card/cards (and the type gate)
|
|
397
|
+
# are handled by the recursion, so a field that is not itself a card
|
|
398
|
+
# contributes nothing but is still traversed for nested cards. Keys are
|
|
399
|
+
# rendered quote/dot-safe so a field name like ``o'brien`` yields a usable
|
|
400
|
+
# python_path/jq_path (issue #1599: handle a quote/dot in the field name).
|
|
401
|
+
custom_fields = card.get(_NESTED_CUSTOM_FIELDS_KEY)
|
|
402
|
+
if isinstance(custom_fields, dict):
|
|
403
|
+
for name, field in custom_fields.items():
|
|
404
|
+
if not isinstance(name, str):
|
|
405
|
+
_log_non_str_key(_NESTED_CUSTOM_FIELDS_KEY, name, jq_prefix)
|
|
406
|
+
continue
|
|
407
|
+
matches.extend(
|
|
408
|
+
_walk_card(
|
|
409
|
+
field,
|
|
410
|
+
entity_id,
|
|
411
|
+
card_type,
|
|
412
|
+
heading,
|
|
413
|
+
jq_prefix=f"{jq_prefix}.{_NESTED_CUSTOM_FIELDS_KEY}{_jq_key(name)}",
|
|
414
|
+
python_prefix=(
|
|
415
|
+
f"{python_prefix}['{_NESTED_CUSTOM_FIELDS_KEY}']{_py_key(name)}"
|
|
416
|
+
),
|
|
417
|
+
view_index=view_index,
|
|
418
|
+
section_index=section_index,
|
|
419
|
+
card_index=card_index,
|
|
420
|
+
depth=depth + 1,
|
|
421
|
+
truncation=truncation,
|
|
422
|
+
uncovered=uncovered,
|
|
423
|
+
)
|
|
424
|
+
)
|
|
425
|
+
elif custom_fields is not None:
|
|
426
|
+
logger.debug(
|
|
427
|
+
"Card-search skipping non-dict '%s' under %s (%s)",
|
|
428
|
+
_NESTED_CUSTOM_FIELDS_KEY,
|
|
429
|
+
jq_prefix,
|
|
430
|
+
type(custom_fields).__name__,
|
|
431
|
+
)
|
|
432
|
+
|
|
433
|
+
# custom:state-switch swaps a whole card per source state under states.<name>.
|
|
434
|
+
# Each value is itself a card (not a field-config wrapper), descended
|
|
435
|
+
# directly — the same quote/dot-safe key rendering applies for state names
|
|
436
|
+
# like ``on'hold`` (issue #1599: state-switch nests a card per state).
|
|
437
|
+
states = card.get(_NESTED_STATES_KEY)
|
|
438
|
+
if isinstance(states, dict):
|
|
439
|
+
for name, child in states.items():
|
|
440
|
+
if not isinstance(name, str):
|
|
441
|
+
_log_non_str_key(_NESTED_STATES_KEY, name, jq_prefix)
|
|
442
|
+
continue
|
|
443
|
+
matches.extend(
|
|
444
|
+
_walk_card(
|
|
445
|
+
child,
|
|
446
|
+
entity_id,
|
|
447
|
+
card_type,
|
|
448
|
+
heading,
|
|
449
|
+
jq_prefix=f"{jq_prefix}.{_NESTED_STATES_KEY}{_jq_key(name)}",
|
|
450
|
+
python_prefix=(
|
|
451
|
+
f"{python_prefix}['{_NESTED_STATES_KEY}']{_py_key(name)}"
|
|
452
|
+
),
|
|
453
|
+
view_index=view_index,
|
|
454
|
+
section_index=section_index,
|
|
455
|
+
card_index=card_index,
|
|
456
|
+
depth=depth + 1,
|
|
457
|
+
truncation=truncation,
|
|
458
|
+
uncovered=uncovered,
|
|
459
|
+
)
|
|
460
|
+
)
|
|
461
|
+
elif states is not None:
|
|
462
|
+
logger.debug(
|
|
463
|
+
"Card-search skipping non-dict '%s' under %s (%s)",
|
|
464
|
+
_NESTED_STATES_KEY,
|
|
465
|
+
jq_prefix,
|
|
466
|
+
type(states).__name__,
|
|
467
|
+
)
|
|
468
|
+
|
|
469
|
+
return matches
|
|
470
|
+
|
|
471
|
+
|
|
186
472
|
def _find_cards_in_config(
|
|
187
473
|
config: dict[str, Any],
|
|
188
474
|
entity_id: str | None = None,
|
|
189
475
|
card_type: str | None = None,
|
|
190
476
|
heading: str | None = None,
|
|
477
|
+
truncation: list[str] | None = None,
|
|
478
|
+
uncovered: list[str] | None = None,
|
|
191
479
|
) -> list[dict[str, Any]]:
|
|
192
480
|
"""
|
|
193
481
|
Find cards, badges, and header cards in a dashboard config matching the search criteria.
|
|
194
482
|
|
|
195
483
|
Returns a list of matches with location info and card/badge/header config.
|
|
196
484
|
Searches cards (in sections and flat views), view-level badges, and
|
|
197
|
-
sections-view header cards (views[n].header.card).
|
|
485
|
+
sections-view header cards (views[n].header.card). Card search recurses into
|
|
486
|
+
nested containers (``cards`` lists in stacks/grids, ``card`` dicts in
|
|
487
|
+
conditional/wrapper cards, ``custom_fields`` sub-cards in button-card, and
|
|
488
|
+
``states`` sub-cards in custom:state-switch), so a nested card is found like
|
|
489
|
+
a top-level one (issue #1599) — up to a depth bound.
|
|
490
|
+
|
|
491
|
+
Each match carries both ``jq_path`` (jq dot-notation) and ``python_path``
|
|
492
|
+
(a Python subscript chain appended after ``config`` for
|
|
493
|
+
``ha_config_set_dashboard(python_transform)``); these locate nested as well
|
|
494
|
+
as top-level cards. The flat ``*_index`` fields identify the top-level
|
|
495
|
+
container only. If ``truncation`` is provided, the prefixes of any subtrees
|
|
496
|
+
skipped at the depth bound are appended to it. If ``uncovered`` is provided,
|
|
497
|
+
the paths of any walked cards carrying a non-traversed child-bearing key
|
|
498
|
+
(e.g. picture-elements ``elements``) are appended to it.
|
|
198
499
|
"""
|
|
199
500
|
matches: list[dict[str, Any]] = []
|
|
200
501
|
|
|
@@ -215,38 +516,47 @@ def _find_cards_in_config(
|
|
|
215
516
|
badges = view.get("badges", [])
|
|
216
517
|
for badge_idx, badge in enumerate(badges):
|
|
217
518
|
if _badge_matches(badge, entity_id):
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
519
|
+
is_dict_badge = isinstance(badge, dict)
|
|
520
|
+
badge_config = badge if is_dict_badge else {"entity": badge}
|
|
521
|
+
badge_match: dict[str, Any] = {
|
|
522
|
+
"view_index": view_idx,
|
|
523
|
+
"section_index": None,
|
|
524
|
+
"card_index": None,
|
|
525
|
+
"badge_index": badge_idx,
|
|
526
|
+
"jq_path": f".views[{view_idx}].badges[{badge_idx}]",
|
|
527
|
+
"card_type": "badge",
|
|
528
|
+
"card_config": badge_config,
|
|
529
|
+
}
|
|
530
|
+
# A bare-string badge (the common form) is not subscript-
|
|
531
|
+
# assignable, so a python_path spliced into python_transform
|
|
532
|
+
# would raise TypeError. Only advertise python_path for dict
|
|
533
|
+
# badges; string badges must be converted to dict form first.
|
|
534
|
+
if is_dict_badge:
|
|
535
|
+
badge_match["python_path"] = (
|
|
536
|
+
f"['views'][{view_idx}]['badges'][{badge_idx}]"
|
|
537
|
+
)
|
|
538
|
+
matches.append(badge_match)
|
|
232
539
|
|
|
233
540
|
# Search sections-view header card (views[n].header.card)
|
|
234
541
|
# The header accepts a card (typically Markdown) that can contain entity refs
|
|
235
542
|
header = view.get("header", {})
|
|
236
543
|
if isinstance(header, dict):
|
|
237
544
|
header_card = header.get("card")
|
|
238
|
-
if isinstance(header_card, dict)
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
"
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
545
|
+
if isinstance(header_card, dict):
|
|
546
|
+
matches.extend(
|
|
547
|
+
_walk_card(
|
|
548
|
+
header_card,
|
|
549
|
+
entity_id,
|
|
550
|
+
card_type,
|
|
551
|
+
heading,
|
|
552
|
+
jq_prefix=f".views[{view_idx}].header.card",
|
|
553
|
+
python_prefix=f"['views'][{view_idx}]['header']['card']",
|
|
554
|
+
view_index=view_idx,
|
|
555
|
+
section_index=None,
|
|
556
|
+
card_index=None,
|
|
557
|
+
truncation=truncation,
|
|
558
|
+
uncovered=uncovered,
|
|
559
|
+
)
|
|
250
560
|
)
|
|
251
561
|
|
|
252
562
|
view_type = view.get("type", "masonry")
|
|
@@ -259,36 +569,40 @@ def _find_cards_in_config(
|
|
|
259
569
|
continue
|
|
260
570
|
cards = section.get("cards", [])
|
|
261
571
|
for card_idx, card in enumerate(cards):
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
572
|
+
matches.extend(
|
|
573
|
+
_walk_card(
|
|
574
|
+
card,
|
|
575
|
+
entity_id,
|
|
576
|
+
card_type,
|
|
577
|
+
heading,
|
|
578
|
+
jq_prefix=f".views[{view_idx}].sections[{section_idx}].cards[{card_idx}]",
|
|
579
|
+
python_prefix=f"['views'][{view_idx}]['sections'][{section_idx}]['cards'][{card_idx}]",
|
|
580
|
+
view_index=view_idx,
|
|
581
|
+
section_index=section_idx,
|
|
582
|
+
card_index=card_idx,
|
|
583
|
+
truncation=truncation,
|
|
584
|
+
uncovered=uncovered,
|
|
274
585
|
)
|
|
586
|
+
)
|
|
275
587
|
else:
|
|
276
588
|
# Flat view (masonry, panel, sidebar)
|
|
277
589
|
cards = view.get("cards", [])
|
|
278
590
|
for card_idx, card in enumerate(cards):
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
591
|
+
matches.extend(
|
|
592
|
+
_walk_card(
|
|
593
|
+
card,
|
|
594
|
+
entity_id,
|
|
595
|
+
card_type,
|
|
596
|
+
heading,
|
|
597
|
+
jq_prefix=f".views[{view_idx}].cards[{card_idx}]",
|
|
598
|
+
python_prefix=f"['views'][{view_idx}]['cards'][{card_idx}]",
|
|
599
|
+
view_index=view_idx,
|
|
600
|
+
section_index=None,
|
|
601
|
+
card_index=card_idx,
|
|
602
|
+
truncation=truncation,
|
|
603
|
+
uncovered=uncovered,
|
|
291
604
|
)
|
|
605
|
+
)
|
|
292
606
|
|
|
293
607
|
return matches
|
|
294
608
|
|
|
@@ -687,9 +1001,12 @@ def register_config_dashboard_tools(mcp: Any, client: Any, **kwargs: Any) -> Non
|
|
|
687
1001
|
bool,
|
|
688
1002
|
Field(
|
|
689
1003
|
description="In search mode: include each matched card's own configuration "
|
|
690
|
-
"object in results (increases output size).
|
|
691
|
-
"
|
|
692
|
-
"
|
|
1004
|
+
"object in results (increases output size). Note that a matched container "
|
|
1005
|
+
"card's config contains its descendants, which are themselves separate "
|
|
1006
|
+
"matches with their own config, so deeply-nested stacks multiply the "
|
|
1007
|
+
"payload — keep the default (False) unless you need the bodies. Does not "
|
|
1008
|
+
"affect whether the full dashboard config is returned — search mode always "
|
|
1009
|
+
"returns matches only, not the full dashboard. Ignored outside search mode."
|
|
693
1010
|
),
|
|
694
1011
|
] = False,
|
|
695
1012
|
include_screenshot: Annotated[
|
|
@@ -716,9 +1033,20 @@ def register_config_dashboard_tools(mcp: Any, client: Any, **kwargs: Any) -> Non
|
|
|
716
1033
|
Lists all storage-mode dashboards with metadata (url_path, title, icon).
|
|
717
1034
|
|
|
718
1035
|
MODE 2 — Search: any of entity_id / card_type / heading provided
|
|
719
|
-
Finds cards, badges, and header cards matching the criteria
|
|
720
|
-
|
|
1036
|
+
Finds cards, badges, and header cards matching the criteria, including
|
|
1037
|
+
cards nested inside stacks, grids, conditional cards, button-card
|
|
1038
|
+
custom_fields, and state-switch states. Each match carries a
|
|
1039
|
+
python_path and a jq_path that locate the card for nested as well as
|
|
1040
|
+
top-level cards. The python_path is a Python subscript chain to be
|
|
1041
|
+
appended after `config` — e.g.
|
|
1042
|
+
python_transform=f'config{m["python_path"]}["icon"] = "mdi:x"' (it is
|
|
1043
|
+
NOT valid on its own without the `config` prefix). jq_path is the same
|
|
1044
|
+
location in jq dot-notation.
|
|
721
1045
|
Multiple criteria are AND-ed. Always fetches fresh config (force=True).
|
|
1046
|
+
Search covers cards/card/custom_fields/states containers up to a depth
|
|
1047
|
+
bound; if the dashboard carries a non-traversed child-bearing shape
|
|
1048
|
+
(e.g. picture-elements `elements`), the result carries a `warnings`
|
|
1049
|
+
entry naming where, so its hidden content is not mistaken for absent.
|
|
722
1050
|
Strategy dashboards are not searchable (no explicit cards).
|
|
723
1051
|
|
|
724
1052
|
MODE 3 — Get: Active when list_only=False and no search parameters are provided.
|
|
@@ -742,7 +1070,7 @@ def register_config_dashboard_tools(mcp: Any, client: Any, **kwargs: Any) -> Non
|
|
|
742
1070
|
2. ha_config_set_dashboard(
|
|
743
1071
|
url_path="my-dash",
|
|
744
1072
|
config_hash=find["config_hash"],
|
|
745
|
-
python_transform=f'config{find["matches"][0]["
|
|
1073
|
+
python_transform=f'config{find["matches"][0]["python_path"]}["icon"] = "mdi:lamp"'
|
|
746
1074
|
)
|
|
747
1075
|
|
|
748
1076
|
Note: YAML-mode dashboards (defined in configuration.yaml) are not included in list.
|
|
@@ -859,7 +1187,16 @@ def register_config_dashboard_tools(mcp: Any, client: Any, **kwargs: Any) -> Non
|
|
|
859
1187
|
)
|
|
860
1188
|
)
|
|
861
1189
|
|
|
862
|
-
|
|
1190
|
+
truncation: list[str] = []
|
|
1191
|
+
uncovered: list[str] = []
|
|
1192
|
+
matches = _find_cards_in_config(
|
|
1193
|
+
config,
|
|
1194
|
+
entity_id,
|
|
1195
|
+
card_type,
|
|
1196
|
+
heading,
|
|
1197
|
+
truncation=truncation,
|
|
1198
|
+
uncovered=uncovered,
|
|
1199
|
+
)
|
|
863
1200
|
|
|
864
1201
|
if not include_config:
|
|
865
1202
|
for match in matches:
|
|
@@ -867,6 +1204,46 @@ def register_config_dashboard_tools(mcp: Any, client: Any, **kwargs: Any) -> Non
|
|
|
867
1204
|
|
|
868
1205
|
config_hash: str | None = compute_config_hash(config)
|
|
869
1206
|
|
|
1207
|
+
# Warn-don't-truncate (AGENTS.md Return Values): the walker covers
|
|
1208
|
+
# cards / card / custom_fields / states containers and stops at
|
|
1209
|
+
# the depth bound, so neither a depth-truncated search nor a
|
|
1210
|
+
# search over a dashboard carrying a non-traversed child-bearing
|
|
1211
|
+
# shape may read as an authoritative complete result. Disclosure
|
|
1212
|
+
# keys off the *presence* of such a shape (collected during the
|
|
1213
|
+
# walk), not off a 0-match — a matching un-walkable container no
|
|
1214
|
+
# longer suppresses the warning, and a true negative over a
|
|
1215
|
+
# fully-coverable dashboard no longer cries wolf.
|
|
1216
|
+
warnings: list[str] = []
|
|
1217
|
+
if truncation:
|
|
1218
|
+
warnings.append(
|
|
1219
|
+
f"Search stopped at the nesting depth bound "
|
|
1220
|
+
f"(_MAX_CARD_DEPTH={_MAX_CARD_DEPTH}) in "
|
|
1221
|
+
f"{len(truncation)} place(s); cards nested deeper were not "
|
|
1222
|
+
"searched, so results may be incomplete."
|
|
1223
|
+
)
|
|
1224
|
+
if uncovered:
|
|
1225
|
+
locations = ", ".join(sorted(set(uncovered)))
|
|
1226
|
+
warnings.append(
|
|
1227
|
+
"Cards nesting content under keys this search does not "
|
|
1228
|
+
"traverse (e.g. picture-elements 'elements') are present at: "
|
|
1229
|
+
f"{locations}. That nested content is not searched; fetch the "
|
|
1230
|
+
"full config (ha_config_get_dashboard without search params) "
|
|
1231
|
+
"to inspect those."
|
|
1232
|
+
)
|
|
1233
|
+
|
|
1234
|
+
if matches:
|
|
1235
|
+
hint = (
|
|
1236
|
+
"Use python_path with "
|
|
1237
|
+
"ha_config_set_dashboard(python_transform=...) for targeted "
|
|
1238
|
+
"updates"
|
|
1239
|
+
)
|
|
1240
|
+
else:
|
|
1241
|
+
hint = (
|
|
1242
|
+
"No matches in searched containers. Try other criteria, or "
|
|
1243
|
+
"fetch the full config (no search params) to inspect nesting "
|
|
1244
|
+
"shapes this search does not cover."
|
|
1245
|
+
)
|
|
1246
|
+
|
|
870
1247
|
search_result: dict[str, Any] = {
|
|
871
1248
|
"success": True,
|
|
872
1249
|
"action": "find_card",
|
|
@@ -879,13 +1256,10 @@ def register_config_dashboard_tools(mcp: Any, client: Any, **kwargs: Any) -> Non
|
|
|
879
1256
|
},
|
|
880
1257
|
"matches": matches,
|
|
881
1258
|
"match_count": len(matches),
|
|
882
|
-
"hint":
|
|
883
|
-
"Use jq_path with ha_config_set_dashboard(python_transform=...) "
|
|
884
|
-
"for targeted updates"
|
|
885
|
-
if matches
|
|
886
|
-
else "No matches found. Try broader search criteria."
|
|
887
|
-
),
|
|
1259
|
+
"hint": hint,
|
|
888
1260
|
}
|
|
1261
|
+
if warnings:
|
|
1262
|
+
search_result["warnings"] = warnings
|
|
889
1263
|
if search_resolved_from is not None:
|
|
890
1264
|
search_result["resolved_from"] = search_resolved_from
|
|
891
1265
|
_note_screenshot_ignored(
|
|
@@ -1371,14 +1745,14 @@ def register_config_dashboard_tools(mcp: Any, client: Any, **kwargs: Any) -> Non
|
|
|
1371
1745
|
message, suggestions = format_sandbox_error(e, python_transform)
|
|
1372
1746
|
# A path-shape mismatch (IndexError/KeyError) is almost always
|
|
1373
1747
|
# a hallucinated path; steer the retry toward search mode so
|
|
1374
|
-
# the next transform is built from a verified
|
|
1748
|
+
# the next transform is built from a verified python_path.
|
|
1375
1749
|
if isinstance(e, PythonSandboxExecutionError) and isinstance(
|
|
1376
1750
|
e.__cause__, (IndexError, KeyError)
|
|
1377
1751
|
):
|
|
1378
1752
|
suggestions = [
|
|
1379
1753
|
"Call ha_config_get_dashboard with card_type=..., "
|
|
1380
1754
|
"entity_id=..., or heading=... to get the verified "
|
|
1381
|
-
"
|
|
1755
|
+
"python_path for the target card, then build "
|
|
1382
1756
|
"python_transform from that path",
|
|
1383
1757
|
*suggestions,
|
|
1384
1758
|
]
|
|
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.7.0.dev690 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/dashboard_screenshot/__init__.py
RENAMED
|
File without changes
|
{ha_mcp_dev-7.7.0.dev690 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/dashboard_screenshot/capture.py
RENAMED
|
File without changes
|
{ha_mcp_dev-7.7.0.dev690 → ha_mcp_dev-7.7.0.dev691}/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.7.0.dev690 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/resources/skills-vendor/AGENTS.md
RENAMED
|
File without changes
|
{ha_mcp_dev-7.7.0.dev690 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/resources/skills-vendor/CLAUDE.md
RENAMED
|
File without changes
|
|
File without changes
|
{ha_mcp_dev-7.7.0.dev690 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/resources/skills-vendor/LICENSE
RENAMED
|
File without changes
|
{ha_mcp_dev-7.7.0.dev690 → ha_mcp_dev-7.7.0.dev691}/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.7.0.dev690 → ha_mcp_dev-7.7.0.dev691}/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.7.0.dev690 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/tools/smart_search/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{ha_mcp_dev-7.7.0.dev690 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/tools/smart_search/_config.py
RENAMED
|
File without changes
|
|
File without changes
|
{ha_mcp_dev-7.7.0.dev690 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/tools/smart_search/_entities.py
RENAMED
|
File without changes
|
|
File without changes
|
{ha_mcp_dev-7.7.0.dev690 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/tools/smart_search/_overview.py
RENAMED
|
File without changes
|
{ha_mcp_dev-7.7.0.dev690 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/tools/smart_search/_scenes.py
RENAMED
|
File without changes
|
{ha_mcp_dev-7.7.0.dev690 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/tools/smart_search/_scoring.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.7.0.dev690 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/tools/tools_config_automations.py
RENAMED
|
File without changes
|
{ha_mcp_dev-7.7.0.dev690 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/tools/tools_config_helpers.py
RENAMED
|
File without changes
|
|
File without changes
|
{ha_mcp_dev-7.7.0.dev690 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/tools/tools_config_scripts.py
RENAMED
|
File without changes
|
{ha_mcp_dev-7.7.0.dev690 → ha_mcp_dev-7.7.0.dev691}/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
|
|
File without changes
|
{ha_mcp_dev-7.7.0.dev690 → ha_mcp_dev-7.7.0.dev691}/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.7.0.dev690 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/tools/validation_middleware.py
RENAMED
|
File without changes
|
|
File without changes
|
{ha_mcp_dev-7.7.0.dev690 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp/transforms/categorized_search.py
RENAMED
|
File without changes
|
{ha_mcp_dev-7.7.0.dev690 → ha_mcp_dev-7.7.0.dev691}/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.7.0.dev690 → ha_mcp_dev-7.7.0.dev691}/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.7.0.dev690 → ha_mcp_dev-7.7.0.dev691}/src/ha_mcp_dev.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{ha_mcp_dev-7.7.0.dev690 → ha_mcp_dev-7.7.0.dev691}/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
|