ha-mcp-dev 7.13.0.dev2007__tar.gz → 7.13.0.dev2011__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.13.0.dev2007/src/ha_mcp_dev.egg-info → ha_mcp_dev-7.13.0.dev2011}/PKG-INFO +1 -1
- {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/pyproject.toml +1 -1
- {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/client/rest_client.py +69 -19
- {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/tools/component_api.py +9 -1
- {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/tools/tools_config_automations.py +63 -10
- {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/tools/tools_config_scenes.py +50 -16
- {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/tools/tools_config_scripts.py +64 -16
- {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/tools/tools_dev.py +141 -0
- {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/tools/tools_traces.py +99 -90
- {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/tools/util_helpers.py +3 -2
- {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011/src/ha_mcp_dev.egg-info}/PKG-INFO +1 -1
- {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/LICENSE +0 -0
- {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/MANIFEST.in +0 -0
- {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/README.md +0 -0
- {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/setup.cfg +0 -0
- {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/__init__.py +0 -0
- {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/__main__.py +0 -0
- {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/_pypi_marker +0 -0
- {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/_version.py +0 -0
- {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/auth/__init__.py +0 -0
- {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/auth/consent_form.py +0 -0
- {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/auth/provider.py +0 -0
- {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/backup_manager.py +0 -0
- {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/browser_landing.py +0 -0
- {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/client/__init__.py +0 -0
- {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/client/supervisor_client.py +0 -0
- {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/client/websocket_client.py +0 -0
- {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/client/websocket_listener.py +0 -0
- {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/config.py +0 -0
- {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/dashboard_screenshot/__init__.py +0 -0
- {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/dashboard_screenshot/capture.py +0 -0
- {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/dashboard_screenshot/content.py +0 -0
- {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/dashboard_screenshot/paths.py +0 -0
- {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/dashboard_screenshot/provision.py +0 -0
- {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/dashboard_screenshot/theme_guard.py +0 -0
- {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/errors.py +0 -0
- {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/llm_exposure.py +0 -0
- {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/policy/__init__.py +0 -0
- {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/policy/approval_queue.py +0 -0
- {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/policy/evaluator.py +0 -0
- {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/policy/handlers.py +0 -0
- {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/policy/middleware.py +0 -0
- {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/policy/model.py +0 -0
- {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/policy/persistence.py +0 -0
- {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/policy/value_sources.py +0 -0
- {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/py.typed +0 -0
- {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/read_only.py +0 -0
- {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/resources/skills-vendor/.claude/settings.json +0 -0
- {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/resources/skills-vendor/.claude-plugin/marketplace.json +0 -0
- {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/resources/skills-vendor/.claude-plugin/plugin.json +0 -0
- {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/resources/skills-vendor/.github/ISSUE_TEMPLATE/skill-rca.md +0 -0
- {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/resources/skills-vendor/.github/pull_request_template.md +0 -0
- {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/resources/skills-vendor/AGENTS.md +0 -0
- {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/resources/skills-vendor/CLAUDE.md +0 -0
- {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/resources/skills-vendor/CONTRIBUTING.md +0 -0
- {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/resources/skills-vendor/LICENSE +0 -0
- {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/resources/skills-vendor/README.md +0 -0
- {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/SKILL.md +0 -0
- {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/evals/evals.json +0 -0
- {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/appdaemon.md +0 -0
- {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/automation-patterns.md +0 -0
- {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/blueprint-guide.md +0 -0
- {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-cards.md +0 -0
- {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-guide.md +0 -0
- {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/device-control.md +0 -0
- {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/domain-docs.md +0 -0
- {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/examples.yaml +0 -0
- {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/helper-selection.md +0 -0
- {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/safe-refactoring.md +0 -0
- {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/scenes.md +0 -0
- {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/template-guidelines.md +0 -0
- {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/yaml-only-integrations.md +0 -0
- {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/server.py +0 -0
- {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/settings_ui/__init__.py +0 -0
- {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/settings_ui/_handlers_advanced.py +0 -0
- {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/settings_ui/_handlers_backups.py +0 -0
- {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/settings_ui/_handlers_fs.py +0 -0
- {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/settings_ui/_handlers_server.py +0 -0
- {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/settings_ui/_handlers_theme.py +0 -0
- {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/settings_ui/_handlers_tools.py +0 -0
- {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/settings_ui/_i18n.py +0 -0
- {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/settings_ui/_persistence.py +0 -0
- {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/settings_ui/_supervisor.py +0 -0
- {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/settings_ui/_theme.py +0 -0
- {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/settings_ui/_tools_meta.py +0 -0
- {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/settings_ui/locales/README.md +0 -0
- {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/settings_ui/locales/de.json +0 -0
- {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/settings_ui/locales/en.json +0 -0
- {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/settings_ui/locales/ru.json +0 -0
- {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/settings_ui/settings.css +0 -0
- {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/settings_ui/settings.html +0 -0
- {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/settings_ui/settings.js +0 -0
- {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/smoke_test.py +0 -0
- {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/stdio_settings_sidecar.py +0 -0
- {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/strict_bps.py +0 -0
- {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/tools/__init__.py +0 -0
- {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/tools/auto_backup.py +0 -0
- {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/tools/backup.py +0 -0
- {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/tools/best_practice_checker.py +0 -0
- {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/tools/component_config_reads.py +0 -0
- {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/tools/component_devices.py +0 -0
- {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/tools/component_registries.py +0 -0
- {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/tools/component_registry_lookup.py +0 -0
- {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/tools/config_entry_flow.py +0 -0
- {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/tools/device_control.py +0 -0
- {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/tools/enhanced.py +0 -0
- {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/tools/helpers.py +0 -0
- {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/tools/radio/__init__.py +0 -0
- {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/tools/radio/base.py +0 -0
- {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/tools/radio/matter.py +0 -0
- {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/tools/radio/thread.py +0 -0
- {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/tools/radio/zigbee.py +0 -0
- {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/tools/radio/zwave.py +0 -0
- {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/tools/reference_validator.py +0 -0
- {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/tools/registry.py +0 -0
- {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/tools/smart_search/__init__.py +0 -0
- {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/tools/smart_search/_base.py +0 -0
- {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/tools/smart_search/_config.py +0 -0
- {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/tools/smart_search/_deep.py +0 -0
- {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/tools/smart_search/_entities.py +0 -0
- {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/tools/smart_search/_fetch.py +0 -0
- {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/tools/smart_search/_overview.py +0 -0
- {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/tools/smart_search/_scenes.py +0 -0
- {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/tools/smart_search/_scoring.py +0 -0
- {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/tools/tool_search_hint_middleware.py +0 -0
- {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/tools/tools_addons.py +0 -0
- {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/tools/tools_areas.py +0 -0
- {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/tools/tools_blueprints.py +0 -0
- {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/tools/tools_bug_report.py +0 -0
- {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/tools/tools_calendar.py +0 -0
- {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/tools/tools_camera.py +0 -0
- {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/tools/tools_categories.py +0 -0
- {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/tools/tools_code.py +0 -0
- {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/tools/tools_config_dashboards.py +0 -0
- {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/tools/tools_config_helpers.py +0 -0
- {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/tools/tools_dashboard_screenshot.py +0 -0
- {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/tools/tools_energy.py +0 -0
- {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/tools/tools_entities.py +0 -0
- {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/tools/tools_filesystem.py +0 -0
- {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/tools/tools_groups.py +0 -0
- {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/tools/tools_hacs.py +0 -0
- {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/tools/tools_history.py +0 -0
- {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/tools/tools_integrations.py +0 -0
- {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/tools/tools_labels.py +0 -0
- {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/tools/tools_radio.py +0 -0
- {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/tools/tools_registry.py +0 -0
- {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/tools/tools_resources.py +0 -0
- {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/tools/tools_search.py +0 -0
- {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/tools/tools_service.py +0 -0
- {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/tools/tools_services.py +0 -0
- {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/tools/tools_system.py +0 -0
- {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/tools/tools_themes.py +0 -0
- {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/tools/tools_todo.py +0 -0
- {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/tools/tools_updates.py +0 -0
- {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/tools/tools_utility.py +0 -0
- {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/tools/tools_voice_assistant.py +0 -0
- {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/tools/tools_yaml_config.py +0 -0
- {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/tools/tools_yaml_read.py +0 -0
- {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/tools/tools_zones.py +0 -0
- {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/tools/validation_middleware.py +0 -0
- {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/transforms/__init__.py +0 -0
- {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/transforms/categorized_search.py +0 -0
- {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/transforms/lite_docstrings.py +0 -0
- {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/transport_security.py +0 -0
- {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/update_check.py +0 -0
- {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/utils/__init__.py +0 -0
- {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/utils/config_hash.py +0 -0
- {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/utils/data_paths.py +0 -0
- {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/utils/domain_handlers.py +0 -0
- {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/utils/fuzzy_search.py +0 -0
- {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/utils/kill_signal_diagnostics.py +0 -0
- {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/utils/operation_manager.py +0 -0
- {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/utils/python_sandbox.py +0 -0
- {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/utils/skill_loader.py +0 -0
- {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/utils/usage_logger.py +0 -0
- {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/visibility/__init__.py +0 -0
- {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/visibility/model.py +0 -0
- {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/visibility/persistence.py +0 -0
- {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/visibility/resolver.py +0 -0
- {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp_dev.egg-info/SOURCES.txt +0 -0
- {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp_dev.egg-info/dependency_links.txt +0 -0
- {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp_dev.egg-info/entry_points.txt +0 -0
- {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp_dev.egg-info/requires.txt +0 -0
- {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp_dev.egg-info/top_level.txt +0 -0
- {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/tests/__init__.py +0 -0
- {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/tests/test_constants.py +0 -0
- {ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/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.13.0.
|
|
7
|
+
version = "7.13.0.dev2011"
|
|
8
8
|
description = "Home Assistant MCP Server - Complete control of Home Assistant through MCP"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
# Upper bound widened to <3.15 so the package installs on Home Assistant's
|
|
@@ -975,7 +975,11 @@ class HomeAssistantClient:
|
|
|
975
975
|
raise
|
|
976
976
|
|
|
977
977
|
async def upsert_automation_config(
|
|
978
|
-
self,
|
|
978
|
+
self,
|
|
979
|
+
config: dict[str, Any],
|
|
980
|
+
identifier: str | None = None,
|
|
981
|
+
*,
|
|
982
|
+
_resolved: bool = False,
|
|
979
983
|
) -> dict[str, Any]:
|
|
980
984
|
"""
|
|
981
985
|
Create new automation or update existing one.
|
|
@@ -983,6 +987,12 @@ class HomeAssistantClient:
|
|
|
983
987
|
Args:
|
|
984
988
|
config: Automation configuration dictionary
|
|
985
989
|
identifier: Optional automation entity_id or unique_id (None = create new)
|
|
990
|
+
_resolved: When True, ``identifier`` is already the resolved unique_id
|
|
991
|
+
(the caller ran ``_resolve_automation_id``), so the redundant
|
|
992
|
+
entity_id→unique_id lookup is skipped. Ignored on the create path
|
|
993
|
+
(``identifier is None``). The #1404 config-id mismatch guard below
|
|
994
|
+
is unaffected — it still compares ``config['id']`` to the resolved
|
|
995
|
+
unique_id.
|
|
986
996
|
|
|
987
997
|
Returns:
|
|
988
998
|
Result with automation unique_id and status
|
|
@@ -996,7 +1006,11 @@ class HomeAssistantClient:
|
|
|
996
1006
|
operation = "created"
|
|
997
1007
|
logger.debug(f"Creating new automation with unique_id: {unique_id}")
|
|
998
1008
|
else:
|
|
999
|
-
unique_id =
|
|
1009
|
+
unique_id = (
|
|
1010
|
+
identifier
|
|
1011
|
+
if _resolved
|
|
1012
|
+
else await self._resolve_automation_id(identifier)
|
|
1013
|
+
)
|
|
1000
1014
|
operation = "updated"
|
|
1001
1015
|
logger.debug(f"Updating automation with unique_id: {unique_id}")
|
|
1002
1016
|
|
|
@@ -1592,14 +1606,33 @@ class HomeAssistantClient:
|
|
|
1592
1606
|
raise
|
|
1593
1607
|
|
|
1594
1608
|
async def upsert_script_config(
|
|
1595
|
-
self,
|
|
1609
|
+
self,
|
|
1610
|
+
config: dict[str, Any],
|
|
1611
|
+
script_id: str,
|
|
1612
|
+
*,
|
|
1613
|
+
resolved_id: str | None = None,
|
|
1596
1614
|
) -> dict[str, Any]:
|
|
1597
|
-
"""Create or update Home Assistant script configuration.
|
|
1598
|
-
|
|
1615
|
+
"""Create or update Home Assistant script configuration.
|
|
1616
|
+
|
|
1617
|
+
``resolved_id``, when provided, is the already-resolved storage key used
|
|
1618
|
+
as the write target (the caller ran :meth:`_resolve_script_id`),
|
|
1619
|
+
skipping the redundant entity-registry lookup. ``script_id`` stays the
|
|
1620
|
+
CALLER's identifier and is used only to default a missing ``alias`` — a
|
|
1621
|
+
renamed script (whose storage key differs from the entity slug the
|
|
1622
|
+
caller passed) keeps its caller-facing name rather than resetting the
|
|
1623
|
+
user-visible alias to the stale storage key (#1935).
|
|
1624
|
+
"""
|
|
1625
|
+
write_id = (
|
|
1626
|
+
resolved_id
|
|
1627
|
+
if resolved_id is not None
|
|
1628
|
+
else await self._resolve_script_id(script_id)
|
|
1629
|
+
)
|
|
1599
1630
|
try:
|
|
1600
|
-
endpoint = f"config/script/config/{
|
|
1631
|
+
endpoint = f"config/script/config/{write_id}"
|
|
1601
1632
|
|
|
1602
|
-
#
|
|
1633
|
+
# Default a missing alias from the CALLER's ``script_id`` (not
|
|
1634
|
+
# ``write_id``) so a renamed script's user-visible name is preserved
|
|
1635
|
+
# instead of being reset to the storage key.
|
|
1603
1636
|
if "alias" not in config:
|
|
1604
1637
|
config["alias"] = script_id
|
|
1605
1638
|
|
|
@@ -1614,7 +1647,7 @@ class HomeAssistantClient:
|
|
|
1614
1647
|
|
|
1615
1648
|
return {
|
|
1616
1649
|
"success": True,
|
|
1617
|
-
"script_id":
|
|
1650
|
+
"script_id": write_id,
|
|
1618
1651
|
"result": response.get("result", "ok"),
|
|
1619
1652
|
"operation": "created" if response.get("result") == "ok" else "updated",
|
|
1620
1653
|
}
|
|
@@ -1722,22 +1755,39 @@ class HomeAssistantClient:
|
|
|
1722
1755
|
raise
|
|
1723
1756
|
|
|
1724
1757
|
async def upsert_scene_config(
|
|
1725
|
-
self,
|
|
1758
|
+
self,
|
|
1759
|
+
config: dict[str, Any],
|
|
1760
|
+
scene_id: str,
|
|
1761
|
+
*,
|
|
1762
|
+
resolved_id: str | None = None,
|
|
1726
1763
|
) -> dict[str, Any]:
|
|
1727
1764
|
"""Create or update Home Assistant scene configuration.
|
|
1728
1765
|
|
|
1729
|
-
``
|
|
1730
|
-
|
|
1731
|
-
|
|
1766
|
+
``resolved_id``, when provided, is the already-resolved storage key used
|
|
1767
|
+
as the write target (the caller ran :meth:`resolve_scene_id`), skipping
|
|
1768
|
+
the redundant registry lookup. ``scene_id`` stays the CALLER's identifier
|
|
1769
|
+
and is used only to default a missing ``name`` — a renamed scene (whose
|
|
1770
|
+
storage key differs from the slug the caller passed) keeps its
|
|
1771
|
+
caller-facing name rather than resetting to the stale storage key
|
|
1772
|
+
(#1935, mirrors the script fix).
|
|
1732
1773
|
"""
|
|
1733
|
-
|
|
1774
|
+
write_id = (
|
|
1775
|
+
resolved_id
|
|
1776
|
+
if resolved_id is not None
|
|
1777
|
+
else await self.resolve_scene_id(scene_id)
|
|
1778
|
+
)
|
|
1734
1779
|
try:
|
|
1735
|
-
endpoint = f"config/scene/config/{
|
|
1736
|
-
|
|
1737
|
-
# Default a name when missing
|
|
1738
|
-
#
|
|
1780
|
+
endpoint = f"config/scene/config/{write_id}"
|
|
1781
|
+
|
|
1782
|
+
# Default a name when missing from the CALLER's ``scene_id`` (not
|
|
1783
|
+
# ``write_id``) — mirrors the script upsert behaviour so a bare
|
|
1784
|
+
# config dict is acceptable, without resetting a renamed scene's
|
|
1785
|
+
# name to the storage key. Strip the ``scene.`` prefix: a caller may
|
|
1786
|
+
# pass the entity_id form (``resolve_scene_id`` accepts it), and HA
|
|
1787
|
+
# derives the entity_id from the name slug — persisting the prefixed
|
|
1788
|
+
# form would rename the scene and change its entity_id on a plain update.
|
|
1739
1789
|
if "name" not in config:
|
|
1740
|
-
config["name"] = scene_id
|
|
1790
|
+
config["name"] = scene_id.removeprefix("scene.")
|
|
1741
1791
|
|
|
1742
1792
|
# Validate required field. ``entities`` is a dict keyed by entity_id,
|
|
1743
1793
|
# not a list — distinct from script ``sequence`` and automation ``action``.
|
|
@@ -1757,7 +1807,7 @@ class HomeAssistantClient:
|
|
|
1757
1807
|
# activity should diff before/after via ``ha_config_get_scene``.
|
|
1758
1808
|
return {
|
|
1759
1809
|
"success": True,
|
|
1760
|
-
"scene_id":
|
|
1810
|
+
"scene_id": write_id,
|
|
1761
1811
|
"result": response.get("result", "ok"),
|
|
1762
1812
|
}
|
|
1763
1813
|
except Exception as e:
|
|
@@ -273,9 +273,17 @@ async def get_component_caps(client: Any) -> ComponentCaps | None:
|
|
|
273
273
|
if not base_url or not token:
|
|
274
274
|
# Not a credentialed HA client — no WS connection to negotiate over.
|
|
275
275
|
return None
|
|
276
|
+
# Thread verify_ssl so a verify_ssl=False client keys (and can establish)
|
|
277
|
+
# its own pooled connection rather than failing the probe or colliding with
|
|
278
|
+
# a default-verification entry — otherwise EVERY component capability is
|
|
279
|
+
# silently unreachable on an HTTPS + verify_ssl=False install. ``None`` is
|
|
280
|
+
# the pool's settings default, so a client without the attr is unchanged.
|
|
281
|
+
verify_ssl = getattr(client, "verify_ssl", None)
|
|
276
282
|
|
|
277
283
|
try:
|
|
278
|
-
ws = await get_websocket_client(
|
|
284
|
+
ws = await get_websocket_client(
|
|
285
|
+
url=base_url, token=token, verify_ssl=verify_ssl
|
|
286
|
+
)
|
|
279
287
|
response = await ws.send_command(INFO_COMMAND)
|
|
280
288
|
except HomeAssistantCommandError:
|
|
281
289
|
# Old component (unknown_command) or an info handler bug: the
|
{ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/tools/tools_config_automations.py
RENAMED
|
@@ -801,9 +801,15 @@ class AutomationConfigTools:
|
|
|
801
801
|
# (trigger -> triggers, action -> actions, condition -> conditions).
|
|
802
802
|
config_dict = _normalize_automation_config(config_dict)
|
|
803
803
|
|
|
804
|
-
# Optional hash check for full config updates
|
|
804
|
+
# Optional hash check for full config updates. When it runs it
|
|
805
|
+
# resolves ``identifier`` to the storage key — thread that through so
|
|
806
|
+
# the upsert doesn't re-resolve (issue #1813 Phase 0). Stays None on
|
|
807
|
+
# the no-hash update path (raw identifier resolved once, in upsert).
|
|
808
|
+
resolved_id: str | None = None
|
|
805
809
|
if identifier and config_hash:
|
|
806
|
-
await self._fetch_and_verify_hash(
|
|
810
|
+
_, resolved_id = await self._fetch_and_verify_hash(
|
|
811
|
+
identifier, config_hash, "set"
|
|
812
|
+
)
|
|
807
813
|
|
|
808
814
|
self._validate_required_fields(config_dict, identifier)
|
|
809
815
|
bp_warnings = _check_best_practices(config_dict)
|
|
@@ -820,6 +826,7 @@ class AutomationConfigTools:
|
|
|
820
826
|
validation_meta,
|
|
821
827
|
MandatoryBPS,
|
|
822
828
|
conflict_warnings,
|
|
829
|
+
resolved_id,
|
|
823
830
|
)
|
|
824
831
|
|
|
825
832
|
except ToolError as te:
|
|
@@ -874,6 +881,30 @@ class AutomationConfigTools:
|
|
|
874
881
|
augment_error_dict_with_skill_content(error, bp_warnings)
|
|
875
882
|
raise_tool_error(error)
|
|
876
883
|
|
|
884
|
+
async def _upsert_automation(
|
|
885
|
+
self,
|
|
886
|
+
config: dict[str, Any],
|
|
887
|
+
identifier: str | None,
|
|
888
|
+
resolved_id: str | None,
|
|
889
|
+
) -> dict[str, Any]:
|
|
890
|
+
"""Upsert, threading a pre-resolved unique_id when available.
|
|
891
|
+
|
|
892
|
+
When ``resolved_id`` is set the caller already resolved ``identifier``
|
|
893
|
+
(via ``_fetch_and_verify_hash``); pass it with ``_resolved=True`` so the
|
|
894
|
+
REST client skips the redundant entity_id→unique_id lookup (issue #1813
|
|
895
|
+
Phase 0). Otherwise fall back to the raw ``identifier`` and let the REST
|
|
896
|
+
client resolve — the create path (``identifier is None``) and the
|
|
897
|
+
no-hash update path both land here unchanged.
|
|
898
|
+
"""
|
|
899
|
+
result: dict[str, Any]
|
|
900
|
+
if resolved_id is not None:
|
|
901
|
+
result = await self._client.upsert_automation_config(
|
|
902
|
+
config, resolved_id, _resolved=True
|
|
903
|
+
)
|
|
904
|
+
else:
|
|
905
|
+
result = await self._client.upsert_automation_config(config, identifier)
|
|
906
|
+
return result
|
|
907
|
+
|
|
877
908
|
async def _run_python_transform(
|
|
878
909
|
self,
|
|
879
910
|
identifier: str | None,
|
|
@@ -908,7 +939,7 @@ class AutomationConfigTools:
|
|
|
908
939
|
)
|
|
909
940
|
)
|
|
910
941
|
|
|
911
|
-
current_config = await self._fetch_and_verify_hash(
|
|
942
|
+
current_config, resolved_id = await self._fetch_and_verify_hash(
|
|
912
943
|
identifier, config_hash, "python_transform"
|
|
913
944
|
)
|
|
914
945
|
|
|
@@ -938,8 +969,12 @@ class AutomationConfigTools:
|
|
|
938
969
|
self._validate_required_fields(transformed_config, identifier)
|
|
939
970
|
bp_warnings = _check_best_practices(transformed_config)
|
|
940
971
|
|
|
941
|
-
|
|
942
|
-
|
|
972
|
+
# ``_fetch_and_verify_hash`` already resolved ``identifier`` to the
|
|
973
|
+
# storage key; thread it so the upsert skips the redundant re-resolve
|
|
974
|
+
# (issue #1813 Phase 0). Fall back to the raw identifier if the fetched
|
|
975
|
+
# body carried no ``id`` (not expected for a real automation).
|
|
976
|
+
result = await self._upsert_automation(
|
|
977
|
+
transformed_config, identifier, resolved_id
|
|
943
978
|
)
|
|
944
979
|
for warning in conflict_warnings:
|
|
945
980
|
result.setdefault("warnings", []).append(warning)
|
|
@@ -988,9 +1023,15 @@ class AutomationConfigTools:
|
|
|
988
1023
|
validation_meta: dict[str, Any],
|
|
989
1024
|
MandatoryBPS: bool,
|
|
990
1025
|
conflict_warnings: list[str] | None = None,
|
|
1026
|
+
resolved_id: str | None = None,
|
|
991
1027
|
) -> dict[str, Any]:
|
|
992
|
-
"""Execute config-replacement mode and return the tool response.
|
|
993
|
-
|
|
1028
|
+
"""Execute config-replacement mode and return the tool response.
|
|
1029
|
+
|
|
1030
|
+
``resolved_id`` (set only when the optional hash check pre-resolved
|
|
1031
|
+
``identifier``) is threaded to the upsert so it skips the redundant
|
|
1032
|
+
re-resolve; None falls back to resolving inside the REST client.
|
|
1033
|
+
"""
|
|
1034
|
+
result = await self._upsert_automation(config_dict, identifier, resolved_id)
|
|
994
1035
|
|
|
995
1036
|
for warning in conflict_warnings or []:
|
|
996
1037
|
result.setdefault("warnings", []).append(warning)
|
|
@@ -1137,10 +1178,18 @@ class AutomationConfigTools:
|
|
|
1137
1178
|
|
|
1138
1179
|
async def _fetch_and_verify_hash(
|
|
1139
1180
|
self, identifier: str, config_hash: str, action: str
|
|
1140
|
-
) -> dict[str, Any]:
|
|
1181
|
+
) -> tuple[dict[str, Any], str | None]:
|
|
1141
1182
|
"""Fetch current automation config and verify config_hash for optimistic locking.
|
|
1142
1183
|
|
|
1143
|
-
Returns
|
|
1184
|
+
Returns ``(current_normalized_config, resolved_unique_id)``. The fetch
|
|
1185
|
+
already resolved ``identifier`` to the storage key inside
|
|
1186
|
+
``get_automation_config``; HA returns the stored body keyed by that
|
|
1187
|
+
unique_id, so ``config['id']`` IS the resolved unique_id (the same value
|
|
1188
|
+
the #1404 guard compares against). Threading it lets the caller pass
|
|
1189
|
+
``_resolved=True`` to ``upsert_automation_config`` and skip the redundant
|
|
1190
|
+
second resolve (issue #1813 Phase 0). ``None`` when the stored body has
|
|
1191
|
+
no ``id`` (not expected for a real automation) — the caller then falls
|
|
1192
|
+
back to re-resolving.
|
|
1144
1193
|
Raises ToolError if the hash does not match (conflict).
|
|
1145
1194
|
"""
|
|
1146
1195
|
current_config, current_hash = await self._get_automation_config_internal(
|
|
@@ -1158,7 +1207,11 @@ class AutomationConfigTools:
|
|
|
1158
1207
|
context={"action": action, "identifier": identifier},
|
|
1159
1208
|
)
|
|
1160
1209
|
)
|
|
1161
|
-
|
|
1210
|
+
raw_id = current_config.get("id")
|
|
1211
|
+
# Stringify to match ``_resolve_automation_id``'s ``str()`` coercion so a
|
|
1212
|
+
# threaded id is byte-for-byte what the unthreaded path would produce.
|
|
1213
|
+
resolved_id = str(raw_id) if raw_id is not None else None
|
|
1214
|
+
return current_config, resolved_id
|
|
1162
1215
|
|
|
1163
1216
|
@staticmethod
|
|
1164
1217
|
def _parse_and_validate_config(config: str | dict[str, Any]) -> dict[str, Any]:
|
{ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/tools/tools_config_scenes.py
RENAMED
|
@@ -386,7 +386,7 @@ class ConfigSceneTools:
|
|
|
386
386
|
|
|
387
387
|
async def _get_scene_config_internal(
|
|
388
388
|
self, scene_id: str, *, _resolved: bool = False
|
|
389
|
-
) -> tuple[dict[str, Any], str, str]:
|
|
389
|
+
) -> tuple[dict[str, Any], str, str | None]:
|
|
390
390
|
"""Fetch scene config without logging or category injection.
|
|
391
391
|
|
|
392
392
|
Returns ``(actual_config, config_hash, resolved_id)`` where
|
|
@@ -394,27 +394,36 @@ class ConfigSceneTools:
|
|
|
394
394
|
``resolved_id`` is the storage key the rest-client resolved the input
|
|
395
395
|
to (issue #1168 R3 blocker 6). Used by ``_fetch_and_verify_hash``.
|
|
396
396
|
|
|
397
|
+
``resolved_id`` is ``None`` when the envelope omits ``scene_id`` — the
|
|
398
|
+
write target must then be re-resolved from the caller's input rather
|
|
399
|
+
than defaulting to the (unresolved) caller slug, which for a renamed
|
|
400
|
+
scene would target the wrong storage key. This is a structural
|
|
401
|
+
invariant, not a live path: ``get_scene_config`` always sets the key.
|
|
402
|
+
|
|
397
403
|
``_resolved=True`` forwards to ``get_scene_config`` that ``scene_id`` is
|
|
398
404
|
already a resolved storage key, so the redundant registry lookup is
|
|
399
405
|
skipped (e.g. the python_transform re-fetch, which already resolved).
|
|
400
406
|
"""
|
|
401
407
|
envelope = await self._client.get_scene_config(scene_id, _resolved=_resolved)
|
|
402
408
|
actual_config = envelope.get("config", envelope)
|
|
403
|
-
resolved_id = envelope.get("scene_id"
|
|
409
|
+
resolved_id = envelope.get("scene_id")
|
|
404
410
|
config_hash_value = compute_config_hash(actual_config)
|
|
405
411
|
return actual_config, config_hash_value, resolved_id
|
|
406
412
|
|
|
407
413
|
async def _fetch_and_verify_hash(
|
|
408
414
|
self, scene_id: str, config_hash: str, action: str
|
|
409
|
-
) -> tuple[dict[str, Any], str]:
|
|
415
|
+
) -> tuple[dict[str, Any], str | None]:
|
|
410
416
|
"""Fetch current scene config and verify config_hash for optimistic locking.
|
|
411
417
|
|
|
412
418
|
Returns ``(actual_config, resolved_id)`` — the inner scene body and
|
|
413
|
-
the storage key the rest-client resolved the input to.
|
|
414
|
-
``
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
419
|
+
the storage key the rest-client resolved the input to. ``resolved_id``
|
|
420
|
+
is ``None`` when the envelope omitted the key, so the upsert re-resolves
|
|
421
|
+
rather than writing to the caller slug; the caller then re-binds it from
|
|
422
|
+
the upsert result. Raises ``ToolError`` if the hash does not match
|
|
423
|
+
(conflict). Issue #1168 R3 blocker 6: callers thread ``resolved_id`` into
|
|
424
|
+
responses so the outer ``scene_id`` matches the inner body's ``id``
|
|
425
|
+
regardless of whether the caller passed the entity_id slug or the
|
|
426
|
+
storage key.
|
|
418
427
|
"""
|
|
419
428
|
(
|
|
420
429
|
actual_config,
|
|
@@ -691,6 +700,12 @@ class ConfigSceneTools:
|
|
|
691
700
|
actual_config, resolved_id = await self._fetch_and_verify_hash(
|
|
692
701
|
scene_id, config_hash, "python_transform"
|
|
693
702
|
)
|
|
703
|
+
# Capture the scene's own ``id`` BEFORE the transform mutates the
|
|
704
|
+
# config in place — it is the stable identity the id-change guard
|
|
705
|
+
# compares against (in production it equals ``resolved_id``; it is
|
|
706
|
+
# also correct when ``resolved_id`` is None because the envelope
|
|
707
|
+
# omitted ``scene_id``).
|
|
708
|
+
original_id = actual_config.get("id")
|
|
694
709
|
|
|
695
710
|
try:
|
|
696
711
|
transformed_config = safe_execute(python_transform, actual_config)
|
|
@@ -773,23 +788,26 @@ class ConfigSceneTools:
|
|
|
773
788
|
# legitimate (HA treats ``id`` as optional) and should pass
|
|
774
789
|
# through. R7 blocker 24: ``transformed_config["id"] = None``
|
|
775
790
|
# is the in-place equivalent of ``del`` — normalise both
|
|
776
|
-
# by checking against ``(None,
|
|
777
|
-
# explicit non-None mismatch triggers the reject.
|
|
791
|
+
# by checking against ``(None, original_id)``, so only an
|
|
792
|
+
# explicit non-None mismatch triggers the reject. Compare against
|
|
793
|
+
# the scene's own captured ``id`` (not ``resolved_id``, which may
|
|
794
|
+
# be None on the structural-invariant path) — in production the
|
|
795
|
+
# two are equal.
|
|
778
796
|
if "id" in transformed_config and transformed_config["id"] not in (
|
|
779
797
|
None,
|
|
780
|
-
|
|
798
|
+
original_id,
|
|
781
799
|
):
|
|
782
800
|
raise_tool_error(
|
|
783
801
|
create_error_response(
|
|
784
802
|
ErrorCode.VALIDATION_FAILED,
|
|
785
803
|
"Transform must not change ``config['id']``",
|
|
786
804
|
suggestions=[
|
|
787
|
-
f"Original scene_id is {
|
|
805
|
+
f"Original scene_id is {original_id!r} — keep it unchanged",
|
|
788
806
|
"To rename a scene, use ha_config_remove_scene + ha_config_set_scene",
|
|
789
807
|
],
|
|
790
808
|
context={
|
|
791
809
|
"action": "python_transform",
|
|
792
|
-
"scene_id":
|
|
810
|
+
"scene_id": original_id,
|
|
793
811
|
"attempted_id": transformed_config.get("id"),
|
|
794
812
|
},
|
|
795
813
|
)
|
|
@@ -828,9 +846,17 @@ class ConfigSceneTools:
|
|
|
828
846
|
if category:
|
|
829
847
|
await self._validate_category_id(category)
|
|
830
848
|
|
|
849
|
+
# ``resolved_id`` is the storage key (write target); ``scene_id``
|
|
850
|
+
# stays the caller id so a missing ``name`` defaults caller-facing
|
|
851
|
+
# rather than to the storage key (#1935).
|
|
831
852
|
result = await self._client.upsert_scene_config(
|
|
832
|
-
transformed_config,
|
|
853
|
+
transformed_config, scene_id, resolved_id=resolved_id
|
|
833
854
|
)
|
|
855
|
+
# The upsert re-resolves when ``resolved_id`` was None (envelope
|
|
856
|
+
# omitted the key); re-bind to the authoritative write target it
|
|
857
|
+
# returned so the re-fetch, entity resolution, and response all
|
|
858
|
+
# use the resolved storage key rather than a possible None.
|
|
859
|
+
resolved_id = result.get("scene_id", resolved_id)
|
|
834
860
|
|
|
835
861
|
# Re-fetch to get authoritative hash (HA may normalise after save).
|
|
836
862
|
# ``resolved_id`` is already the storage key, so skip the re-resolve.
|
|
@@ -960,10 +986,18 @@ class ConfigSceneTools:
|
|
|
960
986
|
)
|
|
961
987
|
|
|
962
988
|
# ``resolved_id`` is already the storage key (from the hash-verify
|
|
963
|
-
# fetch or the resolve above)
|
|
989
|
+
# fetch or the resolve above) — pass it as the write target to skip
|
|
990
|
+
# the redundant re-resolve. ``scene_id`` stays the caller id so a
|
|
991
|
+
# missing ``name`` defaults caller-facing, not to the storage key
|
|
992
|
+
# (#1935).
|
|
964
993
|
result = await self._client.upsert_scene_config(
|
|
965
|
-
config_dict,
|
|
994
|
+
config_dict, scene_id, resolved_id=resolved_id
|
|
966
995
|
)
|
|
996
|
+
# The upsert re-resolves when ``resolved_id`` was None (envelope
|
|
997
|
+
# omitted the key); re-bind to the authoritative write target it
|
|
998
|
+
# returned so entity resolution and the response use the resolved
|
|
999
|
+
# storage key rather than a possible None.
|
|
1000
|
+
resolved_id = result.get("scene_id", resolved_id)
|
|
967
1001
|
|
|
968
1002
|
# Resolve actual entity_id via registry — HA derives scene
|
|
969
1003
|
# entity_ids from the 'name' slug, not the scene_id storage key,
|
{ha_mcp_dev-7.13.0.dev2007 → ha_mcp_dev-7.13.0.dev2011}/src/ha_mcp/tools/tools_config_scripts.py
RENAMED
|
@@ -302,30 +302,48 @@ class ConfigScriptTools:
|
|
|
302
302
|
|
|
303
303
|
async def _get_script_config_internal(
|
|
304
304
|
self, script_id: str
|
|
305
|
-
) -> tuple[dict[str, Any], str]:
|
|
305
|
+
) -> tuple[dict[str, Any], str, str | None]:
|
|
306
306
|
"""Fetch script config without logging or category injection.
|
|
307
307
|
|
|
308
|
-
Returns (actual_config, config_hash)
|
|
309
|
-
the inner script body (not the REST wrapper)
|
|
308
|
+
Returns ``(actual_config, config_hash, resolved_id)`` where
|
|
309
|
+
``actual_config`` is the inner script body (not the REST wrapper) and
|
|
310
|
+
``resolved_id`` is the storage key the REST client resolved the input to
|
|
311
|
+
(from the envelope's ``script_id``). Threading ``resolved_id`` lets the
|
|
312
|
+
upsert call site skip the redundant re-resolve (issue #1813 Phase 0).
|
|
310
313
|
Used internally by _fetch_and_verify_hash and ha_config_get_script.
|
|
311
314
|
|
|
315
|
+
``resolved_id`` is ``None`` when the envelope omits ``script_id`` — the
|
|
316
|
+
write target must then be re-resolved from the caller's input rather
|
|
317
|
+
than defaulting to the (unresolved) caller slug, which for a renamed
|
|
318
|
+
script would target the wrong storage key. This is a structural
|
|
319
|
+
invariant, not a live path: ``get_script_config`` always sets the key.
|
|
320
|
+
|
|
312
321
|
404 responses from the REST client are mapped to a structured
|
|
313
322
|
``RESOURCE_NOT_FOUND`` ToolError via ``_fetch_script_config_envelope``.
|
|
314
323
|
"""
|
|
315
324
|
config_result = await self._fetch_script_config_envelope(script_id)
|
|
316
325
|
actual_config = config_result.get("config", config_result)
|
|
317
326
|
config_hash_value = compute_config_hash(actual_config)
|
|
318
|
-
|
|
327
|
+
resolved_id = config_result.get("script_id")
|
|
328
|
+
return actual_config, config_hash_value, resolved_id
|
|
319
329
|
|
|
320
330
|
async def _fetch_and_verify_hash(
|
|
321
331
|
self, script_id: str, config_hash: str, action: str
|
|
322
|
-
) -> dict[str, Any]:
|
|
332
|
+
) -> tuple[dict[str, Any], str | None]:
|
|
323
333
|
"""Fetch current script config and verify config_hash for optimistic locking.
|
|
324
334
|
|
|
325
|
-
Returns the
|
|
326
|
-
|
|
335
|
+
Returns ``(actual_config, resolved_id)`` — the inner script body and the
|
|
336
|
+
storage key the REST client resolved the input to. Threading
|
|
337
|
+
``resolved_id`` lets the upsert call site skip the redundant re-resolve
|
|
338
|
+
(issue #1813 Phase 0); it is ``None`` when the envelope omitted the key,
|
|
339
|
+
so the upsert re-resolves rather than writing to the caller slug. Raises
|
|
340
|
+
ToolError if the hash does not match (conflict).
|
|
327
341
|
"""
|
|
328
|
-
|
|
342
|
+
(
|
|
343
|
+
actual_config,
|
|
344
|
+
current_hash,
|
|
345
|
+
resolved_id,
|
|
346
|
+
) = await self._get_script_config_internal(script_id)
|
|
329
347
|
if current_hash != config_hash:
|
|
330
348
|
raise_tool_error(
|
|
331
349
|
create_error_response(
|
|
@@ -338,7 +356,24 @@ class ConfigScriptTools:
|
|
|
338
356
|
context={"action": action, "script_id": script_id},
|
|
339
357
|
)
|
|
340
358
|
)
|
|
341
|
-
return actual_config
|
|
359
|
+
return actual_config, resolved_id
|
|
360
|
+
|
|
361
|
+
async def _upsert_script(
|
|
362
|
+
self, config: dict[str, Any], script_id: str, resolved_id: str | None
|
|
363
|
+
) -> dict[str, Any]:
|
|
364
|
+
"""Upsert, threading a pre-resolved storage key when available.
|
|
365
|
+
|
|
366
|
+
``script_id`` is always the CALLER's identifier (used to default a
|
|
367
|
+
missing alias). ``resolved_id`` — set only when ``_fetch_and_verify_hash``
|
|
368
|
+
already resolved the storage key — is passed as the write target so the
|
|
369
|
+
REST client skips the redundant entity-registry lookup (issue #1813
|
|
370
|
+
Phase 0); None lets the REST client resolve. Passing the caller id
|
|
371
|
+
separately keeps a renamed script's alias caller-facing (#1935).
|
|
372
|
+
"""
|
|
373
|
+
result: dict[str, Any] = await self._client.upsert_script_config(
|
|
374
|
+
config, script_id, resolved_id=resolved_id
|
|
375
|
+
)
|
|
376
|
+
return result
|
|
342
377
|
|
|
343
378
|
@staticmethod
|
|
344
379
|
def _validate_script_config(
|
|
@@ -677,7 +712,7 @@ class ConfigScriptTools:
|
|
|
677
712
|
)
|
|
678
713
|
|
|
679
714
|
# Fetch current config and verify hash
|
|
680
|
-
actual_config = await self._fetch_and_verify_hash(
|
|
715
|
+
actual_config, resolved_id = await self._fetch_and_verify_hash(
|
|
681
716
|
script_id, config_hash, "python_transform"
|
|
682
717
|
)
|
|
683
718
|
|
|
@@ -719,13 +754,20 @@ class ConfigScriptTools:
|
|
|
719
754
|
)
|
|
720
755
|
bp_warnings = _check_best_practices(transformed_config)
|
|
721
756
|
|
|
722
|
-
# Save transformed config
|
|
757
|
+
# Save transformed config. ``_fetch_and_verify_hash`` already
|
|
758
|
+
# resolved the storage key; pass it as the write target so the
|
|
759
|
+
# upsert skips the redundant re-resolve (issue #1813 Phase 0).
|
|
760
|
+
# ``script_id`` stays the caller id (the fetched config already
|
|
761
|
+
# carries an alias here, so the default is a no-op, but keep the
|
|
762
|
+
# contract consistent — #1935).
|
|
723
763
|
result = await self._client.upsert_script_config(
|
|
724
|
-
transformed_config, script_id
|
|
764
|
+
transformed_config, script_id, resolved_id=resolved_id
|
|
725
765
|
)
|
|
726
766
|
|
|
727
767
|
# Re-fetch to get authoritative hash (HA may normalize after save)
|
|
728
|
-
_, new_config_hash = await self._get_script_config_internal(
|
|
768
|
+
_, new_config_hash, _ = await self._get_script_config_internal(
|
|
769
|
+
script_id
|
|
770
|
+
)
|
|
729
771
|
|
|
730
772
|
response: dict[str, Any] = {
|
|
731
773
|
"success": True,
|
|
@@ -766,9 +808,15 @@ class ConfigScriptTools:
|
|
|
766
808
|
category,
|
|
767
809
|
)
|
|
768
810
|
|
|
769
|
-
# Optional hash check for full config updates
|
|
811
|
+
# Optional hash check for full config updates. When it runs it
|
|
812
|
+
# resolves ``script_id`` to the storage key — thread that through so
|
|
813
|
+
# the upsert doesn't re-resolve (issue #1813 Phase 0). Stays None on
|
|
814
|
+
# the no-hash path (raw script_id resolved once, inside upsert).
|
|
815
|
+
resolved_key: str | None = None
|
|
770
816
|
if config_hash:
|
|
771
|
-
await self._fetch_and_verify_hash(
|
|
817
|
+
_, resolved_key = await self._fetch_and_verify_hash(
|
|
818
|
+
script_id, config_hash, "set"
|
|
819
|
+
)
|
|
772
820
|
|
|
773
821
|
# Pre-check for best-practice issues.
|
|
774
822
|
bp_warnings = _check_best_practices(config_dict)
|
|
@@ -780,7 +828,7 @@ class ConfigScriptTools:
|
|
|
780
828
|
self._client, config_dict
|
|
781
829
|
)
|
|
782
830
|
|
|
783
|
-
result = await self.
|
|
831
|
+
result = await self._upsert_script(config_dict, script_id, resolved_key)
|
|
784
832
|
|
|
785
833
|
# Wait for script to be queryable
|
|
786
834
|
entity_id = f"script.{script_id}"
|