ha-mcp-dev 7.12.0.dev1835__tar.gz → 7.12.0.dev1839__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.12.0.dev1835/src/ha_mcp_dev.egg-info → ha_mcp_dev-7.12.0.dev1839}/PKG-INFO +1 -1
- {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/pyproject.toml +1 -9
- {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/tools/best_practice_checker.py +201 -106
- {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/tools/tools_calendar.py +116 -80
- {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/tools/tools_mcp_component.py +80 -67
- {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/tools/tools_resources.py +42 -36
- {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/tools/tools_search.py +76 -41
- {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/tools/tools_service.py +100 -48
- {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/tools/tools_utility.py +138 -94
- {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/tools/util_helpers.py +80 -58
- {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839/src/ha_mcp_dev.egg-info}/PKG-INFO +1 -1
- {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/LICENSE +0 -0
- {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/MANIFEST.in +0 -0
- {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/README.md +0 -0
- {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/setup.cfg +0 -0
- {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/__init__.py +0 -0
- {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/__main__.py +0 -0
- {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/_pypi_marker +0 -0
- {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/_version.py +0 -0
- {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/auth/__init__.py +0 -0
- {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/auth/consent_form.py +0 -0
- {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/auth/provider.py +0 -0
- {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/backup_manager.py +0 -0
- {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/browser_landing.py +0 -0
- {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/client/__init__.py +0 -0
- {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/client/rest_client.py +0 -0
- {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/client/supervisor_client.py +0 -0
- {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/client/websocket_client.py +0 -0
- {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/client/websocket_listener.py +0 -0
- {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/config.py +0 -0
- {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/dashboard_screenshot/__init__.py +0 -0
- {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/dashboard_screenshot/capture.py +0 -0
- {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/dashboard_screenshot/provision.py +0 -0
- {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/errors.py +0 -0
- {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/policy/__init__.py +0 -0
- {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/policy/approval_queue.py +0 -0
- {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/policy/evaluator.py +0 -0
- {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/policy/handlers.py +0 -0
- {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/policy/middleware.py +0 -0
- {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/policy/model.py +0 -0
- {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/policy/persistence.py +0 -0
- {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/policy/value_sources.py +0 -0
- {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/py.typed +0 -0
- {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/read_only.py +0 -0
- {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/resources/skills-vendor/.claude/settings.json +0 -0
- {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/resources/skills-vendor/.claude-plugin/marketplace.json +0 -0
- {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/resources/skills-vendor/.claude-plugin/plugin.json +0 -0
- {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/resources/skills-vendor/.github/ISSUE_TEMPLATE/skill-rca.md +0 -0
- {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/resources/skills-vendor/.github/pull_request_template.md +0 -0
- {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/resources/skills-vendor/AGENTS.md +0 -0
- {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/resources/skills-vendor/CLAUDE.md +0 -0
- {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/resources/skills-vendor/CONTRIBUTING.md +0 -0
- {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/resources/skills-vendor/LICENSE +0 -0
- {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/resources/skills-vendor/README.md +0 -0
- {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/SKILL.md +0 -0
- {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/evals/evals.json +0 -0
- {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/appdaemon.md +0 -0
- {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/automation-patterns.md +0 -0
- {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/blueprint-guide.md +0 -0
- {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-cards.md +0 -0
- {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-guide.md +0 -0
- {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/device-control.md +0 -0
- {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/domain-docs.md +0 -0
- {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/examples.yaml +0 -0
- {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/helper-selection.md +0 -0
- {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/safe-refactoring.md +0 -0
- {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/scenes.md +0 -0
- {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/template-guidelines.md +0 -0
- {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/yaml-only-integrations.md +0 -0
- {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/server.py +0 -0
- {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/settings_ui/__init__.py +0 -0
- {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/settings_ui/settings.css +0 -0
- {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/settings_ui/settings.html +0 -0
- {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/settings_ui/settings.js +0 -0
- {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/smoke_test.py +0 -0
- {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/stdio_settings_sidecar.py +0 -0
- {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/tools/__init__.py +0 -0
- {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/tools/auto_backup.py +0 -0
- {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/tools/backup.py +0 -0
- {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/tools/config_entry_flow.py +0 -0
- {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/tools/device_control.py +0 -0
- {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/tools/enhanced.py +0 -0
- {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/tools/helpers.py +0 -0
- {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/tools/radio/__init__.py +0 -0
- {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/tools/radio/base.py +0 -0
- {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/tools/radio/matter.py +0 -0
- {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/tools/radio/thread.py +0 -0
- {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/tools/radio/zigbee.py +0 -0
- {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/tools/radio/zwave.py +0 -0
- {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/tools/reference_validator.py +0 -0
- {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/tools/registry.py +0 -0
- {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/tools/smart_search/__init__.py +0 -0
- {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/tools/smart_search/_base.py +0 -0
- {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/tools/smart_search/_config.py +0 -0
- {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/tools/smart_search/_deep.py +0 -0
- {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/tools/smart_search/_entities.py +0 -0
- {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/tools/smart_search/_fetch.py +0 -0
- {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/tools/smart_search/_overview.py +0 -0
- {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/tools/smart_search/_scenes.py +0 -0
- {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/tools/smart_search/_scoring.py +0 -0
- {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/tools/tool_search_hint_middleware.py +0 -0
- {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/tools/tools_addons.py +0 -0
- {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/tools/tools_areas.py +0 -0
- {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/tools/tools_blueprints.py +0 -0
- {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/tools/tools_bug_report.py +0 -0
- {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/tools/tools_camera.py +0 -0
- {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/tools/tools_categories.py +0 -0
- {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/tools/tools_code.py +0 -0
- {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/tools/tools_config_automations.py +0 -0
- {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/tools/tools_config_dashboards.py +0 -0
- {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/tools/tools_config_helpers.py +0 -0
- {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/tools/tools_config_scenes.py +0 -0
- {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/tools/tools_config_scripts.py +0 -0
- {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/tools/tools_dashboard_screenshot.py +0 -0
- {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/tools/tools_dev.py +0 -0
- {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/tools/tools_energy.py +0 -0
- {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/tools/tools_entities.py +0 -0
- {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/tools/tools_filesystem.py +0 -0
- {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/tools/tools_groups.py +0 -0
- {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/tools/tools_hacs.py +0 -0
- {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/tools/tools_history.py +0 -0
- {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/tools/tools_integrations.py +0 -0
- {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/tools/tools_labels.py +0 -0
- {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/tools/tools_radio.py +0 -0
- {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/tools/tools_registry.py +0 -0
- {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/tools/tools_services.py +0 -0
- {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/tools/tools_system.py +0 -0
- {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/tools/tools_themes.py +0 -0
- {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/tools/tools_todo.py +0 -0
- {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/tools/tools_traces.py +0 -0
- {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/tools/tools_updates.py +0 -0
- {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/tools/tools_voice_assistant.py +0 -0
- {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/tools/tools_yaml_config.py +0 -0
- {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/tools/tools_zones.py +0 -0
- {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/tools/validation_middleware.py +0 -0
- {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/transforms/__init__.py +0 -0
- {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/transforms/categorized_search.py +0 -0
- {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/transforms/lite_docstrings.py +0 -0
- {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/transport_security.py +0 -0
- {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/update_check.py +0 -0
- {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/utils/__init__.py +0 -0
- {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/utils/config_hash.py +0 -0
- {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/utils/data_paths.py +0 -0
- {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/utils/domain_handlers.py +0 -0
- {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/utils/fuzzy_search.py +0 -0
- {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/utils/kill_signal_diagnostics.py +0 -0
- {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/utils/operation_manager.py +0 -0
- {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/utils/python_sandbox.py +0 -0
- {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/utils/skill_loader.py +0 -0
- {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/utils/usage_logger.py +0 -0
- {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/visibility/__init__.py +0 -0
- {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/visibility/model.py +0 -0
- {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/visibility/persistence.py +0 -0
- {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/visibility/resolver.py +0 -0
- {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp_dev.egg-info/SOURCES.txt +0 -0
- {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp_dev.egg-info/dependency_links.txt +0 -0
- {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp_dev.egg-info/entry_points.txt +0 -0
- {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp_dev.egg-info/requires.txt +0 -0
- {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp_dev.egg-info/top_level.txt +0 -0
- {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/tests/__init__.py +0 -0
- {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/tests/test_constants.py +0 -0
- {ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/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.12.0.
|
|
7
|
+
version = "7.12.0.dev1839"
|
|
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
|
|
@@ -181,14 +181,12 @@ ignore = [
|
|
|
181
181
|
"src/ha_mcp/settings_ui/__init__.py" = ["C901"]
|
|
182
182
|
"src/ha_mcp/tools/auto_backup.py" = ["C901"]
|
|
183
183
|
"src/ha_mcp/tools/backup.py" = ["C901"]
|
|
184
|
-
"src/ha_mcp/tools/best_practice_checker.py" = ["C901"]
|
|
185
184
|
"src/ha_mcp/tools/radio/matter.py" = ["C901"]
|
|
186
185
|
"src/ha_mcp/tools/radio/zigbee.py" = ["C901"]
|
|
187
186
|
"src/ha_mcp/tools/radio/zwave.py" = ["C901"]
|
|
188
187
|
"src/ha_mcp/tools/reference_validator.py" = ["C901"]
|
|
189
188
|
"src/ha_mcp/tools/tools_addons.py" = ["C901"]
|
|
190
189
|
"src/ha_mcp/tools/tools_areas.py" = ["C901"]
|
|
191
|
-
"src/ha_mcp/tools/tools_calendar.py" = ["C901"]
|
|
192
190
|
"src/ha_mcp/tools/tools_code.py" = ["C901"]
|
|
193
191
|
"src/ha_mcp/tools/tools_config_automations.py" = ["C901"]
|
|
194
192
|
"src/ha_mcp/tools/tools_config_scenes.py" = ["C901"]
|
|
@@ -198,17 +196,11 @@ ignore = [
|
|
|
198
196
|
"src/ha_mcp/tools/tools_hacs.py" = ["C901"]
|
|
199
197
|
"src/ha_mcp/tools/tools_history.py" = ["C901"]
|
|
200
198
|
"src/ha_mcp/tools/tools_integrations.py" = ["C901"]
|
|
201
|
-
"src/ha_mcp/tools/tools_mcp_component.py" = ["C901"]
|
|
202
|
-
"src/ha_mcp/tools/tools_resources.py" = ["C901"]
|
|
203
|
-
"src/ha_mcp/tools/tools_search.py" = ["C901"]
|
|
204
|
-
"src/ha_mcp/tools/tools_service.py" = ["C901"]
|
|
205
199
|
"src/ha_mcp/tools/tools_system.py" = ["C901"]
|
|
206
200
|
"src/ha_mcp/tools/tools_traces.py" = ["C901"]
|
|
207
201
|
"src/ha_mcp/tools/tools_updates.py" = ["C901"]
|
|
208
|
-
"src/ha_mcp/tools/tools_utility.py" = ["C901"]
|
|
209
202
|
"src/ha_mcp/tools/tools_yaml_config.py" = ["C901"]
|
|
210
203
|
"src/ha_mcp/tools/tools_zones.py" = ["C901"]
|
|
211
|
-
"src/ha_mcp/tools/util_helpers.py" = ["C901"]
|
|
212
204
|
"src/ha_mcp/transforms/categorized_search.py" = ["C901"]
|
|
213
205
|
"src/ha_mcp/utils/data_paths.py" = ["C901"]
|
|
214
206
|
"src/ha_mcp/utils/fuzzy_search.py" = ["C901"]
|
{ha_mcp_dev-7.12.0.dev1835 → ha_mcp_dev-7.12.0.dev1839}/src/ha_mcp/tools/best_practice_checker.py
RENAMED
|
@@ -388,6 +388,11 @@ def _check_template_string(
|
|
|
388
388
|
prefix AND the suggestion text adapt if a future caller passes "trigger".
|
|
389
389
|
The native shapes named here (numeric_state, state, time, sun) work as
|
|
390
390
|
both conditions and triggers in HA — only the noun changes.
|
|
391
|
+
|
|
392
|
+
Detection is split across three helpers (comparison/sun/is_state, time
|
|
393
|
+
patterns, then state-list/direct-state/duration) purely to keep each
|
|
394
|
+
function's branching low. They run in the same order as before and
|
|
395
|
+
share ``duration_match`` so the split is invisible to callers.
|
|
391
396
|
"""
|
|
392
397
|
initial_count = len(warnings)
|
|
393
398
|
label = position.capitalize()
|
|
@@ -400,6 +405,44 @@ def _check_template_string(
|
|
|
400
405
|
# fires instead).
|
|
401
406
|
duration_match = _RE_DURATION_MATH.search(template)
|
|
402
407
|
|
|
408
|
+
_check_template_comparison_patterns(
|
|
409
|
+
template, warnings, skill_prefix, position, label, duration_match
|
|
410
|
+
)
|
|
411
|
+
_check_template_time_patterns(template, warnings, skill_prefix, position, label)
|
|
412
|
+
_check_template_state_and_duration_patterns(
|
|
413
|
+
template, warnings, skill_prefix, position, label, duration_match
|
|
414
|
+
)
|
|
415
|
+
|
|
416
|
+
# Generic fallback: any Jinja in this logic position that didn't match
|
|
417
|
+
# a specific detector. Catches new anti-patterns (issue #1011) and
|
|
418
|
+
# reframes #695 from "enumerate bad shapes" to "surface every template
|
|
419
|
+
# in a logic position". Specific detectors above keep their tailored
|
|
420
|
+
# messages.
|
|
421
|
+
if len(warnings) == initial_count and _RE_ANY_TEMPLATE.search(template):
|
|
422
|
+
_emit(
|
|
423
|
+
warnings,
|
|
424
|
+
f"Template detected in {position} — if this maps to a native option "
|
|
425
|
+
"(`numeric_state`, `state`, `time`, `sun`, `zone`, `device`), use that "
|
|
426
|
+
"instead. Templates fail silently at runtime and bypass schema validation.",
|
|
427
|
+
skill_prefix,
|
|
428
|
+
"template-guidelines.md#when-to-avoid-templates",
|
|
429
|
+
)
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
def _check_template_comparison_patterns(
|
|
433
|
+
template: str,
|
|
434
|
+
warnings: BestPracticeCheckResult,
|
|
435
|
+
skill_prefix: str | None,
|
|
436
|
+
position: str,
|
|
437
|
+
label: str,
|
|
438
|
+
duration_match: re.Match[str] | None,
|
|
439
|
+
) -> None:
|
|
440
|
+
"""Flag numeric-comparison, `sun.sun`, and `is_state()` template shapes.
|
|
441
|
+
|
|
442
|
+
Split out of :func:`_check_template_string` to keep its complexity low.
|
|
443
|
+
``duration_match`` is precomputed by the caller so the numeric-comparison
|
|
444
|
+
check can suppress its suggestion when duration math is also present.
|
|
445
|
+
"""
|
|
403
446
|
if _RE_NUMERIC_CMP.search(template) and not duration_match:
|
|
404
447
|
_emit(
|
|
405
448
|
warnings,
|
|
@@ -429,6 +472,19 @@ def _check_template_string(
|
|
|
429
472
|
skill_prefix,
|
|
430
473
|
"automation-patterns.md#native-conditions",
|
|
431
474
|
)
|
|
475
|
+
|
|
476
|
+
|
|
477
|
+
def _check_template_time_patterns(
|
|
478
|
+
template: str,
|
|
479
|
+
warnings: BestPracticeCheckResult,
|
|
480
|
+
skill_prefix: str | None,
|
|
481
|
+
position: str,
|
|
482
|
+
label: str,
|
|
483
|
+
) -> None:
|
|
484
|
+
"""Flag `now().hour/minute`, weekday, and date-based template shapes.
|
|
485
|
+
|
|
486
|
+
Split out of :func:`_check_template_string` to keep its complexity low.
|
|
487
|
+
"""
|
|
432
488
|
if _RE_NOW_TIME.search(template):
|
|
433
489
|
_emit(
|
|
434
490
|
warnings,
|
|
@@ -458,6 +514,21 @@ def _check_template_string(
|
|
|
458
514
|
skill_prefix,
|
|
459
515
|
"automation-patterns.md#native-conditions",
|
|
460
516
|
)
|
|
517
|
+
|
|
518
|
+
|
|
519
|
+
def _check_template_state_and_duration_patterns(
|
|
520
|
+
template: str,
|
|
521
|
+
warnings: BestPracticeCheckResult,
|
|
522
|
+
skill_prefix: str | None,
|
|
523
|
+
position: str,
|
|
524
|
+
label: str,
|
|
525
|
+
duration_match: re.Match[str] | None,
|
|
526
|
+
) -> None:
|
|
527
|
+
"""Flag `states(...) in [...]`, direct-state-access, and duration-math shapes.
|
|
528
|
+
|
|
529
|
+
Split out of :func:`_check_template_string` to keep its complexity low.
|
|
530
|
+
``duration_match`` is precomputed by the caller.
|
|
531
|
+
"""
|
|
461
532
|
if _RE_STATE_IN.search(template):
|
|
462
533
|
_emit(
|
|
463
534
|
warnings,
|
|
@@ -489,21 +560,6 @@ def _check_template_string(
|
|
|
489
560
|
"automation-patterns.md#native-conditions",
|
|
490
561
|
)
|
|
491
562
|
|
|
492
|
-
# Generic fallback: any Jinja in this logic position that didn't match
|
|
493
|
-
# a specific detector. Catches new anti-patterns (issue #1011) and
|
|
494
|
-
# reframes #695 from "enumerate bad shapes" to "surface every template
|
|
495
|
-
# in a logic position". Specific detectors above keep their tailored
|
|
496
|
-
# messages.
|
|
497
|
-
if len(warnings) == initial_count and _RE_ANY_TEMPLATE.search(template):
|
|
498
|
-
_emit(
|
|
499
|
-
warnings,
|
|
500
|
-
f"Template detected in {position} — if this maps to a native option "
|
|
501
|
-
"(`numeric_state`, `state`, `time`, `sun`, `zone`, `device`), use that "
|
|
502
|
-
"instead. Templates fail silently at runtime and bypass schema validation.",
|
|
503
|
-
skill_prefix,
|
|
504
|
-
"template-guidelines.md#when-to-avoid-templates",
|
|
505
|
-
)
|
|
506
|
-
|
|
507
563
|
|
|
508
564
|
# ---------------------------------------------------------------------------
|
|
509
565
|
# Action tree checks
|
|
@@ -687,42 +743,21 @@ def _check_target_dict(
|
|
|
687
743
|
def _check_triggers(
|
|
688
744
|
triggers: Any, warnings: BestPracticeCheckResult, skill_prefix: str | None
|
|
689
745
|
) -> None:
|
|
690
|
-
"""Check triggers for device_id and template anti-patterns.
|
|
746
|
+
"""Check triggers for device_id and template anti-patterns.
|
|
747
|
+
|
|
748
|
+
Per-trigger detection is split into focused helpers (renamed-key check,
|
|
749
|
+
deprecated-behavior check, template-trigger check, numeric_state-trigger
|
|
750
|
+
check) purely to keep this function's complexity low. They run in the
|
|
751
|
+
same order as before.
|
|
752
|
+
"""
|
|
691
753
|
for trigger in _as_list(triggers):
|
|
692
754
|
if not isinstance(trigger, dict):
|
|
693
755
|
continue
|
|
694
756
|
|
|
695
757
|
platform = trigger.get("platform", trigger.get("trigger", ""))
|
|
696
758
|
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
_RENAMED_TRIGGER_KEYS.get(platform) if isinstance(platform, str) else None
|
|
700
|
-
)
|
|
701
|
-
if renamed_trigger:
|
|
702
|
-
_emit(
|
|
703
|
-
warnings,
|
|
704
|
-
f"Trigger key `{platform}` was renamed to `{renamed_trigger}` "
|
|
705
|
-
"in HA 2026.7 and the old key no longer loads — use "
|
|
706
|
-
f"`trigger: {renamed_trigger}`.",
|
|
707
|
-
skill_prefix,
|
|
708
|
-
"automation-patterns.md#trigger-types",
|
|
709
|
-
)
|
|
710
|
-
|
|
711
|
-
# 2026.7 renamed trigger `options.behavior` values (any→each, last→all).
|
|
712
|
-
options = trigger.get("options")
|
|
713
|
-
if isinstance(options, dict):
|
|
714
|
-
behavior = options.get("behavior")
|
|
715
|
-
if isinstance(behavior, str) and behavior in _DEPRECATED_TRIGGER_BEHAVIOR:
|
|
716
|
-
_emit(
|
|
717
|
-
warnings,
|
|
718
|
-
f"Trigger `options.behavior: {behavior}` was renamed to "
|
|
719
|
-
f"`{_DEPRECATED_TRIGGER_BEHAVIOR[behavior]}` in HA 2026.7 — "
|
|
720
|
-
"the old value still loads but raises a repair issue and "
|
|
721
|
-
"will be removed. Valid trigger values: `each`, `first`, "
|
|
722
|
-
"`all` (conditions keep `any`/`all`).",
|
|
723
|
-
skill_prefix,
|
|
724
|
-
"automation-patterns.md#trigger-types",
|
|
725
|
-
)
|
|
759
|
+
_check_renamed_trigger_key(platform, warnings, skill_prefix)
|
|
760
|
+
_check_deprecated_trigger_behavior(trigger, warnings, skill_prefix)
|
|
726
761
|
|
|
727
762
|
# Device trigger → prefer entity_id-based triggers
|
|
728
763
|
if platform == "device":
|
|
@@ -739,71 +774,131 @@ def _check_triggers(
|
|
|
739
774
|
|
|
740
775
|
# Template trigger — specific shapes first, generic fallback after.
|
|
741
776
|
if platform == "template":
|
|
742
|
-
|
|
743
|
-
if isinstance(vt, str):
|
|
744
|
-
initial = len(warnings)
|
|
745
|
-
# See `_check_template_string`: duration math also trips the numeric
|
|
746
|
-
# comparison detector, but maps to `for:`, not `numeric_state`. Suppress
|
|
747
|
-
# the numeric_state suggestion when duration math is present.
|
|
748
|
-
duration_match = _RE_DURATION_MATH.search(vt)
|
|
749
|
-
if _RE_NUMERIC_CMP.search(vt) and not duration_match:
|
|
750
|
-
_emit(
|
|
751
|
-
warnings,
|
|
752
|
-
"Trigger uses template with float/int comparison — "
|
|
753
|
-
"use native `numeric_state` trigger instead "
|
|
754
|
-
"(e.g., `platform: numeric_state, entity_id: sensor.temp, above: 30`).",
|
|
755
|
-
skill_prefix,
|
|
756
|
-
"automation-patterns.md#trigger-types",
|
|
757
|
-
)
|
|
758
|
-
if _RE_IS_STATE.search(vt):
|
|
759
|
-
_emit(
|
|
760
|
-
warnings,
|
|
761
|
-
"Trigger uses template with `is_state()` — use "
|
|
762
|
-
"native `state` trigger instead "
|
|
763
|
-
"(e.g., `platform: state, entity_id: light.x, to: 'on'`).",
|
|
764
|
-
skill_prefix,
|
|
765
|
-
"automation-patterns.md#trigger-types",
|
|
766
|
-
)
|
|
767
|
-
if duration_match:
|
|
768
|
-
_emit(
|
|
769
|
-
warnings,
|
|
770
|
-
"Trigger uses template for duration/recency check "
|
|
771
|
-
"(`now() - X.last_changed/last_updated`) — use the native "
|
|
772
|
-
"`for:` field on a `state` trigger instead "
|
|
773
|
-
"(e.g., `platform: state, entity_id: binary_sensor.motion, "
|
|
774
|
-
"to: 'off', for: {minutes: 5}`). Native `for:` is event-driven "
|
|
775
|
-
"and doesn't re-evaluate on every state change.",
|
|
776
|
-
skill_prefix,
|
|
777
|
-
"automation-patterns.md#trigger-types",
|
|
778
|
-
)
|
|
779
|
-
# Generic fallback for unmatched template triggers.
|
|
780
|
-
if len(warnings) == initial and _RE_ANY_TEMPLATE.search(vt):
|
|
781
|
-
_emit(
|
|
782
|
-
warnings,
|
|
783
|
-
"Trigger uses `template` platform — if this maps to a native option "
|
|
784
|
-
"(`state`, `numeric_state`, `time`, `time_pattern`, `sun`, `zone`, "
|
|
785
|
-
"`event`), use that instead. Native triggers are event-driven; "
|
|
786
|
-
"template triggers re-evaluate on every state change.",
|
|
787
|
-
skill_prefix,
|
|
788
|
-
"automation-patterns.md#trigger-types",
|
|
789
|
-
)
|
|
777
|
+
_check_template_trigger(trigger, warnings, skill_prefix)
|
|
790
778
|
|
|
791
779
|
# numeric_state trigger: value_template can also contain duration math
|
|
792
780
|
# (e.g. transforming last_changed into a seconds value for the threshold).
|
|
793
781
|
if platform == "numeric_state":
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
782
|
+
_check_numeric_state_trigger(trigger, warnings, skill_prefix)
|
|
783
|
+
|
|
784
|
+
|
|
785
|
+
def _check_renamed_trigger_key(
|
|
786
|
+
platform: Any, warnings: BestPracticeCheckResult, skill_prefix: str | None
|
|
787
|
+
) -> None:
|
|
788
|
+
"""Flag HA 2026.7 purpose-specific trigger key renames (old keys no longer load)."""
|
|
789
|
+
renamed_trigger = (
|
|
790
|
+
_RENAMED_TRIGGER_KEYS.get(platform) if isinstance(platform, str) else None
|
|
791
|
+
)
|
|
792
|
+
if renamed_trigger:
|
|
793
|
+
_emit(
|
|
794
|
+
warnings,
|
|
795
|
+
f"Trigger key `{platform}` was renamed to `{renamed_trigger}` "
|
|
796
|
+
"in HA 2026.7 and the old key no longer loads — use "
|
|
797
|
+
f"`trigger: {renamed_trigger}`.",
|
|
798
|
+
skill_prefix,
|
|
799
|
+
"automation-patterns.md#trigger-types",
|
|
800
|
+
)
|
|
801
|
+
|
|
802
|
+
|
|
803
|
+
def _check_deprecated_trigger_behavior(
|
|
804
|
+
trigger: dict[str, Any], warnings: BestPracticeCheckResult, skill_prefix: str | None
|
|
805
|
+
) -> None:
|
|
806
|
+
"""Flag HA 2026.7 trigger `options.behavior` value renames (any→each, last→all)."""
|
|
807
|
+
options = trigger.get("options")
|
|
808
|
+
if not isinstance(options, dict):
|
|
809
|
+
return
|
|
810
|
+
behavior = options.get("behavior")
|
|
811
|
+
if isinstance(behavior, str) and behavior in _DEPRECATED_TRIGGER_BEHAVIOR:
|
|
812
|
+
_emit(
|
|
813
|
+
warnings,
|
|
814
|
+
f"Trigger `options.behavior: {behavior}` was renamed to "
|
|
815
|
+
f"`{_DEPRECATED_TRIGGER_BEHAVIOR[behavior]}` in HA 2026.7 — "
|
|
816
|
+
"the old value still loads but raises a repair issue and "
|
|
817
|
+
"will be removed. Valid trigger values: `each`, `first`, "
|
|
818
|
+
"`all` (conditions keep `any`/`all`).",
|
|
819
|
+
skill_prefix,
|
|
820
|
+
"automation-patterns.md#trigger-types",
|
|
821
|
+
)
|
|
822
|
+
|
|
823
|
+
|
|
824
|
+
def _check_template_trigger(
|
|
825
|
+
trigger: dict[str, Any], warnings: BestPracticeCheckResult, skill_prefix: str | None
|
|
826
|
+
) -> None:
|
|
827
|
+
"""Check a `template` trigger's `value_template` for anti-patterns.
|
|
828
|
+
|
|
829
|
+
Split out of :func:`_check_triggers` to keep its complexity low.
|
|
830
|
+
"""
|
|
831
|
+
vt = trigger.get("value_template", "")
|
|
832
|
+
if not isinstance(vt, str):
|
|
833
|
+
return
|
|
834
|
+
initial = len(warnings)
|
|
835
|
+
# See `_check_template_string`: duration math also trips the numeric
|
|
836
|
+
# comparison detector, but maps to `for:`, not `numeric_state`. Suppress
|
|
837
|
+
# the numeric_state suggestion when duration math is present.
|
|
838
|
+
duration_match = _RE_DURATION_MATH.search(vt)
|
|
839
|
+
if _RE_NUMERIC_CMP.search(vt) and not duration_match:
|
|
840
|
+
_emit(
|
|
841
|
+
warnings,
|
|
842
|
+
"Trigger uses template with float/int comparison — "
|
|
843
|
+
"use native `numeric_state` trigger instead "
|
|
844
|
+
"(e.g., `platform: numeric_state, entity_id: sensor.temp, above: 30`).",
|
|
845
|
+
skill_prefix,
|
|
846
|
+
"automation-patterns.md#trigger-types",
|
|
847
|
+
)
|
|
848
|
+
if _RE_IS_STATE.search(vt):
|
|
849
|
+
_emit(
|
|
850
|
+
warnings,
|
|
851
|
+
"Trigger uses template with `is_state()` — use "
|
|
852
|
+
"native `state` trigger instead "
|
|
853
|
+
"(e.g., `platform: state, entity_id: light.x, to: 'on'`).",
|
|
854
|
+
skill_prefix,
|
|
855
|
+
"automation-patterns.md#trigger-types",
|
|
856
|
+
)
|
|
857
|
+
if duration_match:
|
|
858
|
+
_emit(
|
|
859
|
+
warnings,
|
|
860
|
+
"Trigger uses template for duration/recency check "
|
|
861
|
+
"(`now() - X.last_changed/last_updated`) — use the native "
|
|
862
|
+
"`for:` field on a `state` trigger instead "
|
|
863
|
+
"(e.g., `platform: state, entity_id: binary_sensor.motion, "
|
|
864
|
+
"to: 'off', for: {minutes: 5}`). Native `for:` is event-driven "
|
|
865
|
+
"and doesn't re-evaluate on every state change.",
|
|
866
|
+
skill_prefix,
|
|
867
|
+
"automation-patterns.md#trigger-types",
|
|
868
|
+
)
|
|
869
|
+
# Generic fallback for unmatched template triggers.
|
|
870
|
+
if len(warnings) == initial and _RE_ANY_TEMPLATE.search(vt):
|
|
871
|
+
_emit(
|
|
872
|
+
warnings,
|
|
873
|
+
"Trigger uses `template` platform — if this maps to a native option "
|
|
874
|
+
"(`state`, `numeric_state`, `time`, `time_pattern`, `sun`, `zone`, "
|
|
875
|
+
"`event`), use that instead. Native triggers are event-driven; "
|
|
876
|
+
"template triggers re-evaluate on every state change.",
|
|
877
|
+
skill_prefix,
|
|
878
|
+
"automation-patterns.md#trigger-types",
|
|
879
|
+
)
|
|
880
|
+
|
|
881
|
+
|
|
882
|
+
def _check_numeric_state_trigger(
|
|
883
|
+
trigger: dict[str, Any], warnings: BestPracticeCheckResult, skill_prefix: str | None
|
|
884
|
+
) -> None:
|
|
885
|
+
"""Flag a `numeric_state` trigger's `value_template` used for duration math.
|
|
886
|
+
|
|
887
|
+
Split out of :func:`_check_triggers` to keep its complexity low.
|
|
888
|
+
"""
|
|
889
|
+
vt = trigger.get("value_template", "")
|
|
890
|
+
if isinstance(vt, str) and _RE_DURATION_MATH.search(vt):
|
|
891
|
+
_emit(
|
|
892
|
+
warnings,
|
|
893
|
+
"`numeric_state` trigger uses `value_template` for duration/recency "
|
|
894
|
+
"check (`now() - X.last_changed/last_updated`) — use the native "
|
|
895
|
+
"`for:` field on a `state` trigger instead "
|
|
896
|
+
"(e.g., `platform: state, entity_id: binary_sensor.motion, "
|
|
897
|
+
"to: 'off', for: {minutes: 5}`). Native `for:` is event-driven "
|
|
898
|
+
"and doesn't re-evaluate on every state change.",
|
|
899
|
+
skill_prefix,
|
|
900
|
+
"automation-patterns.md#trigger-types",
|
|
901
|
+
)
|
|
807
902
|
|
|
808
903
|
|
|
809
904
|
# ---------------------------------------------------------------------------
|
|
@@ -170,6 +170,115 @@ class CalendarTools:
|
|
|
170
170
|
)
|
|
171
171
|
return None # unreachable: exception_to_structured_error always raises
|
|
172
172
|
|
|
173
|
+
async def _create_recurring_calendar_event(
|
|
174
|
+
self,
|
|
175
|
+
entity_id: str,
|
|
176
|
+
summary: str,
|
|
177
|
+
start: str,
|
|
178
|
+
end: str,
|
|
179
|
+
description: str | None,
|
|
180
|
+
location: str | None,
|
|
181
|
+
rrule: str,
|
|
182
|
+
) -> Any:
|
|
183
|
+
"""Create a recurring calendar event series via the WebSocket API.
|
|
184
|
+
|
|
185
|
+
The ``calendar.create_event`` service schema has no rrule field --
|
|
186
|
+
recurrence is only accepted by the WebSocket command
|
|
187
|
+
``calendar/event/create`` (see HA Core
|
|
188
|
+
``homeassistant/components/calendar/__init__.py``), the same split
|
|
189
|
+
that forces the delete tool below onto the WebSocket API.
|
|
190
|
+
"""
|
|
191
|
+
event: dict[str, Any] = {
|
|
192
|
+
"summary": summary,
|
|
193
|
+
"dtstart": start,
|
|
194
|
+
"dtend": end,
|
|
195
|
+
"rrule": rrule,
|
|
196
|
+
}
|
|
197
|
+
if description is not None:
|
|
198
|
+
event["description"] = description
|
|
199
|
+
if location is not None:
|
|
200
|
+
event["location"] = location
|
|
201
|
+
|
|
202
|
+
ws_client, conn_error = await get_connected_ws_client(
|
|
203
|
+
self._client.base_url,
|
|
204
|
+
self._client.token,
|
|
205
|
+
verify_ssl=self._client.verify_ssl,
|
|
206
|
+
)
|
|
207
|
+
if conn_error or ws_client is None:
|
|
208
|
+
raise_tool_error(
|
|
209
|
+
conn_error
|
|
210
|
+
or create_error_response(
|
|
211
|
+
ErrorCode.CONNECTION_FAILED,
|
|
212
|
+
"Failed to connect to Home Assistant WebSocket",
|
|
213
|
+
context={"entity_id": entity_id},
|
|
214
|
+
)
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
try:
|
|
218
|
+
return await ws_client.send_command(
|
|
219
|
+
"calendar/event/create", entity_id=entity_id, event=event
|
|
220
|
+
)
|
|
221
|
+
finally:
|
|
222
|
+
# Guard disconnect: a transport-teardown error here would
|
|
223
|
+
# otherwise replace the original send_command exception.
|
|
224
|
+
try:
|
|
225
|
+
await ws_client.disconnect()
|
|
226
|
+
except Exception as disconnect_error:
|
|
227
|
+
logger.debug(
|
|
228
|
+
f"WebSocket disconnect after create_event for "
|
|
229
|
+
f"{entity_id}: {disconnect_error}"
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
async def _create_simple_calendar_event(
|
|
233
|
+
self,
|
|
234
|
+
entity_id: str,
|
|
235
|
+
summary: str,
|
|
236
|
+
start: str,
|
|
237
|
+
end: str,
|
|
238
|
+
description: str | None,
|
|
239
|
+
location: str | None,
|
|
240
|
+
) -> Any:
|
|
241
|
+
"""Create a one-off calendar event via the calendar.create_event service."""
|
|
242
|
+
service_data: dict[str, Any] = {
|
|
243
|
+
"entity_id": entity_id,
|
|
244
|
+
"summary": summary,
|
|
245
|
+
"start_date_time": start,
|
|
246
|
+
"end_date_time": end,
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if description is not None:
|
|
250
|
+
service_data["description"] = description
|
|
251
|
+
if location is not None:
|
|
252
|
+
service_data["location"] = location
|
|
253
|
+
|
|
254
|
+
return await self._client.call_service("calendar", "create_event", service_data)
|
|
255
|
+
|
|
256
|
+
def _build_set_calendar_event_error_suggestions(
|
|
257
|
+
self, entity_id: str, rrule: str | None, error: Exception
|
|
258
|
+
) -> list[str]:
|
|
259
|
+
"""Build suggestions for a failed ha_config_set_calendar_event call."""
|
|
260
|
+
suggestions = [
|
|
261
|
+
f"Verify calendar entity '{entity_id}' exists and supports event creation",
|
|
262
|
+
"Check datetime format (ISO 8601)",
|
|
263
|
+
"Ensure end time is after start time",
|
|
264
|
+
"Some calendar integrations may be read-only",
|
|
265
|
+
]
|
|
266
|
+
if rrule:
|
|
267
|
+
suggestions.insert(
|
|
268
|
+
0,
|
|
269
|
+
"Check RRULE syntax (RFC 5545, without the 'RRULE:' prefix, "
|
|
270
|
+
"e.g. 'FREQ=WEEKLY;BYDAY=MO') and that this calendar "
|
|
271
|
+
"integration supports recurring events",
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
error_str = str(error)
|
|
275
|
+
if "404" in error_str or "not found" in error_str.lower():
|
|
276
|
+
suggestions.insert(0, f"Calendar entity '{entity_id}' not found")
|
|
277
|
+
if "not supported" in error_str.lower():
|
|
278
|
+
suggestions.insert(0, "This calendar does not support event creation")
|
|
279
|
+
|
|
280
|
+
return suggestions
|
|
281
|
+
|
|
173
282
|
@tool(
|
|
174
283
|
name="ha_config_set_calendar_event",
|
|
175
284
|
tags={"Calendar"},
|
|
@@ -277,69 +386,12 @@ class CalendarTools:
|
|
|
277
386
|
)
|
|
278
387
|
|
|
279
388
|
if rrule:
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
# command ``calendar/event/create`` (see HA Core
|
|
283
|
-
# ``homeassistant/components/calendar/__init__.py``), the
|
|
284
|
-
# same split that forces the delete tool below onto the
|
|
285
|
-
# WebSocket API.
|
|
286
|
-
event: dict[str, Any] = {
|
|
287
|
-
"summary": summary,
|
|
288
|
-
"dtstart": start,
|
|
289
|
-
"dtend": end,
|
|
290
|
-
"rrule": rrule,
|
|
291
|
-
}
|
|
292
|
-
if description:
|
|
293
|
-
event["description"] = description
|
|
294
|
-
if location:
|
|
295
|
-
event["location"] = location
|
|
296
|
-
|
|
297
|
-
ws_client, conn_error = await get_connected_ws_client(
|
|
298
|
-
self._client.base_url,
|
|
299
|
-
self._client.token,
|
|
300
|
-
verify_ssl=self._client.verify_ssl,
|
|
389
|
+
result = await self._create_recurring_calendar_event(
|
|
390
|
+
entity_id, summary, start, end, description, location, rrule
|
|
301
391
|
)
|
|
302
|
-
if conn_error or ws_client is None:
|
|
303
|
-
raise_tool_error(
|
|
304
|
-
conn_error
|
|
305
|
-
or create_error_response(
|
|
306
|
-
ErrorCode.CONNECTION_FAILED,
|
|
307
|
-
"Failed to connect to Home Assistant WebSocket",
|
|
308
|
-
context={"entity_id": entity_id},
|
|
309
|
-
)
|
|
310
|
-
)
|
|
311
|
-
|
|
312
|
-
try:
|
|
313
|
-
result = await ws_client.send_command(
|
|
314
|
-
"calendar/event/create", entity_id=entity_id, event=event
|
|
315
|
-
)
|
|
316
|
-
finally:
|
|
317
|
-
# Guard disconnect: a transport-teardown error here would
|
|
318
|
-
# otherwise replace the original send_command exception.
|
|
319
|
-
try:
|
|
320
|
-
await ws_client.disconnect()
|
|
321
|
-
except Exception as disconnect_error:
|
|
322
|
-
logger.debug(
|
|
323
|
-
f"WebSocket disconnect after create_event for "
|
|
324
|
-
f"{entity_id}: {disconnect_error}"
|
|
325
|
-
)
|
|
326
392
|
else:
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
"entity_id": entity_id,
|
|
330
|
-
"summary": summary,
|
|
331
|
-
"start_date_time": start,
|
|
332
|
-
"end_date_time": end,
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
if description:
|
|
336
|
-
service_data["description"] = description
|
|
337
|
-
if location:
|
|
338
|
-
service_data["location"] = location
|
|
339
|
-
|
|
340
|
-
# Call the calendar.create_event service
|
|
341
|
-
result = await self._client.call_service(
|
|
342
|
-
"calendar", "create_event", service_data
|
|
393
|
+
result = await self._create_simple_calendar_event(
|
|
394
|
+
entity_id, summary, start, end, description, location
|
|
343
395
|
)
|
|
344
396
|
|
|
345
397
|
return {
|
|
@@ -362,25 +414,9 @@ class CalendarTools:
|
|
|
362
414
|
except Exception as error:
|
|
363
415
|
logger.error(f"Failed to create calendar event in {entity_id}: {error}")
|
|
364
416
|
|
|
365
|
-
suggestions =
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
"Ensure end time is after start time",
|
|
369
|
-
"Some calendar integrations may be read-only",
|
|
370
|
-
]
|
|
371
|
-
if rrule:
|
|
372
|
-
suggestions.insert(
|
|
373
|
-
0,
|
|
374
|
-
"Check RRULE syntax (RFC 5545, without the 'RRULE:' prefix, "
|
|
375
|
-
"e.g. 'FREQ=WEEKLY;BYDAY=MO') and that this calendar "
|
|
376
|
-
"integration supports recurring events",
|
|
377
|
-
)
|
|
378
|
-
|
|
379
|
-
error_str = str(error)
|
|
380
|
-
if "404" in error_str or "not found" in error_str.lower():
|
|
381
|
-
suggestions.insert(0, f"Calendar entity '{entity_id}' not found")
|
|
382
|
-
if "not supported" in error_str.lower():
|
|
383
|
-
suggestions.insert(0, "This calendar does not support event creation")
|
|
417
|
+
suggestions = self._build_set_calendar_event_error_suggestions(
|
|
418
|
+
entity_id, rrule, error
|
|
419
|
+
)
|
|
384
420
|
|
|
385
421
|
exception_to_structured_error(
|
|
386
422
|
error, context={"entity_id": entity_id}, suggestions=suggestions
|