ha-mcp-dev 8.4.3.dev2579__tar.gz → 8.4.3.dev2584__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-8.4.3.dev2579/src/ha_mcp_dev.egg-info → ha_mcp_dev-8.4.3.dev2584}/PKG-INFO +3 -3
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/pyproject.toml +8 -3
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/config.py +13 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/settings_ui/locales/en.json +2 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/settings_ui/settings.js +1 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/tools/tools_history.py +406 -22
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/tools/util_helpers.py +5 -5
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584/src/ha_mcp_dev.egg-info}/PKG-INFO +3 -3
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp_dev.egg-info/requires.txt +2 -2
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/LICENSE +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/MANIFEST.in +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/README.md +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/setup.cfg +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/__init__.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/__main__.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/_pypi_marker +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/_rich_compat.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/_vendor/__init__.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/_vendor/requirements.txt +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/_vendor/websockets/LICENSE +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/_vendor/websockets/MANIFEST.sha256 +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/_vendor/websockets/VENDORED +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/_vendor/websockets/__init__.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/_vendor/websockets/__main__.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/_vendor/websockets/asyncio/__init__.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/_vendor/websockets/asyncio/client.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/_vendor/websockets/asyncio/connection.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/_vendor/websockets/asyncio/messages.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/_vendor/websockets/asyncio/router.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/_vendor/websockets/asyncio/server.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/_vendor/websockets/auth.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/_vendor/websockets/cli.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/_vendor/websockets/client.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/_vendor/websockets/connection.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/_vendor/websockets/datastructures.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/_vendor/websockets/exceptions.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/_vendor/websockets/extensions/__init__.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/_vendor/websockets/extensions/base.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/_vendor/websockets/extensions/permessage_deflate.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/_vendor/websockets/frames.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/_vendor/websockets/headers.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/_vendor/websockets/http11.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/_vendor/websockets/imports.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/_vendor/websockets/legacy/__init__.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/_vendor/websockets/legacy/auth.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/_vendor/websockets/legacy/client.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/_vendor/websockets/legacy/exceptions.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/_vendor/websockets/legacy/framing.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/_vendor/websockets/legacy/handshake.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/_vendor/websockets/legacy/http.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/_vendor/websockets/legacy/protocol.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/_vendor/websockets/legacy/server.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/_vendor/websockets/protocol.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/_vendor/websockets/proxy.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/_vendor/websockets/py.typed +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/_vendor/websockets/server.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/_vendor/websockets/streams.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/_vendor/websockets/sync/__init__.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/_vendor/websockets/sync/client.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/_vendor/websockets/sync/connection.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/_vendor/websockets/sync/messages.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/_vendor/websockets/sync/router.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/_vendor/websockets/sync/server.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/_vendor/websockets/sync/utils.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/_vendor/websockets/trio/__init__.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/_vendor/websockets/trio/client.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/_vendor/websockets/trio/connection.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/_vendor/websockets/trio/messages.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/_vendor/websockets/trio/router.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/_vendor/websockets/trio/server.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/_vendor/websockets/trio/utils.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/_vendor/websockets/typing.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/_vendor/websockets/uri.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/_vendor/websockets/utils.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/_vendor/websockets/version.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/_version.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/auth/__init__.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/auth/consent_form.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/auth/oidc_compat.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/auth/provider.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/backup_manager.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/browser_landing.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/client/__init__.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/client/rest_client.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/client/supervisor_client.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/client/websocket_client.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/client/websocket_listener.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/dashboard_screenshot/__init__.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/dashboard_screenshot/capture.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/dashboard_screenshot/content.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/dashboard_screenshot/paths.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/dashboard_screenshot/provision.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/dashboard_screenshot/theme_guard.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/errors.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/ha_request_queue.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/hacs_auto_refresh.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/llm_exposure.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/log_filters.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/policy/__init__.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/policy/approval_queue.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/policy/editing.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/policy/evaluator.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/policy/handlers.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/policy/middleware.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/policy/model.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/policy/persistence.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/policy/value_sources.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/py.typed +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/read_only.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/redaction.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/renamed_tools.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/resources/skills-vendor/.claude/settings.json +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/resources/skills-vendor/.claude-plugin/marketplace.json +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/resources/skills-vendor/.claude-plugin/plugin.json +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/resources/skills-vendor/.github/ISSUE_TEMPLATE/skill-rca.md +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/resources/skills-vendor/.github/pull_request_template.md +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/resources/skills-vendor/AGENTS.md +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/resources/skills-vendor/CLAUDE.md +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/resources/skills-vendor/CONTRIBUTING.md +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/resources/skills-vendor/LICENSE +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/resources/skills-vendor/README.md +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/resources/skills-vendor/evals/area-target-over-entity-list/case.yaml +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/resources/skills-vendor/evals/arrive-home-automation/case.yaml +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/resources/skills-vendor/evals/assist-shortcut-card/case.yaml +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/resources/skills-vendor/evals/avg-temperature/case.yaml +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/resources/skills-vendor/evals/backup-deletion-confirmation/case.yaml +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/resources/skills-vendor/evals/blueprint-authoring/case.yaml +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/resources/skills-vendor/evals/button-event-entity/case.yaml +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/resources/skills-vendor/evals/device-id-trigger-migration/case.yaml +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/resources/skills-vendor/evals/helper-flow-fields-not-platform-yaml/case.yaml +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/resources/skills-vendor/evals/media-player-song/case.yaml +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/resources/skills-vendor/evals/motion-light-automation-mode/case.yaml +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/resources/skills-vendor/evals/rename-entity-impact-analysis/case.yaml +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/resources/skills-vendor/evals/template-attributes-yaml-exception/case.yaml +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/resources/skills-vendor/evals/timer-finished-automation/case.yaml +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/resources/skills-vendor/evals/vacuum-last-clean/case.yaml +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/resources/skills-vendor/scripts/check_eval_cases.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/SKILL.md +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/appdaemon.md +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/automation-patterns.md +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/backups.md +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/blueprint-guide.md +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-cards.md +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-guide.md +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/device-control.md +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/domain-docs.md +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/examples.yaml +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/helper-selection.md +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/safe-refactoring.md +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/scenes.md +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/template-guidelines.md +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/yaml-only-integrations.md +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/server.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/settings_ui/__init__.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/settings_ui/_handlers_advanced.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/settings_ui/_handlers_backups.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/settings_ui/_handlers_fs.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/settings_ui/_handlers_server.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/settings_ui/_handlers_theme.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/settings_ui/_handlers_tools.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/settings_ui/_i18n.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/settings_ui/_locale_policy.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/settings_ui/_persistence.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/settings_ui/_supervisor.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/settings_ui/_theme.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/settings_ui/_tools_meta.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/settings_ui/locales/README.md +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/settings_ui/locales/cs.json +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/settings_ui/locales/de.json +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/settings_ui/locales/eo.json +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/settings_ui/locales/es.json +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/settings_ui/locales/fr.json +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/settings_ui/locales/it.json +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/settings_ui/locales/ko.json +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/settings_ui/locales/nl.json +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/settings_ui/locales/pl.json +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/settings_ui/locales/ru.json +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/settings_ui/locales/sv.json +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/settings_ui/locales/tlh.json +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/settings_ui/locales/zh-Hans.json +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/settings_ui/settings.css +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/settings_ui/settings.html +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/smoke_test.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/stdio_settings_sidecar.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/strict_bps.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/tool_dispatch.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/tools/__init__.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/tools/auto_backup.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/tools/backup.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/tools/best_practice_checker.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/tools/best_practice_result.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/tools/best_practice_variables.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/tools/blueprint_sources.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/tools/blueprint_substitute.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/tools/blueprint_write.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/tools/bulk_selector.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/tools/component_api.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/tools/component_config_entries.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/tools/component_config_reads.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/tools/component_devices.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/tools/component_registries.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/tools/component_registry_lookup.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/tools/config_entry_flow.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/tools/config_entry_flow_form.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/tools/config_entry_flow_menu.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/tools/config_entry_flow_walker.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/tools/config_entry_identity.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/tools/config_entry_reconfigure.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/tools/config_entry_reload_watch.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/tools/device_control.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/tools/error_log_parsing.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/tools/hacs_registration.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/tools/helpers.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/tools/integration_reconfigure.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/tools/log_common.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/tools/log_sources.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/tools/log_sources_fault.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/tools/log_sources_supervisor.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/tools/radio/__init__.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/tools/radio/base.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/tools/radio/matter.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/tools/radio/thread.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/tools/radio/zigbee.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/tools/radio/zwave.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/tools/reference_validator.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/tools/registry.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/tools/renamed_tool_middleware.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/tools/smart_search/__init__.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/tools/smart_search/_base.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/tools/smart_search/_config.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/tools/smart_search/_deep.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/tools/smart_search/_entities.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/tools/smart_search/_fetch.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/tools/smart_search/_graph.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/tools/smart_search/_overview.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/tools/smart_search/_scenes.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/tools/smart_search/_scoring.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/tools/tool_search_hint_middleware.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/tools/tools_addons.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/tools/tools_areas.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/tools/tools_assist_pipeline.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/tools/tools_blueprints.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/tools/tools_bug_report.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/tools/tools_calendar.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/tools/tools_camera.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/tools/tools_categories.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/tools/tools_code.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/tools/tools_config_automations.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/tools/tools_config_dashboards.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/tools/tools_config_helpers.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/tools/tools_config_scenes.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/tools/tools_config_scripts.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/tools/tools_dashboard_screenshot.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/tools/tools_dev.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/tools/tools_energy.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/tools/tools_entities.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/tools/tools_filesystem.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/tools/tools_groups.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/tools/tools_hacs.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/tools/tools_integrations.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/tools/tools_labels.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/tools/tools_logs.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/tools/tools_radio.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/tools/tools_registry.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/tools/tools_resources.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/tools/tools_search.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/tools/tools_security_policy.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/tools/tools_service.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/tools/tools_services.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/tools/tools_system.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/tools/tools_themes.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/tools/tools_todo.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/tools/tools_traces.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/tools/tools_updates.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/tools/tools_utility.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/tools/tools_voice_assistant.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/tools/tools_yaml_config.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/tools/tools_yaml_read.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/tools/tools_zones.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/tools/validation_middleware.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/transforms/__init__.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/transforms/categorized_search.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/transforms/lite_docstrings.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/transport_security.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/update_check.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/utils/__init__.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/utils/config_hash.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/utils/config_write_lock.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/utils/data_paths.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/utils/device_registry_semantics.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/utils/domain_handlers.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/utils/entity_membership.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/utils/fuzzy_search.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/utils/kill_signal_diagnostics.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/utils/operation_manager.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/utils/python_sandbox.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/utils/skill_loader.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/utils/usage_logger.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/visibility/__init__.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/visibility/enforcement.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/visibility/model.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/visibility/persistence.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/visibility/resolver.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp_dev.egg-info/SOURCES.txt +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp_dev.egg-info/dependency_links.txt +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp_dev.egg-info/entry_points.txt +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp_dev.egg-info/top_level.txt +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/tests/__init__.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/tests/test_constants.py +0 -0
- {ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/tests/test_env_manager.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ha-mcp-dev
|
|
3
|
-
Version: 8.4.3.
|
|
3
|
+
Version: 8.4.3.dev2584
|
|
4
4
|
Summary: Home Assistant MCP Server - Complete control of Home Assistant through MCP
|
|
5
5
|
Author-email: Julien <github@qc-h.net>
|
|
6
6
|
License: MIT
|
|
@@ -20,8 +20,8 @@ Requires-Python: <3.15,>=3.13
|
|
|
20
20
|
Description-Content-Type: text/markdown
|
|
21
21
|
License-File: LICENSE
|
|
22
22
|
Requires-Dist: fastmcp==3.4.7
|
|
23
|
-
Requires-Dist: httpx[socks]
|
|
24
|
-
Requires-Dist: pydantic
|
|
23
|
+
Requires-Dist: httpx[socks]<0.29,>=0.28.1
|
|
24
|
+
Requires-Dist: pydantic<3,>=2.13.4
|
|
25
25
|
Requires-Dist: python-dotenv==1.2.3
|
|
26
26
|
Requires-Dist: truststore==0.10.4
|
|
27
27
|
Requires-Dist: cryptography<51,>=48.0.0
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "ha-mcp-dev"
|
|
7
|
-
version = "8.4.3.
|
|
7
|
+
version = "8.4.3.dev2584"
|
|
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
|
|
@@ -29,8 +29,13 @@ classifiers = [
|
|
|
29
29
|
|
|
30
30
|
dependencies = [
|
|
31
31
|
"fastmcp==3.4.7",
|
|
32
|
-
|
|
33
|
-
|
|
32
|
+
# Shared with the embedded HA process: admit Core's versions, including
|
|
33
|
+
# compatible updates, instead of requiring it to keep our exact pin.
|
|
34
|
+
# uv.lock selects standalone versions; HA's own constraints select
|
|
35
|
+
# the embedded versions. Check both the HACS minimum and current Core
|
|
36
|
+
# before raising these floors; standalone lock updates can move independently.
|
|
37
|
+
"httpx[socks]>=0.28.1,<0.29",
|
|
38
|
+
"pydantic>=2.13.4,<3",
|
|
34
39
|
"python-dotenv==1.2.3",
|
|
35
40
|
"truststore==0.10.4",
|
|
36
41
|
# websockets is deliberately ABSENT from this list: ha-mcp ships its own
|
|
@@ -100,6 +100,12 @@ class Settings(BaseSettings):
|
|
|
100
100
|
10, alias="HAMCP_INDIVIDUAL_FETCH_BATCH_SIZE"
|
|
101
101
|
)
|
|
102
102
|
|
|
103
|
+
# Optional preflight bounds for recorder queries. Disabled by default to
|
|
104
|
+
# preserve the established ha_get_history request contract.
|
|
105
|
+
enable_history_query_guardrails: bool = Field(
|
|
106
|
+
False, alias="HAMCP_ENABLE_HISTORY_QUERY_GUARDRAILS"
|
|
107
|
+
)
|
|
108
|
+
|
|
103
109
|
# Backup tool configuration
|
|
104
110
|
backup_hint: str = Field("normal", alias="BACKUP_HINT")
|
|
105
111
|
|
|
@@ -894,6 +900,13 @@ ADVANCED_SETTINGS_FIELDS: tuple[AdvancedField, ...] = (
|
|
|
894
900
|
True,
|
|
895
901
|
),
|
|
896
902
|
# Operations.
|
|
903
|
+
AdvancedField(
|
|
904
|
+
"enable_history_query_guardrails",
|
|
905
|
+
"HAMCP_ENABLE_HISTORY_QUERY_GUARDRAILS",
|
|
906
|
+
bool,
|
|
907
|
+
"operations",
|
|
908
|
+
True,
|
|
909
|
+
),
|
|
897
910
|
AdvancedField(
|
|
898
911
|
"ha_tool_concurrency", "HA_TOOL_CONCURRENCY", int, "operations", True
|
|
899
912
|
),
|
{ha_mcp_dev-8.4.3.dev2579 → ha_mcp_dev-8.4.3.dev2584}/src/ha_mcp/settings_ui/locales/en.json
RENAMED
|
@@ -404,6 +404,8 @@
|
|
|
404
404
|
"advanced.individual_config_timeout.help": "Timeout for each automation/script/scene config fetch. Range 1–600. Restart required.",
|
|
405
405
|
"advanced.individual_fetch_batch_size.label": "Config fetch batch size",
|
|
406
406
|
"advanced.individual_fetch_batch_size.help": "Concurrent per-ID config fetches. Lower toward 1 on serial HA servers. Range 1–100. Restart required.",
|
|
407
|
+
"advanced.enable_history_query_guardrails.label": "History query guardrails",
|
|
408
|
+
"advanced.enable_history_query_guardrails.help": "Reject recorder queries that exceed conservative entity and time-range budgets. Disabled by default.",
|
|
407
409
|
"advanced.ha_tool_concurrency.label": "Home Assistant tool concurrency",
|
|
408
410
|
"advanced.ha_tool_concurrency.help": "Optional limit on outer Home Assistant tool calls across all sessions. Internal REST, WebSocket, and per-tool fan-out concurrency is unchanged. 0 keeps unlimited behavior. A call waiting for capacity fails after 60 seconds. Range 0–32. Restart required.",
|
|
409
411
|
"advanced.backup_hint.label": "Backup-hint level",
|
|
@@ -3875,6 +3875,7 @@ const ADVANCED_FIELD_META = {
|
|
|
3875
3875
|
scene_config_time_budget: { label: "Scene config time budget (s)", help: "Max seconds deep search spends fetching scene configs before returning a partial result. Range 1–600. Restart required." },
|
|
3876
3876
|
individual_config_timeout: { label: "Per-request config fetch timeout (s)", help: "Timeout for each individual automation/script/scene config fetch during deep search. On HA servers that serve config reads serially, raise this and/or lower the batch size so queued requests don't time out. Values above the HA request timeout (HA_TIMEOUT, default 30) have no extra effect — the HTTP client gives up first. Range 1–600. Restart required." },
|
|
3877
3877
|
individual_fetch_batch_size: { label: "Config fetch batch size", help: "How many per-id config fetches deep search issues concurrently. Lower toward 1 on HA servers that serve config reads serially (symptom: 'timed out' partial-result warnings). Range 1–100. Restart required." },
|
|
3878
|
+
enable_history_query_guardrails: { label: "History query guardrails", help: "Reject recorder queries that exceed conservative entity and time-range budgets. Disabled by default." },
|
|
3878
3879
|
backup_hint: { label: "Backup-hint level", help: "Tunes how strongly the LLM is prompted to take a full-HA snapshot before risky writes." },
|
|
3879
3880
|
dashboard_screenshot_engine_url: { label: "Dashboard screenshot engine URL", help: "Base URL of the screenshot engine (e.g. http://puppet:10000). Leave blank to auto-discover the Puppet App (add-on) via the Supervisor (HA OS / Supervised). Only used when the Dashboard Screenshot beta feature is enabled. Takes effect without a restart." },
|
|
3880
3881
|
enable_websocket: { label: "Enable WebSocket", help: "WebSocket-based state monitoring. Disabling falls back to polling; many tools degrade. Restart required." },
|
|
@@ -10,8 +10,9 @@ ha_get_history -- Retrieve historical data with source-selectable mode:
|
|
|
10
10
|
"""
|
|
11
11
|
|
|
12
12
|
import logging
|
|
13
|
+
import math
|
|
13
14
|
import re
|
|
14
|
-
from datetime import UTC, datetime, timedelta
|
|
15
|
+
from datetime import UTC, datetime, timedelta, tzinfo
|
|
15
16
|
from typing import Annotated, Any, Literal
|
|
16
17
|
|
|
17
18
|
from fastmcp import Context
|
|
@@ -19,6 +20,7 @@ from fastmcp.exceptions import ToolError
|
|
|
19
20
|
from fastmcp.tools import tool
|
|
20
21
|
from pydantic import Field
|
|
21
22
|
|
|
23
|
+
from ..config import get_global_settings
|
|
22
24
|
from ..errors import ErrorCode, create_error_response, create_validation_error
|
|
23
25
|
from .helpers import (
|
|
24
26
|
exception_to_structured_error,
|
|
@@ -32,13 +34,22 @@ from .util_helpers import (
|
|
|
32
34
|
JSON_STRING_COERCION,
|
|
33
35
|
add_timezone_metadata,
|
|
34
36
|
build_pagination_metadata,
|
|
37
|
+
fetch_ha_timezone,
|
|
35
38
|
is_connection_error_message,
|
|
36
39
|
parse_string_list_param,
|
|
37
40
|
project_fields,
|
|
41
|
+
resolve_local_timezone,
|
|
38
42
|
)
|
|
39
43
|
|
|
40
44
|
logger = logging.getLogger(__name__)
|
|
41
45
|
|
|
46
|
+
_RELATIVE_TIME_UNIT_SECONDS = {
|
|
47
|
+
"h": 60 * 60,
|
|
48
|
+
"d": 24 * 60 * 60,
|
|
49
|
+
"w": 7 * 24 * 60 * 60,
|
|
50
|
+
"m": 30 * 24 * 60 * 60,
|
|
51
|
+
}
|
|
52
|
+
|
|
42
53
|
|
|
43
54
|
def _convert_timestamp(value: Any) -> str | None:
|
|
44
55
|
"""Convert a timestamp value to ISO format string.
|
|
@@ -61,19 +72,27 @@ def _convert_timestamp(value: Any) -> str | None:
|
|
|
61
72
|
return None
|
|
62
73
|
|
|
63
74
|
|
|
64
|
-
def parse_relative_time(
|
|
75
|
+
def parse_relative_time(
|
|
76
|
+
time_str: str | None,
|
|
77
|
+
default_hours: int = 24,
|
|
78
|
+
*,
|
|
79
|
+
reference_time: datetime | None = None,
|
|
80
|
+
) -> datetime:
|
|
65
81
|
"""
|
|
66
82
|
Parse a time string that can be either ISO format or relative (e.g., '24h', '7d').
|
|
67
83
|
|
|
68
84
|
Args:
|
|
69
85
|
time_str: Time string in ISO format or relative format (e.g., "24h", "7d", "2w", "1m" where 1m = 30 days)
|
|
70
86
|
default_hours: Default hours to go back if time_str is None
|
|
87
|
+
reference_time: Reference datetime for relative values and defaults.
|
|
71
88
|
|
|
72
89
|
Returns:
|
|
73
|
-
datetime
|
|
90
|
+
A datetime with the parsed value's timezone, or ``reference_time``'s
|
|
91
|
+
timezone for relative values.
|
|
74
92
|
"""
|
|
93
|
+
now = reference_time or datetime.now(UTC)
|
|
75
94
|
if time_str is None:
|
|
76
|
-
return
|
|
95
|
+
return now - timedelta(hours=default_hours)
|
|
77
96
|
|
|
78
97
|
# Check for relative time format
|
|
79
98
|
relative_pattern = r"^(\d+)([hdwm])$"
|
|
@@ -82,16 +101,12 @@ def parse_relative_time(time_str: str | None, default_hours: int = 24) -> dateti
|
|
|
82
101
|
if match:
|
|
83
102
|
value = int(match.group(1))
|
|
84
103
|
unit = match.group(2)
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
return datetime.now(UTC) - timedelta(weeks=value)
|
|
92
|
-
elif unit == "m":
|
|
93
|
-
# Approximate month as 30 days
|
|
94
|
-
return datetime.now(UTC) - timedelta(days=value * 30)
|
|
104
|
+
try:
|
|
105
|
+
return now - timedelta(seconds=value * _RELATIVE_TIME_UNIT_SECONDS[unit])
|
|
106
|
+
except OverflowError as exc:
|
|
107
|
+
raise ValueError(
|
|
108
|
+
f"Invalid time format: {time_str} is out of range"
|
|
109
|
+
) from exc
|
|
95
110
|
|
|
96
111
|
# Try parsing as ISO format
|
|
97
112
|
try:
|
|
@@ -103,7 +118,7 @@ def parse_relative_time(time_str: str | None, default_hours: int = 24) -> dateti
|
|
|
103
118
|
if dt.tzinfo is None:
|
|
104
119
|
dt = dt.replace(tzinfo=UTC)
|
|
105
120
|
return dt
|
|
106
|
-
except ValueError as e:
|
|
121
|
+
except (ValueError, OverflowError) as e:
|
|
107
122
|
raise ValueError(
|
|
108
123
|
f"Invalid time format: {time_str}. Use ISO format or relative (e.g., '24h', '7d', '2w', '1m')"
|
|
109
124
|
) from e
|
|
@@ -116,6 +131,74 @@ _DEFAULT_START_HOURS_BY_SOURCE: dict[str, int] = {"history": 24, "statistics": 3
|
|
|
116
131
|
_DEFAULT_HISTORY_LIMIT = 100
|
|
117
132
|
_MAX_HISTORY_LIMIT = 1000
|
|
118
133
|
|
|
134
|
+
# Home Assistant's recorder WebSocket APIs have no server-side row limit. These
|
|
135
|
+
# estimated safety budgets bound recorder scan work; they are not measured HA
|
|
136
|
+
# limits and deliberately favor ordinary default-window requests.
|
|
137
|
+
_MAX_HISTORY_ENTITIES = 10
|
|
138
|
+
_MAX_HISTORY_ENTITY_HOURS = 240.0
|
|
139
|
+
_MAX_STATISTICS_ENTITIES = 25
|
|
140
|
+
_MAX_SHORT_TERM_STATISTICS_ROWS = 10_000
|
|
141
|
+
_MAX_LONG_TERM_STATISTICS_ROWS = 20_000
|
|
142
|
+
_CALENDAR_STATISTICS_PERIODS = frozenset({"day", "week", "month", "year"})
|
|
143
|
+
_VALID_STATISTICS_PERIODS = frozenset(
|
|
144
|
+
{"5minute", "hour", *_CALENDAR_STATISTICS_PERIODS}
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
async def _get_statistics_timezone(client: Any) -> tzinfo:
|
|
149
|
+
"""Resolve HA's timezone without accepting an unsafe UTC fallback."""
|
|
150
|
+
timezone_name, fetch_failed = await fetch_ha_timezone(client)
|
|
151
|
+
if fetch_failed:
|
|
152
|
+
raise_tool_error(
|
|
153
|
+
create_error_response(
|
|
154
|
+
ErrorCode.CONNECTION_FAILED,
|
|
155
|
+
"Could not fetch the Home Assistant timezone required for safe calendar statistics estimation",
|
|
156
|
+
suggestions=[
|
|
157
|
+
"Check the Home Assistant connection, then retry.",
|
|
158
|
+
"Use period='hour' or period='5minute', which do not require calendar alignment.",
|
|
159
|
+
],
|
|
160
|
+
)
|
|
161
|
+
)
|
|
162
|
+
timezone, resolved_timezone_name = resolve_local_timezone(timezone_name)
|
|
163
|
+
if resolved_timezone_name != timezone_name:
|
|
164
|
+
raise_tool_error(
|
|
165
|
+
create_error_response(
|
|
166
|
+
ErrorCode.VALIDATION_INVALID_PARAMETER,
|
|
167
|
+
"Home Assistant reports a timezone that could not be resolved for safe calendar statistics estimation",
|
|
168
|
+
context={"home_assistant_timezone": timezone_name},
|
|
169
|
+
suggestions=[
|
|
170
|
+
"Configure a valid Home Assistant timezone, then retry.",
|
|
171
|
+
"Use period='hour' or period='5minute', which do not require calendar alignment.",
|
|
172
|
+
],
|
|
173
|
+
)
|
|
174
|
+
)
|
|
175
|
+
return timezone
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
async def _prepare_guardrail_query(
|
|
179
|
+
client: Any,
|
|
180
|
+
*,
|
|
181
|
+
enabled: bool,
|
|
182
|
+
source: str,
|
|
183
|
+
period: str,
|
|
184
|
+
start_dt: datetime,
|
|
185
|
+
end_dt: datetime,
|
|
186
|
+
end_time_was_provided: bool,
|
|
187
|
+
) -> tzinfo:
|
|
188
|
+
"""Validate guarded ranges and resolve calendar statistics timezone."""
|
|
189
|
+
_validate_time_range(
|
|
190
|
+
start_dt,
|
|
191
|
+
end_dt,
|
|
192
|
+
end_time_was_provided=end_time_was_provided,
|
|
193
|
+
reject_zero_length=enabled,
|
|
194
|
+
)
|
|
195
|
+
if not enabled:
|
|
196
|
+
return UTC
|
|
197
|
+
if source != "statistics" or period not in _CALENDAR_STATISTICS_PERIODS:
|
|
198
|
+
return UTC
|
|
199
|
+
_validate_calendar_statistics_range(start_dt, end_dt, period)
|
|
200
|
+
return await _get_statistics_timezone(client)
|
|
201
|
+
|
|
119
202
|
|
|
120
203
|
class HistoryTools:
|
|
121
204
|
"""Historical data access tools for Home Assistant."""
|
|
@@ -243,7 +326,7 @@ class HistoryTools:
|
|
|
243
326
|
ctx: Context | None = None,
|
|
244
327
|
) -> dict[str, Any]:
|
|
245
328
|
"""
|
|
246
|
-
|
|
329
|
+
Get historical data from Home Assistant's recorder.
|
|
247
330
|
|
|
248
331
|
**Sources:**
|
|
249
332
|
- "history" (default): Raw state changes, ~10 day retention, full resolution
|
|
@@ -269,11 +352,16 @@ class HistoryTools:
|
|
|
269
352
|
All data is fetched from HA before slicing; limit/offset are client-side.
|
|
270
353
|
With multiple entity_ids, offset must be 0 — use a single entity_id for offset > 0.
|
|
271
354
|
Use has_more and next_offset from the response to paginate.
|
|
355
|
+
Administrators can optionally enable recorder workload guardrails in Advanced
|
|
356
|
+
settings. When enabled, oversized entity/time workloads are rejected before the
|
|
357
|
+
recorder query is issued; narrow the time range or entity list to stay within
|
|
358
|
+
the budget. Calendar statistics may first read HA's configured timezone so the
|
|
359
|
+
estimate follows local calendar boundaries.
|
|
272
360
|
|
|
273
361
|
**Example -- history (default):**
|
|
274
362
|
```python
|
|
275
363
|
ha_get_history(entity_ids="sensor.bedroom_temperature", start_time="24h")
|
|
276
|
-
ha_get_history(entity_ids=["sensor.temperature", "sensor.humidity"], start_time="
|
|
364
|
+
ha_get_history(entity_ids=["sensor.temperature", "sensor.humidity"], start_time="3d", limit=500)
|
|
277
365
|
# Default order="desc" returns newest states first.
|
|
278
366
|
# To paginate oldest-first, use order="asc":
|
|
279
367
|
ha_get_history(entity_ids="sensor.temperature", start_time="7d", limit=100, offset=100, order="asc")
|
|
@@ -321,6 +409,27 @@ class HistoryTools:
|
|
|
321
409
|
|
|
322
410
|
# Parse time parameters
|
|
323
411
|
start_dt, end_dt = _parse_time_range(start_time, end_time, default_hours)
|
|
412
|
+
query_settings = get_global_settings()
|
|
413
|
+
statistics_timezone = await _prepare_guardrail_query(
|
|
414
|
+
self._client,
|
|
415
|
+
enabled=query_settings.enable_history_query_guardrails,
|
|
416
|
+
source=source,
|
|
417
|
+
period=period,
|
|
418
|
+
start_dt=start_dt,
|
|
419
|
+
end_dt=end_dt,
|
|
420
|
+
end_time_was_provided=end_time is not None,
|
|
421
|
+
)
|
|
422
|
+
_validate_query_workload(
|
|
423
|
+
source=source,
|
|
424
|
+
entity_ids=entity_id_list,
|
|
425
|
+
start_dt=start_dt,
|
|
426
|
+
end_dt=end_dt,
|
|
427
|
+
minimal_response=minimal_response,
|
|
428
|
+
significant_changes_only=significant_changes_only,
|
|
429
|
+
period=period,
|
|
430
|
+
enforce_budget=query_settings.enable_history_query_guardrails,
|
|
431
|
+
statistics_timezone=statistics_timezone,
|
|
432
|
+
)
|
|
324
433
|
|
|
325
434
|
await safe_info(
|
|
326
435
|
ctx,
|
|
@@ -459,9 +568,14 @@ def _parse_time_range(
|
|
|
459
568
|
default_hours: int,
|
|
460
569
|
) -> tuple[datetime, datetime]:
|
|
461
570
|
"""Parse start_time and end_time into datetime objects."""
|
|
571
|
+
reference_time = datetime.now(UTC)
|
|
462
572
|
try:
|
|
463
|
-
start_dt = parse_relative_time(
|
|
464
|
-
|
|
573
|
+
start_dt = parse_relative_time(
|
|
574
|
+
start_time,
|
|
575
|
+
default_hours=default_hours,
|
|
576
|
+
reference_time=reference_time,
|
|
577
|
+
)
|
|
578
|
+
except (ValueError, OverflowError) as e:
|
|
465
579
|
raise_tool_error(
|
|
466
580
|
create_error_response(
|
|
467
581
|
ErrorCode.VALIDATION_INVALID_PARAMETER,
|
|
@@ -476,8 +590,12 @@ def _parse_time_range(
|
|
|
476
590
|
|
|
477
591
|
if end_time:
|
|
478
592
|
try:
|
|
479
|
-
end_dt = parse_relative_time(
|
|
480
|
-
|
|
593
|
+
end_dt = parse_relative_time(
|
|
594
|
+
end_time,
|
|
595
|
+
default_hours=0,
|
|
596
|
+
reference_time=reference_time,
|
|
597
|
+
)
|
|
598
|
+
except (ValueError, OverflowError) as e:
|
|
481
599
|
raise_tool_error(
|
|
482
600
|
create_error_response(
|
|
483
601
|
ErrorCode.VALIDATION_INVALID_PARAMETER,
|
|
@@ -487,11 +605,277 @@ def _parse_time_range(
|
|
|
487
605
|
)
|
|
488
606
|
)
|
|
489
607
|
else:
|
|
490
|
-
end_dt =
|
|
608
|
+
end_dt = reference_time
|
|
491
609
|
|
|
492
610
|
return start_dt, end_dt
|
|
493
611
|
|
|
494
612
|
|
|
613
|
+
def _validate_time_range(
|
|
614
|
+
start_dt: datetime,
|
|
615
|
+
end_dt: datetime,
|
|
616
|
+
*,
|
|
617
|
+
end_time_was_provided: bool = True,
|
|
618
|
+
reject_zero_length: bool = True,
|
|
619
|
+
) -> None:
|
|
620
|
+
"""Reject reversed ranges and, when requested, zero-length ranges."""
|
|
621
|
+
if end_dt < start_dt or (reject_zero_length and end_dt == start_dt):
|
|
622
|
+
if not end_time_was_provided and start_dt > end_dt:
|
|
623
|
+
message = "start_time must not be in the future when end_time is omitted"
|
|
624
|
+
suggestions = ["Choose a start_time at or before the current time."]
|
|
625
|
+
else:
|
|
626
|
+
message = "end_time must be later than start_time"
|
|
627
|
+
suggestions = ["Choose an end_time later than start_time."]
|
|
628
|
+
raise_tool_error(
|
|
629
|
+
create_error_response(
|
|
630
|
+
ErrorCode.VALIDATION_INVALID_PARAMETER,
|
|
631
|
+
message,
|
|
632
|
+
context={
|
|
633
|
+
"start_time": start_dt.isoformat(),
|
|
634
|
+
"end_time": end_dt.isoformat(),
|
|
635
|
+
},
|
|
636
|
+
suggestions=suggestions,
|
|
637
|
+
)
|
|
638
|
+
)
|
|
639
|
+
|
|
640
|
+
|
|
641
|
+
def _validate_calendar_statistics_range(
|
|
642
|
+
start_dt: datetime, end_dt: datetime, period: str
|
|
643
|
+
) -> None:
|
|
644
|
+
"""Reject boundary years that calendar alignment cannot advance safely."""
|
|
645
|
+
if start_dt.year <= datetime.min.year or end_dt.year >= datetime.max.year:
|
|
646
|
+
raise_tool_error(
|
|
647
|
+
create_error_response(
|
|
648
|
+
ErrorCode.VALIDATION_INVALID_PARAMETER,
|
|
649
|
+
f"Time range is outside the safe calendar alignment bounds for period='{period}'",
|
|
650
|
+
context={
|
|
651
|
+
"start_time": start_dt.isoformat(),
|
|
652
|
+
"end_time": end_dt.isoformat(),
|
|
653
|
+
"period": period,
|
|
654
|
+
},
|
|
655
|
+
suggestions=[
|
|
656
|
+
"Choose start_time and end_time between years 2 and 9998."
|
|
657
|
+
],
|
|
658
|
+
)
|
|
659
|
+
)
|
|
660
|
+
|
|
661
|
+
|
|
662
|
+
def _validate_query_workload(
|
|
663
|
+
*,
|
|
664
|
+
source: str,
|
|
665
|
+
entity_ids: list[str],
|
|
666
|
+
start_dt: datetime,
|
|
667
|
+
end_dt: datetime,
|
|
668
|
+
minimal_response: bool,
|
|
669
|
+
significant_changes_only: bool,
|
|
670
|
+
period: str,
|
|
671
|
+
enforce_budget: bool,
|
|
672
|
+
statistics_timezone: tzinfo = UTC,
|
|
673
|
+
) -> None:
|
|
674
|
+
"""Reject recorder requests likely to monopolize Home Assistant resources."""
|
|
675
|
+
if not enforce_budget:
|
|
676
|
+
return
|
|
677
|
+
|
|
678
|
+
_validate_time_range(start_dt, end_dt)
|
|
679
|
+
if source == "history":
|
|
680
|
+
violation = _history_workload_violation(
|
|
681
|
+
entity_ids,
|
|
682
|
+
start_dt,
|
|
683
|
+
end_dt,
|
|
684
|
+
minimal_response,
|
|
685
|
+
significant_changes_only,
|
|
686
|
+
)
|
|
687
|
+
else:
|
|
688
|
+
violation = _statistics_workload_violation(
|
|
689
|
+
entity_ids, start_dt, end_dt, period, statistics_timezone
|
|
690
|
+
)
|
|
691
|
+
|
|
692
|
+
if violation is None:
|
|
693
|
+
return
|
|
694
|
+
context, suggestions = violation
|
|
695
|
+
|
|
696
|
+
suggestions.append(
|
|
697
|
+
"An administrator can disable history query guardrails in Advanced settings after evaluating the workload risk."
|
|
698
|
+
)
|
|
699
|
+
raise_tool_error(
|
|
700
|
+
create_error_response(
|
|
701
|
+
ErrorCode.VALIDATION_INVALID_PARAMETER,
|
|
702
|
+
"Recorder query exceeds the safe workload budget",
|
|
703
|
+
context=context,
|
|
704
|
+
suggestions=suggestions,
|
|
705
|
+
)
|
|
706
|
+
)
|
|
707
|
+
|
|
708
|
+
|
|
709
|
+
def _history_workload_violation(
|
|
710
|
+
entity_ids: list[str],
|
|
711
|
+
start_dt: datetime,
|
|
712
|
+
end_dt: datetime,
|
|
713
|
+
minimal_response: bool,
|
|
714
|
+
significant_changes_only: bool,
|
|
715
|
+
) -> tuple[dict[str, Any], list[str]] | None:
|
|
716
|
+
"""Return raw-history violation details, or None when within budget."""
|
|
717
|
+
# Estimated multipliers account for the larger recorder payload when
|
|
718
|
+
# filtering and attribute minimization are disabled; they are not measured
|
|
719
|
+
# row counts.
|
|
720
|
+
detail_weight = 1
|
|
721
|
+
if not significant_changes_only:
|
|
722
|
+
detail_weight *= 4
|
|
723
|
+
if not minimal_response:
|
|
724
|
+
detail_weight *= 2
|
|
725
|
+
entity_count = len(entity_ids)
|
|
726
|
+
estimated_entity_hours = (
|
|
727
|
+
(end_dt - start_dt).total_seconds() / 3600 * entity_count * detail_weight
|
|
728
|
+
)
|
|
729
|
+
if (
|
|
730
|
+
entity_count <= _MAX_HISTORY_ENTITIES
|
|
731
|
+
and estimated_entity_hours <= _MAX_HISTORY_ENTITY_HOURS
|
|
732
|
+
):
|
|
733
|
+
return None
|
|
734
|
+
context = {
|
|
735
|
+
"entity_count": entity_count,
|
|
736
|
+
"max_entities": _MAX_HISTORY_ENTITIES,
|
|
737
|
+
"estimated_entity_hours": round(estimated_entity_hours, 2),
|
|
738
|
+
"max_entity_hours": _MAX_HISTORY_ENTITY_HOURS,
|
|
739
|
+
"detail_weight": detail_weight,
|
|
740
|
+
"minimal_response": minimal_response,
|
|
741
|
+
"significant_changes_only": significant_changes_only,
|
|
742
|
+
}
|
|
743
|
+
suggestions = ["Query fewer entities or use a shorter time range."]
|
|
744
|
+
if not significant_changes_only:
|
|
745
|
+
suggestions.append("Set significant_changes_only=true to reduce recorder work.")
|
|
746
|
+
if not minimal_response:
|
|
747
|
+
suggestions.append("Set minimal_response=true to omit full state attributes.")
|
|
748
|
+
suggestions.append(
|
|
749
|
+
"Use source='statistics' for long ranges when the entities support long-term statistics."
|
|
750
|
+
)
|
|
751
|
+
return context, suggestions
|
|
752
|
+
|
|
753
|
+
|
|
754
|
+
def _statistics_workload_violation(
|
|
755
|
+
entity_ids: list[str],
|
|
756
|
+
start_dt: datetime,
|
|
757
|
+
end_dt: datetime,
|
|
758
|
+
period: str,
|
|
759
|
+
statistics_timezone: tzinfo,
|
|
760
|
+
) -> tuple[dict[str, Any], list[str]] | None:
|
|
761
|
+
"""Return statistics violation details, or None when within budget."""
|
|
762
|
+
if period not in _VALID_STATISTICS_PERIODS:
|
|
763
|
+
raise_tool_error(
|
|
764
|
+
create_error_response(
|
|
765
|
+
ErrorCode.VALIDATION_INVALID_PARAMETER,
|
|
766
|
+
f"Invalid statistics period: {period}",
|
|
767
|
+
context={"period": period},
|
|
768
|
+
suggestions=[
|
|
769
|
+
"Use one of: '5minute', 'hour', 'day', 'week', 'month', 'year'."
|
|
770
|
+
],
|
|
771
|
+
)
|
|
772
|
+
)
|
|
773
|
+
scan_start, scan_end = _statistics_scan_window(
|
|
774
|
+
start_dt, end_dt, period, statistics_timezone
|
|
775
|
+
)
|
|
776
|
+
scan_granularity_seconds = 300 if period == "5minute" else 3600
|
|
777
|
+
max_statistics_rows = (
|
|
778
|
+
_MAX_SHORT_TERM_STATISTICS_ROWS
|
|
779
|
+
if period == "5minute"
|
|
780
|
+
else _MAX_LONG_TERM_STATISTICS_ROWS
|
|
781
|
+
)
|
|
782
|
+
entity_count = len(entity_ids)
|
|
783
|
+
estimated_rows = (
|
|
784
|
+
math.ceil((scan_end - scan_start).total_seconds() / scan_granularity_seconds)
|
|
785
|
+
* entity_count
|
|
786
|
+
)
|
|
787
|
+
if (
|
|
788
|
+
entity_count <= _MAX_STATISTICS_ENTITIES
|
|
789
|
+
and estimated_rows <= max_statistics_rows
|
|
790
|
+
):
|
|
791
|
+
return None
|
|
792
|
+
context = {
|
|
793
|
+
"entity_count": entity_count,
|
|
794
|
+
"max_entities": _MAX_STATISTICS_ENTITIES,
|
|
795
|
+
"estimated_rows": estimated_rows,
|
|
796
|
+
"max_estimated_rows": max_statistics_rows,
|
|
797
|
+
"period": period,
|
|
798
|
+
"scan_granularity_minutes": scan_granularity_seconds // 60,
|
|
799
|
+
"scan_start_time": scan_start.isoformat(),
|
|
800
|
+
"scan_end_time": scan_end.isoformat(),
|
|
801
|
+
}
|
|
802
|
+
suggestions = ["Query fewer entities or use a shorter time range."]
|
|
803
|
+
if period == "5minute":
|
|
804
|
+
suggestions.append("Use period='hour' to scan the long-term statistics table.")
|
|
805
|
+
return context, suggestions
|
|
806
|
+
|
|
807
|
+
|
|
808
|
+
def _statistics_scan_window(
|
|
809
|
+
start_dt: datetime,
|
|
810
|
+
end_dt: datetime,
|
|
811
|
+
period: str,
|
|
812
|
+
local_timezone: tzinfo,
|
|
813
|
+
) -> tuple[datetime, datetime]:
|
|
814
|
+
"""Mirror Core's calendar alignment before its statistics table query."""
|
|
815
|
+
if period not in _CALENDAR_STATISTICS_PERIODS:
|
|
816
|
+
return start_dt, end_dt
|
|
817
|
+
|
|
818
|
+
local_start = start_dt.astimezone(local_timezone)
|
|
819
|
+
local_end = end_dt.astimezone(local_timezone)
|
|
820
|
+
|
|
821
|
+
if period == "day":
|
|
822
|
+
scan_start = local_start.replace(hour=0, minute=0, second=0, microsecond=0)
|
|
823
|
+
scan_end = local_end.replace(
|
|
824
|
+
hour=0, minute=0, second=0, microsecond=0
|
|
825
|
+
) + timedelta(days=1)
|
|
826
|
+
elif period == "week":
|
|
827
|
+
scan_start = local_start.replace(
|
|
828
|
+
hour=0, minute=0, second=0, microsecond=0
|
|
829
|
+
) - timedelta(days=local_start.weekday())
|
|
830
|
+
scan_end = (
|
|
831
|
+
local_end.replace(hour=0, minute=0, second=0, microsecond=0)
|
|
832
|
+
- timedelta(days=local_end.weekday())
|
|
833
|
+
+ timedelta(days=7)
|
|
834
|
+
)
|
|
835
|
+
elif period == "month":
|
|
836
|
+
scan_start = local_start.replace(
|
|
837
|
+
day=1, hour=0, minute=0, second=0, microsecond=0
|
|
838
|
+
)
|
|
839
|
+
scan_end = _next_month_start(local_end)
|
|
840
|
+
else:
|
|
841
|
+
scan_start = local_start.replace(
|
|
842
|
+
month=1, day=1, hour=0, minute=0, second=0, microsecond=0
|
|
843
|
+
)
|
|
844
|
+
scan_end = local_end.replace(
|
|
845
|
+
year=local_end.year + 1,
|
|
846
|
+
month=1,
|
|
847
|
+
day=1,
|
|
848
|
+
hour=0,
|
|
849
|
+
minute=0,
|
|
850
|
+
second=0,
|
|
851
|
+
microsecond=0,
|
|
852
|
+
)
|
|
853
|
+
|
|
854
|
+
return scan_start.astimezone(UTC), scan_end.astimezone(UTC)
|
|
855
|
+
|
|
856
|
+
|
|
857
|
+
def _next_month_start(value: datetime) -> datetime:
|
|
858
|
+
"""Return local midnight on the first day of the following month."""
|
|
859
|
+
if value.month == 12:
|
|
860
|
+
return value.replace(
|
|
861
|
+
year=value.year + 1,
|
|
862
|
+
month=1,
|
|
863
|
+
day=1,
|
|
864
|
+
hour=0,
|
|
865
|
+
minute=0,
|
|
866
|
+
second=0,
|
|
867
|
+
microsecond=0,
|
|
868
|
+
)
|
|
869
|
+
return value.replace(
|
|
870
|
+
month=value.month + 1,
|
|
871
|
+
day=1,
|
|
872
|
+
hour=0,
|
|
873
|
+
minute=0,
|
|
874
|
+
second=0,
|
|
875
|
+
microsecond=0,
|
|
876
|
+
)
|
|
877
|
+
|
|
878
|
+
|
|
495
879
|
def _raise_recorder_ws_failure(
|
|
496
880
|
kind: str,
|
|
497
881
|
error_msg: str,
|
|
@@ -712,7 +712,7 @@ _TIMESTAMP_METADATA_FIELDS = {
|
|
|
712
712
|
}
|
|
713
713
|
|
|
714
714
|
|
|
715
|
-
async def
|
|
715
|
+
async def fetch_ha_timezone(client: Any) -> tuple[str, bool]:
|
|
716
716
|
"""Fetch the HA timezone, preferring the ``ha_mcp_tools`` component's cached
|
|
717
717
|
``info`` handshake over a fresh ``/api/config`` REST call.
|
|
718
718
|
|
|
@@ -757,7 +757,7 @@ async def _fetch_ha_timezone(client: Any) -> tuple[str, bool]:
|
|
|
757
757
|
return "UTC", True
|
|
758
758
|
|
|
759
759
|
|
|
760
|
-
def
|
|
760
|
+
def resolve_local_timezone(ha_timezone: str) -> tuple[_TZInfo, str]:
|
|
761
761
|
"""Resolve *ha_timezone* to a ``ZoneInfo``, falling back to UTC if unknown.
|
|
762
762
|
|
|
763
763
|
Returns ``(local_tz, ha_timezone)``. ``ha_timezone`` is normalized to
|
|
@@ -805,7 +805,7 @@ async def add_timezone_metadata(
|
|
|
805
805
|
) -> dict[str, Any]:
|
|
806
806
|
"""Add Home Assistant timezone to tool responses and convert timestamps to local time.
|
|
807
807
|
|
|
808
|
-
Resolves the Home Assistant time zone via ``
|
|
808
|
+
Resolves the Home Assistant time zone via ``fetch_ha_timezone`` (which
|
|
809
809
|
prefers the ``ha_mcp_tools`` component's cached handshake and falls back to
|
|
810
810
|
``/api/config``), converts every ``last_changed``, ``last_updated``,
|
|
811
811
|
``last_reported``, ``when``, and ``last_triggered`` field found anywhere in
|
|
@@ -824,7 +824,7 @@ async def add_timezone_metadata(
|
|
|
824
824
|
if not include_metadata:
|
|
825
825
|
return data
|
|
826
826
|
|
|
827
|
-
ha_timezone, fetch_failed = await
|
|
827
|
+
ha_timezone, fetch_failed = await fetch_ha_timezone(client)
|
|
828
828
|
|
|
829
829
|
if fetch_failed:
|
|
830
830
|
return {
|
|
@@ -836,7 +836,7 @@ async def add_timezone_metadata(
|
|
|
836
836
|
},
|
|
837
837
|
}
|
|
838
838
|
|
|
839
|
-
local_tz, ha_timezone =
|
|
839
|
+
local_tz, ha_timezone = resolve_local_timezone(ha_timezone)
|
|
840
840
|
converted_data = _convert_timestamp_fields(data, local_tz)
|
|
841
841
|
|
|
842
842
|
return {
|