ha-mcp-dev 7.14.2.dev2183__tar.gz → 7.14.2.dev2188__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.14.2.dev2183/src/ha_mcp_dev.egg-info → ha_mcp_dev-7.14.2.dev2188}/PKG-INFO +1 -1
- {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/pyproject.toml +1 -1
- {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/tools/config_entry_flow.py +411 -52
- {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/tools/tools_service.py +147 -16
- {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/tools/util_helpers.py +11 -2
- {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188/src/ha_mcp_dev.egg-info}/PKG-INFO +1 -1
- {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/LICENSE +0 -0
- {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/MANIFEST.in +0 -0
- {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/README.md +0 -0
- {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/setup.cfg +0 -0
- {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/__init__.py +0 -0
- {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/__main__.py +0 -0
- {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/_pypi_marker +0 -0
- {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/_version.py +0 -0
- {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/auth/__init__.py +0 -0
- {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/auth/consent_form.py +0 -0
- {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/auth/provider.py +0 -0
- {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/backup_manager.py +0 -0
- {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/browser_landing.py +0 -0
- {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/client/__init__.py +0 -0
- {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/client/rest_client.py +0 -0
- {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/client/supervisor_client.py +0 -0
- {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/client/websocket_client.py +0 -0
- {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/client/websocket_listener.py +0 -0
- {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/config.py +0 -0
- {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/dashboard_screenshot/__init__.py +0 -0
- {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/dashboard_screenshot/capture.py +0 -0
- {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/dashboard_screenshot/content.py +0 -0
- {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/dashboard_screenshot/paths.py +0 -0
- {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/dashboard_screenshot/provision.py +0 -0
- {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/dashboard_screenshot/theme_guard.py +0 -0
- {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/errors.py +0 -0
- {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/llm_exposure.py +0 -0
- {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/policy/__init__.py +0 -0
- {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/policy/approval_queue.py +0 -0
- {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/policy/evaluator.py +0 -0
- {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/policy/handlers.py +0 -0
- {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/policy/middleware.py +0 -0
- {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/policy/model.py +0 -0
- {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/policy/persistence.py +0 -0
- {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/policy/value_sources.py +0 -0
- {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/py.typed +0 -0
- {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/read_only.py +0 -0
- {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/resources/skills-vendor/.claude/settings.json +0 -0
- {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/resources/skills-vendor/.claude-plugin/marketplace.json +0 -0
- {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/resources/skills-vendor/.claude-plugin/plugin.json +0 -0
- {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/resources/skills-vendor/.github/ISSUE_TEMPLATE/skill-rca.md +0 -0
- {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/resources/skills-vendor/.github/pull_request_template.md +0 -0
- {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/resources/skills-vendor/AGENTS.md +0 -0
- {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/resources/skills-vendor/CLAUDE.md +0 -0
- {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/resources/skills-vendor/CONTRIBUTING.md +0 -0
- {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/resources/skills-vendor/LICENSE +0 -0
- {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/resources/skills-vendor/README.md +0 -0
- {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/SKILL.md +0 -0
- {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/evals/evals.json +0 -0
- {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/appdaemon.md +0 -0
- {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/automation-patterns.md +0 -0
- {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/blueprint-guide.md +0 -0
- {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-cards.md +0 -0
- {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-guide.md +0 -0
- {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/device-control.md +0 -0
- {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/domain-docs.md +0 -0
- {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/examples.yaml +0 -0
- {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/helper-selection.md +0 -0
- {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/safe-refactoring.md +0 -0
- {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/scenes.md +0 -0
- {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/template-guidelines.md +0 -0
- {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/yaml-only-integrations.md +0 -0
- {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/server.py +0 -0
- {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/settings_ui/__init__.py +0 -0
- {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/settings_ui/_handlers_advanced.py +0 -0
- {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/settings_ui/_handlers_backups.py +0 -0
- {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/settings_ui/_handlers_fs.py +0 -0
- {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/settings_ui/_handlers_server.py +0 -0
- {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/settings_ui/_handlers_theme.py +0 -0
- {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/settings_ui/_handlers_tools.py +0 -0
- {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/settings_ui/_i18n.py +0 -0
- {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/settings_ui/_persistence.py +0 -0
- {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/settings_ui/_supervisor.py +0 -0
- {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/settings_ui/_theme.py +0 -0
- {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/settings_ui/_tools_meta.py +0 -0
- {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/settings_ui/locales/README.md +0 -0
- {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/settings_ui/locales/de.json +0 -0
- {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/settings_ui/locales/en.json +0 -0
- {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/settings_ui/locales/es.json +0 -0
- {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/settings_ui/locales/fr.json +0 -0
- {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/settings_ui/locales/it.json +0 -0
- {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/settings_ui/locales/ru.json +0 -0
- {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/settings_ui/locales/zh-Hans.json +0 -0
- {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/settings_ui/settings.css +0 -0
- {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/settings_ui/settings.html +0 -0
- {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/settings_ui/settings.js +0 -0
- {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/smoke_test.py +0 -0
- {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/stdio_settings_sidecar.py +0 -0
- {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/strict_bps.py +0 -0
- {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/tools/__init__.py +0 -0
- {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/tools/auto_backup.py +0 -0
- {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/tools/backup.py +0 -0
- {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/tools/best_practice_checker.py +0 -0
- {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/tools/component_api.py +0 -0
- {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/tools/component_config_reads.py +0 -0
- {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/tools/component_devices.py +0 -0
- {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/tools/component_registries.py +0 -0
- {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/tools/component_registry_lookup.py +0 -0
- {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/tools/device_control.py +0 -0
- {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/tools/helpers.py +0 -0
- {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/tools/radio/__init__.py +0 -0
- {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/tools/radio/base.py +0 -0
- {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/tools/radio/matter.py +0 -0
- {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/tools/radio/thread.py +0 -0
- {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/tools/radio/zigbee.py +0 -0
- {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/tools/radio/zwave.py +0 -0
- {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/tools/reference_validator.py +0 -0
- {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/tools/registry.py +0 -0
- {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/tools/smart_search/__init__.py +0 -0
- {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/tools/smart_search/_base.py +0 -0
- {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/tools/smart_search/_config.py +0 -0
- {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/tools/smart_search/_deep.py +0 -0
- {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/tools/smart_search/_entities.py +0 -0
- {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/tools/smart_search/_fetch.py +0 -0
- {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/tools/smart_search/_overview.py +0 -0
- {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/tools/smart_search/_scenes.py +0 -0
- {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/tools/smart_search/_scoring.py +0 -0
- {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/tools/tool_search_hint_middleware.py +0 -0
- {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/tools/tools_addons.py +0 -0
- {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/tools/tools_areas.py +0 -0
- {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/tools/tools_blueprints.py +0 -0
- {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/tools/tools_bug_report.py +0 -0
- {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/tools/tools_calendar.py +0 -0
- {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/tools/tools_camera.py +0 -0
- {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/tools/tools_categories.py +0 -0
- {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/tools/tools_code.py +0 -0
- {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/tools/tools_config_automations.py +0 -0
- {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/tools/tools_config_dashboards.py +0 -0
- {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/tools/tools_config_helpers.py +0 -0
- {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/tools/tools_config_scenes.py +0 -0
- {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/tools/tools_config_scripts.py +0 -0
- {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/tools/tools_dashboard_screenshot.py +0 -0
- {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/tools/tools_dev.py +0 -0
- {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/tools/tools_energy.py +0 -0
- {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/tools/tools_entities.py +0 -0
- {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/tools/tools_filesystem.py +0 -0
- {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/tools/tools_groups.py +0 -0
- {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/tools/tools_hacs.py +0 -0
- {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/tools/tools_history.py +0 -0
- {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/tools/tools_integrations.py +0 -0
- {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/tools/tools_labels.py +0 -0
- {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/tools/tools_radio.py +0 -0
- {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/tools/tools_registry.py +0 -0
- {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/tools/tools_resources.py +0 -0
- {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/tools/tools_search.py +0 -0
- {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/tools/tools_services.py +0 -0
- {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/tools/tools_system.py +0 -0
- {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/tools/tools_themes.py +0 -0
- {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/tools/tools_todo.py +0 -0
- {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/tools/tools_traces.py +0 -0
- {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/tools/tools_updates.py +0 -0
- {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/tools/tools_utility.py +0 -0
- {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/tools/tools_voice_assistant.py +0 -0
- {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/tools/tools_yaml_config.py +0 -0
- {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/tools/tools_yaml_read.py +0 -0
- {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/tools/tools_zones.py +0 -0
- {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/tools/validation_middleware.py +0 -0
- {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/transforms/__init__.py +0 -0
- {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/transforms/categorized_search.py +0 -0
- {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/transforms/lite_docstrings.py +0 -0
- {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/transport_security.py +0 -0
- {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/update_check.py +0 -0
- {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/utils/__init__.py +0 -0
- {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/utils/config_hash.py +0 -0
- {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/utils/config_write_lock.py +0 -0
- {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/utils/data_paths.py +0 -0
- {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/utils/domain_handlers.py +0 -0
- {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/utils/fuzzy_search.py +0 -0
- {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/utils/kill_signal_diagnostics.py +0 -0
- {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/utils/operation_manager.py +0 -0
- {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/utils/python_sandbox.py +0 -0
- {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/utils/skill_loader.py +0 -0
- {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/utils/usage_logger.py +0 -0
- {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/visibility/__init__.py +0 -0
- {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/visibility/enforcement.py +0 -0
- {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/visibility/model.py +0 -0
- {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/visibility/persistence.py +0 -0
- {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/visibility/resolver.py +0 -0
- {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp_dev.egg-info/SOURCES.txt +0 -0
- {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp_dev.egg-info/dependency_links.txt +0 -0
- {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp_dev.egg-info/entry_points.txt +0 -0
- {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp_dev.egg-info/requires.txt +0 -0
- {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp_dev.egg-info/top_level.txt +0 -0
- {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/tests/__init__.py +0 -0
- {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/tests/test_constants.py +0 -0
- {ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/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.14.2.
|
|
7
|
+
version = "7.14.2.dev2188"
|
|
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
|
{ha_mcp_dev-7.14.2.dev2183 → ha_mcp_dev-7.14.2.dev2188}/src/ha_mcp/tools/config_entry_flow.py
RENAMED
|
@@ -8,12 +8,20 @@ Config Entry Flow API.
|
|
|
8
8
|
The create/update entry point is the unified ha_config_set_helper tool in
|
|
9
9
|
tools_config_helpers.py, which routes to create_flow_helper / update_flow_helper
|
|
10
10
|
for the 15 helper types listed in FLOW_HELPER_TYPES.
|
|
11
|
+
|
|
12
|
+
The same flow walkers drive every other config-entry surface, not just
|
|
13
|
+
helpers: ``ha_set_integration`` creates entries for arbitrary domains through
|
|
14
|
+
``create_config_entry`` and edits them through ``update_config_entry_options``,
|
|
15
|
+
and ``ha_config_set_helper(helper_type="config_subentry")`` drives subentry
|
|
16
|
+
flows through ``set_config_subentry``.
|
|
11
17
|
"""
|
|
12
18
|
|
|
13
19
|
import asyncio
|
|
14
20
|
import copy
|
|
15
21
|
import logging
|
|
16
22
|
from collections.abc import Iterator
|
|
23
|
+
from dataclasses import dataclass
|
|
24
|
+
from dataclasses import field as dc_field
|
|
17
25
|
from enum import StrEnum
|
|
18
26
|
from typing import Any, Literal, NoReturn
|
|
19
27
|
|
|
@@ -80,6 +88,8 @@ _RECONFIGURE_SUCCESS_REASONS = frozenset(
|
|
|
80
88
|
}
|
|
81
89
|
)
|
|
82
90
|
_MISSING_DEFAULT = object()
|
|
91
|
+
# "Submit nothing for this field" — see _redeclared_field_submission.
|
|
92
|
+
_NO_SUBMISSION: tuple[Any, bool] = (_MISSING_DEFAULT, False)
|
|
83
93
|
|
|
84
94
|
|
|
85
95
|
class _FlowType(StrEnum):
|
|
@@ -159,6 +169,89 @@ def _section_path(path_prefix: str, name: Any) -> str:
|
|
|
159
169
|
return f"{path_prefix}.{name}" if path_prefix else name
|
|
160
170
|
|
|
161
171
|
|
|
172
|
+
@dataclass
|
|
173
|
+
class _ReuseState:
|
|
174
|
+
"""Caller-supplied form values a flow's steps have already consumed.
|
|
175
|
+
|
|
176
|
+
A config key applies to every form step that declares it, not only the
|
|
177
|
+
first step to pop it out of ``remaining_config`` — see
|
|
178
|
+
:func:`_redeclared_field_submission` for when a recorded value is
|
|
179
|
+
resubmitted. The record is split by how the caller wrote the value:
|
|
180
|
+
|
|
181
|
+
- ``scoped`` maps a dotted declaration path to a value that came out of an
|
|
182
|
+
explicitly supplied section dict. The caller named that section, so the
|
|
183
|
+
value belongs to that path and nowhere else.
|
|
184
|
+
- ``flat`` maps a leaf name to a value that came out of the flat caller
|
|
185
|
+
dict. The caller named no section, so the value is position-agnostic and
|
|
186
|
+
fills that leaf wherever a later step declares it.
|
|
187
|
+
|
|
188
|
+
``filled`` holds the dotted paths the current step already filled from the
|
|
189
|
+
caller's own keys, so nothing is injected over a value the caller wrote for
|
|
190
|
+
this very step; it is cleared per step. ``fired`` bounds resubmission to
|
|
191
|
+
one write per (step, path) for the whole flow, ``notes`` collects the
|
|
192
|
+
warning each of those writes emits, and ``step_id`` names the step being
|
|
193
|
+
consumed.
|
|
194
|
+
"""
|
|
195
|
+
|
|
196
|
+
scoped: dict[str, Any] = dc_field(default_factory=dict)
|
|
197
|
+
flat: dict[str, Any] = dc_field(default_factory=dict)
|
|
198
|
+
filled: set[str] = dc_field(default_factory=set)
|
|
199
|
+
fired: set[str] = dc_field(default_factory=set)
|
|
200
|
+
notes: list[str] = dc_field(default_factory=list)
|
|
201
|
+
step_id: str | None = None
|
|
202
|
+
|
|
203
|
+
def begin_step(self, step_id: Any) -> None:
|
|
204
|
+
"""Start recording for a new form step."""
|
|
205
|
+
self.step_id = step_id if isinstance(step_id, str) else None
|
|
206
|
+
self.filled.clear()
|
|
207
|
+
|
|
208
|
+
def record(
|
|
209
|
+
self, path_prefix: str, name: str, value: Any, *, scoped_only: bool
|
|
210
|
+
) -> None:
|
|
211
|
+
"""Snapshot a value the caller supplied at this declaration site.
|
|
212
|
+
|
|
213
|
+
A flat value that lands on a path already recorded from an explicit
|
|
214
|
+
section dict also replaces that scoped entry: the flat key is the one
|
|
215
|
+
actually submitted for the path (flat overrides explicit), so the
|
|
216
|
+
record must carry the effective value or a later redeclaration of the
|
|
217
|
+
path would resurrect the overridden one.
|
|
218
|
+
"""
|
|
219
|
+
dotted = _section_path(path_prefix, name)
|
|
220
|
+
self.filled.add(dotted)
|
|
221
|
+
if scoped_only:
|
|
222
|
+
self.scoped[dotted] = copy.deepcopy(value)
|
|
223
|
+
else:
|
|
224
|
+
self.flat[name] = copy.deepcopy(value)
|
|
225
|
+
if dotted in self.scoped:
|
|
226
|
+
self.scoped[dotted] = copy.deepcopy(value)
|
|
227
|
+
|
|
228
|
+
def recorded_value(self, path_prefix: str, name: str) -> Any:
|
|
229
|
+
"""Return the value recorded for this declaration site, else _MISSING_DEFAULT."""
|
|
230
|
+
dotted = _section_path(path_prefix, name)
|
|
231
|
+
if dotted in self.scoped:
|
|
232
|
+
return copy.deepcopy(self.scoped[dotted])
|
|
233
|
+
if name in self.flat:
|
|
234
|
+
return copy.deepcopy(self.flat[name])
|
|
235
|
+
return _MISSING_DEFAULT
|
|
236
|
+
|
|
237
|
+
def claim_write(self, dotted: str) -> bool:
|
|
238
|
+
"""Spend the single resubmission allowed for this (step, path), if unspent.
|
|
239
|
+
|
|
240
|
+
A flow that re-presents the same step gets one reused write and then
|
|
241
|
+
HA's own loud "required key not provided" naming the field, rather than
|
|
242
|
+
an unbounded run of silent rewrites.
|
|
243
|
+
"""
|
|
244
|
+
key = f"{self.step_id}:{dotted}"
|
|
245
|
+
if key in self.fired:
|
|
246
|
+
return False
|
|
247
|
+
self.fired.add(key)
|
|
248
|
+
self.notes.append(
|
|
249
|
+
f"Resubmitted '{dotted}' at step '{self.step_id}': supplied once "
|
|
250
|
+
"but declared at more than one site in this flow"
|
|
251
|
+
)
|
|
252
|
+
return True
|
|
253
|
+
|
|
254
|
+
|
|
162
255
|
def _record_ignored_section_keys(
|
|
163
256
|
ignored_config_keys: set[str] | None,
|
|
164
257
|
remaining_config: dict[str, Any],
|
|
@@ -237,6 +330,22 @@ def _ignored_keys_warnings(
|
|
|
237
330
|
return warnings
|
|
238
331
|
|
|
239
332
|
|
|
333
|
+
def _success_warnings(
|
|
334
|
+
ignored_config_keys: set[str],
|
|
335
|
+
remaining_config: dict[str, Any],
|
|
336
|
+
reuse_state: _ReuseState,
|
|
337
|
+
) -> list[str]:
|
|
338
|
+
"""Build a success response's ``warnings`` list (empty when there is nothing to say).
|
|
339
|
+
|
|
340
|
+
Merges the keys no step declared with the resubmissions the walk performed,
|
|
341
|
+
keeping ``warnings`` a flat ``list[str]`` per the response contract in
|
|
342
|
+
``tests/src/unit/test_helper_response_shape.py``.
|
|
343
|
+
"""
|
|
344
|
+
return _ignored_keys_warnings(ignored_config_keys, remaining_config) + list(
|
|
345
|
+
reuse_state.notes
|
|
346
|
+
)
|
|
347
|
+
|
|
348
|
+
|
|
240
349
|
def _consume_section_schema(
|
|
241
350
|
field: dict[str, Any],
|
|
242
351
|
explicit_section: dict[str, Any] | None,
|
|
@@ -244,8 +353,17 @@ def _consume_section_schema(
|
|
|
244
353
|
ignored_config_keys: set[str] | None,
|
|
245
354
|
consumed_config_keys: set[str] | None,
|
|
246
355
|
path_prefix: str,
|
|
356
|
+
reuse_state: _ReuseState | None = None,
|
|
357
|
+
*,
|
|
358
|
+
allow_reuse: bool = True,
|
|
359
|
+
explicit_source: bool = False,
|
|
247
360
|
) -> dict[str, Any]:
|
|
248
|
-
"""Consume config values for a nested flow section.
|
|
361
|
+
"""Consume config values for a nested flow section.
|
|
362
|
+
|
|
363
|
+
Values inside an explicitly supplied section dict are consumed first, then
|
|
364
|
+
flat caller keys, which is what lets a flat child override the same key
|
|
365
|
+
written inside the section dict.
|
|
366
|
+
"""
|
|
249
367
|
nested_schema = field.get("schema")
|
|
250
368
|
if not isinstance(nested_schema, list):
|
|
251
369
|
return {}
|
|
@@ -263,6 +381,9 @@ def _consume_section_schema(
|
|
|
263
381
|
ignored_config_keys,
|
|
264
382
|
consumed_config_keys,
|
|
265
383
|
section_path,
|
|
384
|
+
reuse_state,
|
|
385
|
+
allow_reuse=allow_reuse,
|
|
386
|
+
explicit_source=True,
|
|
266
387
|
)
|
|
267
388
|
)
|
|
268
389
|
_record_ignored_section_keys(
|
|
@@ -278,6 +399,9 @@ def _consume_section_schema(
|
|
|
278
399
|
ignored_config_keys,
|
|
279
400
|
consumed_config_keys,
|
|
280
401
|
section_path,
|
|
402
|
+
reuse_state,
|
|
403
|
+
allow_reuse=allow_reuse,
|
|
404
|
+
explicit_source=explicit_source,
|
|
281
405
|
)
|
|
282
406
|
)
|
|
283
407
|
return nested_data
|
|
@@ -288,7 +412,13 @@ def _mark_consumed(
|
|
|
288
412
|
path_prefix: str,
|
|
289
413
|
name: str,
|
|
290
414
|
) -> None:
|
|
291
|
-
"""Record that a caller-supplied
|
|
415
|
+
"""Record that a caller-supplied value was used, fresh or resubmitted.
|
|
416
|
+
|
|
417
|
+
Values the step's own schema supplies — section defaults, suggestions,
|
|
418
|
+
constants — are never marked: they are HA's data, and marking them would
|
|
419
|
+
let a config of nothing but misspelled keys look partially applied to
|
|
420
|
+
:func:`_finish_flow_entry`.
|
|
421
|
+
"""
|
|
292
422
|
if consumed_config_keys is not None:
|
|
293
423
|
consumed_config_keys.add(_section_path(path_prefix, name))
|
|
294
424
|
|
|
@@ -315,19 +445,202 @@ def _auto_confirm_form_payload(current_step: dict[str, Any]) -> dict[str, Any] |
|
|
|
315
445
|
return {name: True}
|
|
316
446
|
|
|
317
447
|
|
|
448
|
+
def _step_owned_submission_value(field: dict[str, Any]) -> Any:
|
|
449
|
+
"""Return the value a step's own schema supplies for ``field``, else _MISSING_DEFAULT.
|
|
450
|
+
|
|
451
|
+
Deliberately distinct from :func:`_field_default_value`, which answers
|
|
452
|
+
"what would the UI show in this box" and is what seeds a form. This answers
|
|
453
|
+
"what does the step itself say to submit", which is a different question:
|
|
454
|
+
|
|
455
|
+
- ``voluptuous_serialize`` emits ``"default"`` only for an actual
|
|
456
|
+
voluptuous default. HA's edit-style pre-fill
|
|
457
|
+
(``add_suggested_values_to_schema``) copies the marker and overwrites
|
|
458
|
+
only its description, so a marker that already carried a default
|
|
459
|
+
serializes with both keys; the suggestion is the stored current value
|
|
460
|
+
and outranks the static default.
|
|
461
|
+
- A constant field serializes as ``{"type": "constant", "value": X}`` and
|
|
462
|
+
``X`` is the only value it accepts.
|
|
463
|
+
|
|
464
|
+
The bare top-level ``suggested_value`` shape is not something
|
|
465
|
+
``voluptuous_serialize`` produces; it is read defensively alongside the
|
|
466
|
+
nested one.
|
|
467
|
+
"""
|
|
468
|
+
description = field.get("description")
|
|
469
|
+
if isinstance(description, dict) and description.get("suggested_value") is not None:
|
|
470
|
+
return copy.deepcopy(description["suggested_value"])
|
|
471
|
+
if field.get("suggested_value") is not None:
|
|
472
|
+
return copy.deepcopy(field["suggested_value"])
|
|
473
|
+
if field.get("type") == "constant" and field.get("value") is not None:
|
|
474
|
+
return copy.deepcopy(field["value"])
|
|
475
|
+
return _MISSING_DEFAULT
|
|
476
|
+
|
|
477
|
+
|
|
478
|
+
def _redeclared_field_submission(
|
|
479
|
+
field: dict[str, Any],
|
|
480
|
+
name: str,
|
|
481
|
+
path_prefix: str,
|
|
482
|
+
reuse_state: _ReuseState | None,
|
|
483
|
+
allow_reuse: bool,
|
|
484
|
+
) -> tuple[Any, bool]:
|
|
485
|
+
"""Decide what to submit for a declared field the caller named no key for here.
|
|
486
|
+
|
|
487
|
+
Returns ``(value, from_caller)``, or ``_NO_SUBMISSION`` to omit the key
|
|
488
|
+
entirely. A site the caller's own key already filled earlier in this same
|
|
489
|
+
step is left alone — a section dict and a flat key can name the same leaf,
|
|
490
|
+
and the caller's value for this step outranks anything injected. Otherwise,
|
|
491
|
+
in order:
|
|
492
|
+
|
|
493
|
+
1. The field is not required: omit it. Nothing is ever injected into an
|
|
494
|
+
optional field, or into a section that is neither required nor named by
|
|
495
|
+
the caller (``allow_reuse``) — materializing either would invent data.
|
|
496
|
+
2. The step's own schema supplies a value — a suggestion or a constant's
|
|
497
|
+
only legal value, per :func:`_step_owned_submission_value`: submit that.
|
|
498
|
+
It is schema data rather than a caller key, so it is neither marked
|
|
499
|
+
consumed nor warned about. A suggestion outranks a coexisting
|
|
500
|
+
``"default"``: both keys can serialize together, and the suggestion is
|
|
501
|
+
the stored current value while the default is the static schema one —
|
|
502
|
+
omitting would let voluptuous substitute the static value over it.
|
|
503
|
+
3. The field carries a ``"default"`` key (and no value of its own): omit it
|
|
504
|
+
and let voluptuous fill the default in. Key presence is the test, so
|
|
505
|
+
``default: None`` is a default too.
|
|
506
|
+
4. Otherwise the field is required, has no default and has no value of its
|
|
507
|
+
own, which makes omitting it a guaranteed "required key not provided":
|
|
508
|
+
resubmit the value the caller supplied for an earlier step, warn, and
|
|
509
|
+
spend the one write allowed per (step, path).
|
|
510
|
+
|
|
511
|
+
Mutates ``reuse_state`` on the fourth branch. Menu selection keys never
|
|
512
|
+
reach here. Motivating regression (issue #2057): an options flow —
|
|
513
|
+
LocalTuya's — that declares the same field on an early step and again on a
|
|
514
|
+
later one.
|
|
515
|
+
"""
|
|
516
|
+
if reuse_state is None or not allow_reuse:
|
|
517
|
+
return _NO_SUBMISSION
|
|
518
|
+
dotted = _section_path(path_prefix, name)
|
|
519
|
+
if dotted in reuse_state.filled:
|
|
520
|
+
return _NO_SUBMISSION
|
|
521
|
+
if not field.get("required"):
|
|
522
|
+
return _NO_SUBMISSION
|
|
523
|
+
step_owned = _step_owned_submission_value(field)
|
|
524
|
+
if step_owned is not _MISSING_DEFAULT:
|
|
525
|
+
return step_owned, False
|
|
526
|
+
if "default" in field:
|
|
527
|
+
return _NO_SUBMISSION
|
|
528
|
+
recorded = reuse_state.recorded_value(path_prefix, name)
|
|
529
|
+
if recorded is _MISSING_DEFAULT:
|
|
530
|
+
return _NO_SUBMISSION
|
|
531
|
+
if not reuse_state.claim_write(dotted):
|
|
532
|
+
return _NO_SUBMISSION
|
|
533
|
+
return recorded, True
|
|
534
|
+
|
|
535
|
+
|
|
536
|
+
def _consume_leaf_field(
|
|
537
|
+
field: dict[str, Any],
|
|
538
|
+
name: str,
|
|
539
|
+
form_data: dict[str, Any],
|
|
540
|
+
remaining_config: dict[str, Any],
|
|
541
|
+
consumed_config_keys: set[str] | None,
|
|
542
|
+
reuse_state: _ReuseState | None,
|
|
543
|
+
path_prefix: str,
|
|
544
|
+
*,
|
|
545
|
+
allow_reuse: bool = True,
|
|
546
|
+
explicit_source: bool = False,
|
|
547
|
+
) -> None:
|
|
548
|
+
"""Fill ``name`` in ``form_data`` from the caller's config or from the step itself.
|
|
549
|
+
|
|
550
|
+
A key the caller supplied here is popped, submitted, and recorded in
|
|
551
|
+
``reuse_state`` — scoped to this dotted path when it came out of an
|
|
552
|
+
explicitly supplied section dict, keyed by leaf name when it came out of
|
|
553
|
+
the flat caller dict. With no key to pop,
|
|
554
|
+
:func:`_redeclared_field_submission` chooses between omitting the field,
|
|
555
|
+
submitting the step's own value, and resubmitting the recorded one.
|
|
556
|
+
"""
|
|
557
|
+
if name in remaining_config:
|
|
558
|
+
value = remaining_config.pop(name)
|
|
559
|
+
form_data[name] = value
|
|
560
|
+
_mark_consumed(consumed_config_keys, path_prefix, name)
|
|
561
|
+
if reuse_state is not None:
|
|
562
|
+
reuse_state.record(path_prefix, name, value, scoped_only=explicit_source)
|
|
563
|
+
return
|
|
564
|
+
|
|
565
|
+
value, from_caller = _redeclared_field_submission(
|
|
566
|
+
field, name, path_prefix, reuse_state, allow_reuse
|
|
567
|
+
)
|
|
568
|
+
if value is _MISSING_DEFAULT:
|
|
569
|
+
return
|
|
570
|
+
form_data[name] = value
|
|
571
|
+
if from_caller:
|
|
572
|
+
_mark_consumed(consumed_config_keys, path_prefix, name)
|
|
573
|
+
|
|
574
|
+
|
|
575
|
+
def _consume_declared_section(
|
|
576
|
+
field: dict[str, Any],
|
|
577
|
+
form_data: dict[str, Any],
|
|
578
|
+
remaining_config: dict[str, Any],
|
|
579
|
+
ignored_config_keys: set[str] | None,
|
|
580
|
+
consumed_config_keys: set[str] | None,
|
|
581
|
+
path_prefix: str,
|
|
582
|
+
reuse_state: _ReuseState | None,
|
|
583
|
+
*,
|
|
584
|
+
allow_reuse: bool,
|
|
585
|
+
explicit_source: bool,
|
|
586
|
+
) -> None:
|
|
587
|
+
"""Merge one section field's data into ``form_data``.
|
|
588
|
+
|
|
589
|
+
A caller who supplies the section as a non-dict value gets it submitted
|
|
590
|
+
verbatim — HA, not this walker, decides what that means. Reuse is allowed
|
|
591
|
+
inside the section only when HA marks it required or the caller named it,
|
|
592
|
+
so an untouched optional section is never brought into existence.
|
|
593
|
+
"""
|
|
594
|
+
name = field.get("name")
|
|
595
|
+
section_name = name if isinstance(name, str) else None
|
|
596
|
+
explicit_section: dict[str, Any] | None = None
|
|
597
|
+
if section_name is not None and section_name in remaining_config:
|
|
598
|
+
explicit_value = remaining_config.pop(section_name)
|
|
599
|
+
if not isinstance(explicit_value, dict):
|
|
600
|
+
form_data[section_name] = explicit_value
|
|
601
|
+
_mark_consumed(consumed_config_keys, path_prefix, section_name)
|
|
602
|
+
return
|
|
603
|
+
explicit_section = explicit_value
|
|
604
|
+
|
|
605
|
+
nested_data = _consume_section_schema(
|
|
606
|
+
field,
|
|
607
|
+
explicit_section,
|
|
608
|
+
remaining_config,
|
|
609
|
+
ignored_config_keys,
|
|
610
|
+
consumed_config_keys,
|
|
611
|
+
path_prefix,
|
|
612
|
+
reuse_state,
|
|
613
|
+
allow_reuse=allow_reuse
|
|
614
|
+
and (bool(field.get("required")) or explicit_section is not None),
|
|
615
|
+
explicit_source=explicit_source,
|
|
616
|
+
)
|
|
617
|
+
if not nested_data:
|
|
618
|
+
return
|
|
619
|
+
if section_name is not None:
|
|
620
|
+
form_data[section_name] = nested_data
|
|
621
|
+
else:
|
|
622
|
+
form_data.update(nested_data)
|
|
623
|
+
|
|
624
|
+
|
|
318
625
|
def _consume_form_schema(
|
|
319
626
|
data_schema: list[Any],
|
|
320
627
|
remaining_config: dict[str, Any],
|
|
321
628
|
ignored_config_keys: set[str] | None = None,
|
|
322
629
|
consumed_config_keys: set[str] | None = None,
|
|
323
630
|
path_prefix: str = "",
|
|
631
|
+
reuse_state: _ReuseState | None = None,
|
|
632
|
+
*,
|
|
633
|
+
allow_reuse: bool = True,
|
|
634
|
+
explicit_source: bool = False,
|
|
324
635
|
) -> dict[str, Any]:
|
|
325
636
|
"""Consume matching config values and shape nested flow sections.
|
|
326
637
|
|
|
327
638
|
Mutates ``remaining_config`` by removing every consumed key. Flat child
|
|
328
639
|
values override the same value inside an explicitly supplied section dict.
|
|
329
640
|
Unknown keys inside explicit section dicts are added to
|
|
330
|
-
``ignored_config_keys`` with their dotted section path.
|
|
641
|
+
``ignored_config_keys`` with their dotted section path. A declared field the
|
|
642
|
+
caller named no key for is filled per
|
|
643
|
+
:func:`_redeclared_field_submission`.
|
|
331
644
|
"""
|
|
332
645
|
form_data: dict[str, Any] = {}
|
|
333
646
|
|
|
@@ -336,40 +649,32 @@ def _consume_form_schema(
|
|
|
336
649
|
continue
|
|
337
650
|
|
|
338
651
|
name = field.get("name")
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
section_name = name if isinstance(name, str) else None
|
|
342
|
-
explicit_section = None
|
|
343
|
-
if section_name is not None and section_name in remaining_config:
|
|
344
|
-
explicit_value = remaining_config.pop(section_name)
|
|
345
|
-
if not isinstance(explicit_value, dict):
|
|
346
|
-
form_data[section_name] = explicit_value
|
|
347
|
-
_mark_consumed(consumed_config_keys, path_prefix, section_name)
|
|
348
|
-
continue
|
|
349
|
-
explicit_section = explicit_value
|
|
350
|
-
|
|
351
|
-
nested_data = _consume_section_schema(
|
|
652
|
+
if isinstance(field.get("schema"), list):
|
|
653
|
+
_consume_declared_section(
|
|
352
654
|
field,
|
|
353
|
-
|
|
655
|
+
form_data,
|
|
354
656
|
remaining_config,
|
|
355
657
|
ignored_config_keys,
|
|
356
658
|
consumed_config_keys,
|
|
357
659
|
path_prefix,
|
|
660
|
+
reuse_state,
|
|
661
|
+
allow_reuse=allow_reuse,
|
|
662
|
+
explicit_source=explicit_source,
|
|
358
663
|
)
|
|
359
|
-
if nested_data:
|
|
360
|
-
if section_name is not None:
|
|
361
|
-
form_data[section_name] = nested_data
|
|
362
|
-
else:
|
|
363
|
-
form_data.update(nested_data)
|
|
364
664
|
continue
|
|
365
665
|
|
|
366
|
-
if (
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
666
|
+
if isinstance(name, str) and name not in _MENU_SELECTION_KEYS:
|
|
667
|
+
_consume_leaf_field(
|
|
668
|
+
field,
|
|
669
|
+
name,
|
|
670
|
+
form_data,
|
|
671
|
+
remaining_config,
|
|
672
|
+
consumed_config_keys,
|
|
673
|
+
reuse_state,
|
|
674
|
+
path_prefix,
|
|
675
|
+
allow_reuse=allow_reuse,
|
|
676
|
+
explicit_source=explicit_source,
|
|
677
|
+
)
|
|
373
678
|
|
|
374
679
|
return form_data
|
|
375
680
|
|
|
@@ -393,12 +698,38 @@ def _extract_schema_field_names(data_schema: Any) -> set[str] | None:
|
|
|
393
698
|
return names
|
|
394
699
|
|
|
395
700
|
|
|
701
|
+
def _consume_all_remaining_keys(
|
|
702
|
+
remaining_config: dict[str, Any],
|
|
703
|
+
consumed_config_keys: set[str] | None,
|
|
704
|
+
reuse_state: _ReuseState | None,
|
|
705
|
+
) -> dict[str, Any]:
|
|
706
|
+
"""Submit every non-menu key, for a step whose schema HA did not send.
|
|
707
|
+
|
|
708
|
+
Without field names there is nothing to filter on, so the whole config is
|
|
709
|
+
dumped into this one submit and cleared out of ``remaining_config``. Each
|
|
710
|
+
consumed key is still recorded by leaf name, so a later step that *does*
|
|
711
|
+
arrive with a schema declaring one of them can be filled from the record
|
|
712
|
+
rather than submitted without it.
|
|
713
|
+
"""
|
|
714
|
+
form_data: dict[str, Any] = {}
|
|
715
|
+
for key in list(remaining_config.keys()):
|
|
716
|
+
if key in _MENU_SELECTION_KEYS:
|
|
717
|
+
continue
|
|
718
|
+
value = remaining_config.pop(key)
|
|
719
|
+
form_data[key] = value
|
|
720
|
+
_mark_consumed(consumed_config_keys, "", key)
|
|
721
|
+
if reuse_state is not None:
|
|
722
|
+
reuse_state.record("", key, value, scoped_only=False)
|
|
723
|
+
return form_data
|
|
724
|
+
|
|
725
|
+
|
|
396
726
|
def _handle_form_step(
|
|
397
727
|
flow_id: str,
|
|
398
728
|
current_step: dict[str, Any],
|
|
399
729
|
remaining_config: dict[str, Any],
|
|
400
730
|
ignored_config_keys: set[str] | None = None,
|
|
401
731
|
consumed_config_keys: set[str] | None = None,
|
|
732
|
+
reuse_state: _ReuseState | None = None,
|
|
402
733
|
) -> dict[str, Any]:
|
|
403
734
|
"""Validate a form step and return form data to submit.
|
|
404
735
|
|
|
@@ -408,6 +739,15 @@ def _handle_form_step(
|
|
|
408
739
|
submitted. Fields declared inside a section are grouped under the section
|
|
409
740
|
key; callers may provide them flat or inside an explicit section dict.
|
|
410
741
|
|
|
742
|
+
Caller-supplied values are recorded in ``reuse_state``. A field this step
|
|
743
|
+
declares but the caller named no key for here is filled per
|
|
744
|
+
:func:`_redeclared_field_submission`: omitted when the schema carries a
|
|
745
|
+
``"default"`` or the field is optional, submitted from the step's own
|
|
746
|
+
suggestion or constant, and otherwise — required, no default, no value of
|
|
747
|
+
its own — resubmitted once from an earlier step's caller value with a
|
|
748
|
+
warning. Nothing is injected into an optional field, or into a section
|
|
749
|
+
neither marked required nor named by the caller.
|
|
750
|
+
|
|
411
751
|
When ``data_schema`` is absent (HA didn't tell us field names), falls
|
|
412
752
|
back to legacy behaviour: submit all non-menu keys and clear them. This
|
|
413
753
|
keeps single-step flows working when HA omits the schema.
|
|
@@ -429,24 +769,23 @@ def _handle_form_step(
|
|
|
429
769
|
)
|
|
430
770
|
)
|
|
431
771
|
|
|
432
|
-
|
|
772
|
+
if reuse_state is not None:
|
|
773
|
+
reuse_state.begin_step(current_step.get("step_id"))
|
|
433
774
|
|
|
434
|
-
|
|
775
|
+
data_schema = current_step.get("data_schema")
|
|
435
776
|
if not isinstance(data_schema, list):
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
# re-submit the same data.
|
|
439
|
-
for key in list(remaining_config.keys()):
|
|
440
|
-
if key in _MENU_SELECTION_KEYS:
|
|
441
|
-
continue
|
|
442
|
-
form_data[key] = remaining_config.pop(key)
|
|
443
|
-
_mark_consumed(consumed_config_keys, "", key)
|
|
444
|
-
else:
|
|
445
|
-
form_data = _consume_form_schema(
|
|
446
|
-
data_schema, remaining_config, ignored_config_keys, consumed_config_keys
|
|
777
|
+
return _consume_all_remaining_keys(
|
|
778
|
+
remaining_config, consumed_config_keys, reuse_state
|
|
447
779
|
)
|
|
448
780
|
|
|
449
|
-
return
|
|
781
|
+
return _consume_form_schema(
|
|
782
|
+
data_schema,
|
|
783
|
+
remaining_config,
|
|
784
|
+
ignored_config_keys,
|
|
785
|
+
consumed_config_keys,
|
|
786
|
+
"",
|
|
787
|
+
reuse_state,
|
|
788
|
+
)
|
|
450
789
|
|
|
451
790
|
|
|
452
791
|
def _parse_flow_api_error(
|
|
@@ -738,6 +1077,7 @@ def _finish_flow_entry(
|
|
|
738
1077
|
any_form_key_consumed: bool,
|
|
739
1078
|
ignored_config_keys: set[str],
|
|
740
1079
|
remaining_config: dict[str, Any],
|
|
1080
|
+
reuse_state: _ReuseState,
|
|
741
1081
|
) -> dict[str, Any]:
|
|
742
1082
|
"""Build the CREATE_ENTRY success response, or raise on total key miss.
|
|
743
1083
|
|
|
@@ -770,7 +1110,7 @@ def _finish_flow_entry(
|
|
|
770
1110
|
)
|
|
771
1111
|
)
|
|
772
1112
|
response: dict[str, Any] = {"success": True, "entry": current_step}
|
|
773
|
-
warnings =
|
|
1113
|
+
warnings = _success_warnings(ignored_config_keys, remaining_config, reuse_state)
|
|
774
1114
|
if warnings:
|
|
775
1115
|
response["warnings"] = warnings
|
|
776
1116
|
return response
|
|
@@ -818,8 +1158,13 @@ async def _handle_flow_steps(
|
|
|
818
1158
|
flow_id: Flow ID from start_config_flow or start_options_flow
|
|
819
1159
|
initial_step: The first step returned by the flow start call
|
|
820
1160
|
config: Full caller-provided config dict. Menu selection keys are
|
|
821
|
-
consumed by menu steps; remaining keys are submitted on the
|
|
822
|
-
|
|
1161
|
+
consumed by menu steps; remaining keys are submitted on the first
|
|
1162
|
+
form step whose schema declares them or, when HA omits the schema,
|
|
1163
|
+
on the first form step outright. A later step that redeclares an
|
|
1164
|
+
already-consumed field gets that value resubmitted once, with a
|
|
1165
|
+
warning, where the step marks it required and supplies neither a
|
|
1166
|
+
default nor a value of its own (see
|
|
1167
|
+
:func:`_redeclared_field_submission`).
|
|
823
1168
|
submit_fn: Async function to submit a step. Defaults to
|
|
824
1169
|
client.submit_config_flow_step (create). Pass
|
|
825
1170
|
client.submit_options_flow_step for options (update) flows.
|
|
@@ -830,8 +1175,9 @@ async def _handle_flow_steps(
|
|
|
830
1175
|
Returns:
|
|
831
1176
|
``{"success": True, "entry": result}`` on success, plus ``warnings``
|
|
832
1177
|
when SOME caller-supplied config keys were not declared by any flow
|
|
833
|
-
step
|
|
834
|
-
|
|
1178
|
+
step, or when a key had to be resubmitted to a later step that
|
|
1179
|
+
redeclared it. When the flow presented at least one form step and NONE
|
|
1180
|
+
of the supplied keys were consumed, raises ``VALIDATION_INVALID_PARAMETER``
|
|
835
1181
|
instead of reporting a misleading success — the flow completed on
|
|
836
1182
|
empty forms (defaults), applying nothing the caller asked for (see
|
|
837
1183
|
:func:`_finish_flow_entry`). Raises ToolError on any failure.
|
|
@@ -842,6 +1188,7 @@ async def _handle_flow_steps(
|
|
|
842
1188
|
current_step = initial_step
|
|
843
1189
|
last_menu_choice: str | None = None
|
|
844
1190
|
ignored_config_keys: set[str] = set()
|
|
1191
|
+
reuse_state = _ReuseState()
|
|
845
1192
|
supplied_keys = sorted(k for k in config if k not in _MENU_SELECTION_KEYS)
|
|
846
1193
|
saw_form_step = False
|
|
847
1194
|
any_form_key_consumed = False
|
|
@@ -859,6 +1206,7 @@ async def _handle_flow_steps(
|
|
|
859
1206
|
any_form_key_consumed=any_form_key_consumed,
|
|
860
1207
|
ignored_config_keys=ignored_config_keys,
|
|
861
1208
|
remaining_config=remaining_config,
|
|
1209
|
+
reuse_state=reuse_state,
|
|
862
1210
|
)
|
|
863
1211
|
|
|
864
1212
|
if result_type == _FlowType.ABORT:
|
|
@@ -885,7 +1233,9 @@ async def _handle_flow_steps(
|
|
|
885
1233
|
# _handle_form_step pops only the keys declared in the current
|
|
886
1234
|
# step's data_schema, leaving any other keys in remaining_config
|
|
887
1235
|
# for subsequent steps (HA can present multi-step forms, e.g.
|
|
888
|
-
# statistics: user step then pick-characteristic step)
|
|
1236
|
+
# statistics: user step then pick-characteristic step), and records
|
|
1237
|
+
# what it consumed for any later step that redeclares the same
|
|
1238
|
+
# field.
|
|
889
1239
|
saw_form_step = True
|
|
890
1240
|
consumed_form_keys: set[str] = set()
|
|
891
1241
|
form_data = _auto_confirm_form_payload(current_step)
|
|
@@ -896,6 +1246,7 @@ async def _handle_flow_steps(
|
|
|
896
1246
|
remaining_config,
|
|
897
1247
|
ignored_config_keys,
|
|
898
1248
|
consumed_form_keys,
|
|
1249
|
+
reuse_state,
|
|
899
1250
|
)
|
|
900
1251
|
if consumed_form_keys:
|
|
901
1252
|
any_form_key_consumed = True
|
|
@@ -957,12 +1308,15 @@ async def _handle_config_subentry_flow_steps(
|
|
|
957
1308
|
"""Walk a config subentry flow and accept HA's reconfigure-success abort.
|
|
958
1309
|
|
|
959
1310
|
Successful results include ``warnings`` when caller-supplied config keys
|
|
960
|
-
were not declared by any flow step
|
|
1311
|
+
were not declared by any flow step, or when a key was resubmitted to a
|
|
1312
|
+
later step that redeclared it. Fields redeclared by a later step are filled
|
|
1313
|
+
on the same terms as :func:`_handle_flow_steps`.
|
|
961
1314
|
"""
|
|
962
1315
|
remaining_config = dict(config)
|
|
963
1316
|
current_step = initial_step
|
|
964
1317
|
last_menu_choice: str | None = None
|
|
965
1318
|
ignored_config_keys: set[str] = set()
|
|
1319
|
+
reuse_state = _ReuseState()
|
|
966
1320
|
max_steps = 10
|
|
967
1321
|
|
|
968
1322
|
for step_num in range(max_steps):
|
|
@@ -974,7 +1328,9 @@ async def _handle_config_subentry_flow_steps(
|
|
|
974
1328
|
"operation": "created",
|
|
975
1329
|
"flow_result": current_step,
|
|
976
1330
|
}
|
|
977
|
-
warnings =
|
|
1331
|
+
warnings = _success_warnings(
|
|
1332
|
+
ignored_config_keys, remaining_config, reuse_state
|
|
1333
|
+
)
|
|
978
1334
|
if warnings:
|
|
979
1335
|
response["warnings"] = warnings
|
|
980
1336
|
return response
|
|
@@ -987,7 +1343,9 @@ async def _handle_config_subentry_flow_steps(
|
|
|
987
1343
|
"operation": "reconfigured",
|
|
988
1344
|
"flow_result": current_step,
|
|
989
1345
|
}
|
|
990
|
-
warnings =
|
|
1346
|
+
warnings = _success_warnings(
|
|
1347
|
+
ignored_config_keys, remaining_config, reuse_state
|
|
1348
|
+
)
|
|
991
1349
|
if warnings:
|
|
992
1350
|
response["warnings"] = warnings
|
|
993
1351
|
return response
|
|
@@ -1025,6 +1383,7 @@ async def _handle_config_subentry_flow_steps(
|
|
|
1025
1383
|
current_step,
|
|
1026
1384
|
remaining_config,
|
|
1027
1385
|
ignored_config_keys,
|
|
1386
|
+
reuse_state=reuse_state,
|
|
1028
1387
|
)
|
|
1029
1388
|
logger.debug(
|
|
1030
1389
|
"Config subentry flow step %s: form submit (step_id=%s, keys=%s)",
|