ha-mcp-dev 8.2.0.dev2320__tar.gz → 8.2.0.dev2323__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.2.0.dev2320/src/ha_mcp_dev.egg-info → ha_mcp_dev-8.2.0.dev2323}/PKG-INFO +1 -1
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/pyproject.toml +1 -1
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/client/rest_client.py +31 -12
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/settings_ui/locales/README.md +8 -3
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/settings_ui/locales/en.json +2 -2
- ha_mcp_dev-8.2.0.dev2323/src/ha_mcp/settings_ui/locales/eo.json +26 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/settings_ui/settings.js +1 -1
- ha_mcp_dev-8.2.0.dev2323/src/ha_mcp/tools/error_log_parsing.py +365 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/tools/tools_utility.py +243 -48
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323/src/ha_mcp_dev.egg-info}/PKG-INFO +1 -1
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp_dev.egg-info/SOURCES.txt +2 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/LICENSE +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/MANIFEST.in +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/README.md +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/setup.cfg +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/__init__.py +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/__main__.py +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/_pypi_marker +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/_rich_compat.py +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/_vendor/__init__.py +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/_vendor/requirements.txt +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/_vendor/websockets/LICENSE +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/_vendor/websockets/MANIFEST.sha256 +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/_vendor/websockets/VENDORED +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/_vendor/websockets/__init__.py +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/_vendor/websockets/__main__.py +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/_vendor/websockets/asyncio/__init__.py +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/_vendor/websockets/asyncio/client.py +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/_vendor/websockets/asyncio/connection.py +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/_vendor/websockets/asyncio/messages.py +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/_vendor/websockets/asyncio/router.py +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/_vendor/websockets/asyncio/server.py +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/_vendor/websockets/auth.py +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/_vendor/websockets/cli.py +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/_vendor/websockets/client.py +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/_vendor/websockets/connection.py +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/_vendor/websockets/datastructures.py +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/_vendor/websockets/exceptions.py +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/_vendor/websockets/extensions/__init__.py +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/_vendor/websockets/extensions/base.py +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/_vendor/websockets/extensions/permessage_deflate.py +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/_vendor/websockets/frames.py +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/_vendor/websockets/headers.py +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/_vendor/websockets/http11.py +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/_vendor/websockets/imports.py +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/_vendor/websockets/legacy/__init__.py +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/_vendor/websockets/legacy/auth.py +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/_vendor/websockets/legacy/client.py +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/_vendor/websockets/legacy/exceptions.py +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/_vendor/websockets/legacy/framing.py +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/_vendor/websockets/legacy/handshake.py +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/_vendor/websockets/legacy/http.py +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/_vendor/websockets/legacy/protocol.py +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/_vendor/websockets/legacy/server.py +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/_vendor/websockets/protocol.py +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/_vendor/websockets/proxy.py +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/_vendor/websockets/py.typed +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/_vendor/websockets/server.py +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/_vendor/websockets/streams.py +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/_vendor/websockets/sync/__init__.py +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/_vendor/websockets/sync/client.py +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/_vendor/websockets/sync/connection.py +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/_vendor/websockets/sync/messages.py +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/_vendor/websockets/sync/router.py +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/_vendor/websockets/sync/server.py +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/_vendor/websockets/sync/utils.py +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/_vendor/websockets/trio/__init__.py +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/_vendor/websockets/trio/client.py +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/_vendor/websockets/trio/connection.py +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/_vendor/websockets/trio/messages.py +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/_vendor/websockets/trio/router.py +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/_vendor/websockets/trio/server.py +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/_vendor/websockets/trio/utils.py +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/_vendor/websockets/typing.py +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/_vendor/websockets/uri.py +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/_vendor/websockets/utils.py +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/_vendor/websockets/version.py +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/_version.py +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/auth/__init__.py +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/auth/consent_form.py +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/auth/oidc_compat.py +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/auth/provider.py +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/backup_manager.py +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/browser_landing.py +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/client/__init__.py +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/client/supervisor_client.py +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/client/websocket_client.py +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/client/websocket_listener.py +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/config.py +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/dashboard_screenshot/__init__.py +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/dashboard_screenshot/capture.py +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/dashboard_screenshot/content.py +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/dashboard_screenshot/paths.py +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/dashboard_screenshot/provision.py +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/dashboard_screenshot/theme_guard.py +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/errors.py +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/hacs_auto_refresh.py +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/llm_exposure.py +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/policy/__init__.py +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/policy/approval_queue.py +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/policy/editing.py +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/policy/evaluator.py +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/policy/handlers.py +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/policy/middleware.py +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/policy/model.py +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/policy/persistence.py +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/policy/value_sources.py +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/py.typed +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/read_only.py +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/redaction.py +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/resources/skills-vendor/.claude/settings.json +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/resources/skills-vendor/.claude-plugin/marketplace.json +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/resources/skills-vendor/.claude-plugin/plugin.json +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/resources/skills-vendor/.github/ISSUE_TEMPLATE/skill-rca.md +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/resources/skills-vendor/.github/pull_request_template.md +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/resources/skills-vendor/AGENTS.md +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/resources/skills-vendor/CLAUDE.md +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/resources/skills-vendor/CONTRIBUTING.md +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/resources/skills-vendor/LICENSE +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/resources/skills-vendor/README.md +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/SKILL.md +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/evals/evals.json +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/appdaemon.md +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/automation-patterns.md +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/blueprint-guide.md +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-cards.md +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-guide.md +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/device-control.md +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/domain-docs.md +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/examples.yaml +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/helper-selection.md +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/safe-refactoring.md +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/scenes.md +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/template-guidelines.md +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/yaml-only-integrations.md +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/server.py +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/settings_ui/__init__.py +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/settings_ui/_handlers_advanced.py +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/settings_ui/_handlers_backups.py +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/settings_ui/_handlers_fs.py +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/settings_ui/_handlers_server.py +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/settings_ui/_handlers_theme.py +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/settings_ui/_handlers_tools.py +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/settings_ui/_i18n.py +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/settings_ui/_persistence.py +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/settings_ui/_supervisor.py +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/settings_ui/_theme.py +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/settings_ui/_tools_meta.py +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/settings_ui/locales/de.json +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/settings_ui/locales/es.json +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/settings_ui/locales/fr.json +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/settings_ui/locales/it.json +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/settings_ui/locales/nl.json +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/settings_ui/locales/pl.json +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/settings_ui/locales/ru.json +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/settings_ui/locales/sv.json +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/settings_ui/locales/zh-Hans.json +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/settings_ui/settings.css +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/settings_ui/settings.html +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/smoke_test.py +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/stdio_settings_sidecar.py +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/strict_bps.py +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/tools/__init__.py +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/tools/auto_backup.py +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/tools/backup.py +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/tools/best_practice_checker.py +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/tools/best_practice_result.py +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/tools/best_practice_variables.py +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/tools/component_api.py +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/tools/component_config_reads.py +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/tools/component_devices.py +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/tools/component_registries.py +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/tools/component_registry_lookup.py +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/tools/config_entry_flow.py +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/tools/config_entry_flow_form.py +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/tools/config_entry_flow_menu.py +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/tools/config_entry_flow_walker.py +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/tools/device_control.py +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/tools/hacs_registration.py +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/tools/helpers.py +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/tools/radio/__init__.py +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/tools/radio/base.py +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/tools/radio/matter.py +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/tools/radio/thread.py +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/tools/radio/zigbee.py +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/tools/radio/zwave.py +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/tools/reference_validator.py +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/tools/registry.py +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/tools/smart_search/__init__.py +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/tools/smart_search/_base.py +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/tools/smart_search/_config.py +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/tools/smart_search/_deep.py +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/tools/smart_search/_entities.py +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/tools/smart_search/_fetch.py +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/tools/smart_search/_overview.py +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/tools/smart_search/_scenes.py +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/tools/smart_search/_scoring.py +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/tools/tool_search_hint_middleware.py +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/tools/tools_addons.py +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/tools/tools_areas.py +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/tools/tools_blueprints.py +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/tools/tools_bug_report.py +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/tools/tools_calendar.py +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/tools/tools_camera.py +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/tools/tools_categories.py +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/tools/tools_code.py +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/tools/tools_config_automations.py +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/tools/tools_config_dashboards.py +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/tools/tools_config_helpers.py +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/tools/tools_config_scenes.py +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/tools/tools_config_scripts.py +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/tools/tools_dashboard_screenshot.py +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/tools/tools_dev.py +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/tools/tools_energy.py +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/tools/tools_entities.py +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/tools/tools_filesystem.py +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/tools/tools_groups.py +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/tools/tools_hacs.py +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/tools/tools_history.py +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/tools/tools_integrations.py +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/tools/tools_labels.py +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/tools/tools_radio.py +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/tools/tools_registry.py +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/tools/tools_resources.py +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/tools/tools_search.py +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/tools/tools_security_policy.py +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/tools/tools_service.py +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/tools/tools_services.py +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/tools/tools_system.py +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/tools/tools_themes.py +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/tools/tools_todo.py +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/tools/tools_traces.py +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/tools/tools_updates.py +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/tools/tools_voice_assistant.py +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/tools/tools_yaml_config.py +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/tools/tools_yaml_read.py +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/tools/tools_zones.py +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/tools/util_helpers.py +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/tools/validation_middleware.py +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/transforms/__init__.py +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/transforms/categorized_search.py +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/transforms/lite_docstrings.py +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/transport_security.py +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/update_check.py +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/utils/__init__.py +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/utils/config_hash.py +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/utils/config_write_lock.py +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/utils/data_paths.py +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/utils/domain_handlers.py +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/utils/fuzzy_search.py +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/utils/kill_signal_diagnostics.py +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/utils/operation_manager.py +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/utils/python_sandbox.py +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/utils/skill_loader.py +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/utils/usage_logger.py +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/visibility/__init__.py +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/visibility/enforcement.py +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/visibility/model.py +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/visibility/persistence.py +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/visibility/resolver.py +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp_dev.egg-info/dependency_links.txt +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp_dev.egg-info/entry_points.txt +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp_dev.egg-info/requires.txt +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp_dev.egg-info/top_level.txt +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/tests/__init__.py +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/tests/test_constants.py +0 -0
- {ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/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 = "8.2.0.
|
|
7
|
+
version = "8.2.0.dev2323"
|
|
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
|
|
@@ -40,6 +40,10 @@ logger = logging.getLogger(__name__)
|
|
|
40
40
|
# so the request did not execute and retrying is safe even for writes.
|
|
41
41
|
_RETRYABLE_STATUS = frozenset({502, 503, 504})
|
|
42
42
|
_MAX_REQUEST_ATTEMPTS = 3
|
|
43
|
+
# Journald window requested for the Core error log on Supervisor-backed
|
|
44
|
+
# installs. Both such branches of get_error_log() build their request from this
|
|
45
|
+
# constant, so the window they ask for cannot drift apart.
|
|
46
|
+
_ERROR_LOG_LINES = 20000
|
|
43
47
|
|
|
44
48
|
|
|
45
49
|
class HomeAssistantError(Exception):
|
|
@@ -586,7 +590,11 @@ class HomeAssistantClient:
|
|
|
586
590
|
"""
|
|
587
591
|
if is_running_in_addon():
|
|
588
592
|
logger.debug("Fetching error log via Supervisor direct (core service)")
|
|
589
|
-
|
|
593
|
+
# An explicit `lines` is required: without it Supervisor applies its
|
|
594
|
+
# 100-line default, which is far too short a slice to tell what keeps
|
|
595
|
+
# repeating. `_get_supervisor_log` plumbs the same parameter for the
|
|
596
|
+
# same reason (#1734).
|
|
597
|
+
return await self._supervisor_logs_get("core", lines=_ERROR_LOG_LINES)
|
|
590
598
|
|
|
591
599
|
if await self._is_supervised_install():
|
|
592
600
|
logger.debug(
|
|
@@ -594,7 +602,7 @@ class HomeAssistantClient:
|
|
|
594
602
|
)
|
|
595
603
|
raw_response = await self._raw_request(
|
|
596
604
|
"GET",
|
|
597
|
-
"/hassio/core/logs?lines=
|
|
605
|
+
f"/hassio/core/logs?lines={_ERROR_LOG_LINES}",
|
|
598
606
|
headers={"Accept": "text/plain"},
|
|
599
607
|
)
|
|
600
608
|
return raw_response.text
|
|
@@ -619,11 +627,21 @@ class HomeAssistantClient:
|
|
|
619
627
|
glitch or HTTP error returns False without setting the cache,
|
|
620
628
|
so the next call re-probes. This is the only path that
|
|
621
629
|
intentionally fails open — caller proceeds on the historical
|
|
622
|
-
Container branch
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
630
|
+
Container branch.
|
|
631
|
+
|
|
632
|
+
A 401 is deliberately excluded from that fail-open: it is a
|
|
633
|
+
definitive answer about the credential rather than a transient
|
|
634
|
+
glitch, and swallowing it strands exactly the install class this
|
|
635
|
+
probe exists for. Failing open sends a Supervised caller down
|
|
636
|
+
the Container branch to ``/api/error_log``, which HA Core never
|
|
637
|
+
registers under ``SUPERVISOR`` (see ``get_error_log``), so the
|
|
638
|
+
auth failure resurfaces as a 404 and the caller answers with
|
|
639
|
+
connection advice instead of "re-create the LLAT". Letting
|
|
640
|
+
``HomeAssistantAuthError`` propagate reaches the auth handler
|
|
641
|
+
directly. Transport / HTTP fail-open is unchanged.
|
|
642
|
+
|
|
643
|
+
Connection / HTTP errors are caught explicitly; runtime bugs
|
|
644
|
+
(TypeError, AttributeError) and BaseException derivatives
|
|
627
645
|
(KeyboardInterrupt, CancelledError) deliberately propagate so
|
|
628
646
|
they're not silenced as "not supervised".
|
|
629
647
|
"""
|
|
@@ -632,16 +650,17 @@ class HomeAssistantClient:
|
|
|
632
650
|
try:
|
|
633
651
|
config = await self._request("GET", "/config")
|
|
634
652
|
except (
|
|
635
|
-
HomeAssistantAuthError,
|
|
636
653
|
HomeAssistantAPIError,
|
|
637
654
|
HomeAssistantConnectionError,
|
|
638
655
|
httpx.HTTPError,
|
|
639
656
|
TimeoutError,
|
|
640
657
|
) as exc:
|
|
641
|
-
# Fail-open on transport / HTTP-layer failures only.
|
|
642
|
-
#
|
|
643
|
-
#
|
|
644
|
-
#
|
|
658
|
+
# Fail-open on transport / HTTP-layer failures only.
|
|
659
|
+
# HomeAssistantAuthError is deliberately absent: a 401 is a
|
|
660
|
+
# definitive verdict on the credential, and failing open on
|
|
661
|
+
# it routes a Supervised caller to /api/error_log — a route
|
|
662
|
+
# that does not exist there — turning the auth error into a
|
|
663
|
+
# 404 (see the docstring).
|
|
645
664
|
# Logged at WARNING so it's visible at default log levels
|
|
646
665
|
# without spamming on every call (probe runs at most once
|
|
647
666
|
# per session per outcome).
|
{ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/settings_ui/locales/README.md
RENAMED
|
@@ -37,11 +37,16 @@ register the engine imitates, a component catalog left at a key or two rests
|
|
|
37
37
|
entirely on whichever of them addresses the reader —
|
|
38
38
|
`test_every_shipped_component_catalog_gets_reader_addressing_samples` pins that
|
|
39
39
|
one. Leaving that catalog empty is fine, but the moment you author anything in
|
|
40
|
-
it, at least
|
|
41
|
-
second person, and
|
|
40
|
+
it, at least two keys must be ones whose English addresses the reader in the
|
|
41
|
+
second person, and each must carry a non-empty translation — a key left blank is
|
|
42
42
|
skipped like a missing one rather than sampled empty. Most component strings do
|
|
43
43
|
not address the reader, so starting at the top of the file leaves the catalog
|
|
44
|
-
anchorless and that check red until you add
|
|
44
|
+
anchorless and that check red until you add ones that do. Two rather than one
|
|
45
|
+
because the run likeliest to need the register is the one that rewords such a
|
|
46
|
+
key: that queues it, and a queued key is dropped from the candidates, so a
|
|
47
|
+
surface resting on a single anchor loses its register in exactly that run —
|
|
48
|
+
`test_component_samples_survive_their_own_anchor_being_queued` pins the
|
|
49
|
+
survival. To fill them in your own PR instead, run
|
|
45
50
|
`scripts/translate_locales.py` yourself and review its output like any
|
|
46
51
|
other diff. Also add the new code to the locale list in the repository-root
|
|
47
52
|
`AGENTS.md`
|
{ha_mcp_dev-8.2.0.dev2320 → ha_mcp_dev-8.2.0.dev2323}/src/ha_mcp/settings_ui/locales/en.json
RENAMED
|
@@ -128,7 +128,7 @@
|
|
|
128
128
|
"features.enable_strict_mandatory_bps.label": "Strict best-practices mode",
|
|
129
129
|
"features.enable_strict_mandatory_bps.help": "Strict mode: prevents the client from using the tool until it can prove that it read the best practices. While on, the six best-practice write tools (automations, scripts, scenes, helpers, dashboards, raw YAML) are blocked and return an error directing the client to read the best-practices skill via ha_get_skill_guide and pass back the acknowledgment key it obtains there. While on, the ha_get_skill_guide tool is locked enabled — it is the only publisher of the acknowledgment key. Child of the \"Attach best-practice skills on writes\" option above and inert while that parent is off. Requires restart to take effect.",
|
|
130
130
|
"features.enable_beta_features.label": "Enable beta features (master)",
|
|
131
|
-
"features.enable_beta_features.help": "⚠ DANGER — these tools can PERMANENTLY DAMAGE your Home Assistant installation. They write to your YAML config, your filesystem, install custom components, run arbitrary sandboxed Python, and edit tool docstrings the AI sees. There is no warranty and no support guarantee — you enable them at your OWN RISK. Take a Home Assistant backup before turning this on, and never enable in production without one. Master gate for the
|
|
131
|
+
"features.enable_beta_features.help": "⚠ DANGER — these tools can PERMANENTLY DAMAGE your Home Assistant installation. They write to your YAML config, your filesystem, install custom components, run arbitrary sandboxed Python, and edit tool docstrings the AI sees. There is no warranty and no support guarantee — you enable them at your OWN RISK. Take a Home Assistant backup before turning this on, and never enable in production without one. Master gate for the experimental sub-flags below; sub-flags are ignored at runtime while this master is off, even when explicitly set to true. The same toggle is also surfaced in the web settings UI under \"Beta features (dangerous)\" — either surface reflects the other on restart.",
|
|
132
132
|
"features.enable_yaml_config_editing.label": "Enable YAML config editing (beta)",
|
|
133
133
|
"features.enable_yaml_config_editing.help": "Beta feature. Allows AI assistants to add, replace, or remove top-level keys in configuration.yaml and packages/*.yaml. Only whitelisted keys are allowed (e.g., template, sensor, command_line, mqtt, knx); core keys like homeassistant, http, and recorder are blocked. Each edit validates YAML syntax, runs a config check, and creates an automatic backup. Changes to most keys require a full HA restart to take effect. See docs/beta.md for known limitations. Dedicated tools (automations, scripts, scenes, helpers, template sensors) should be preferred when available. REQUIRES the master \"Enable beta features\" toggle above (and in the web UI) to be on — otherwise this sub-flag is ignored at runtime regardless of its value here.",
|
|
134
134
|
"features.enable_yaml_edit_confirm.label": "Require confirmation for YAML edits (diff preview)",
|
|
@@ -441,7 +441,7 @@
|
|
|
441
441
|
"addon.enable_security_policy_tool.name": "Let AI agents edit tool security policies",
|
|
442
442
|
"addon.enable_security_policy_tool.description": "Registers the ha_manage_security_policy tool, which lets connected AI agents read and rewrite the tool security policies — including removing approval gates. Off by default. Independent of the Tool Security Policies option above: this controls who can edit the rules, not whether they are enforced. The tool cannot see or decide pending approvals. To keep a human in the loop, gate the tool before enabling it: switch on \"security gated\" for Manage Security Policy in the Tools tab of the web UI. Requires restart to take effect.",
|
|
443
443
|
"addon.enable_auto_backup.name": "Enable auto-backup of edits",
|
|
444
|
-
"addon.enable_auto_backup.description": "Captures a per-entity snapshot before every wrapped write/destructive MCP tool call (automation, script, scene, helper, dashboard, label, category, group, zone, area, calendar, todo, entity, integration, and sibling remove/delete tools). Snapshots are saved as YAML files under /data/ha_mcp_backups/ (override via HAMCP_BACKUP_DIR) and listed, restored, or deleted via the Backups tab in the web settings UI or via ha_manage_backup(scope='edits', ...).
|
|
444
|
+
"addon.enable_auto_backup.description": "Captures a per-entity snapshot before every wrapped write/destructive MCP tool call (automation, script, scene, helper, dashboard, dashboard resource, label, category, group, zone, area, calendar, todo, entity, device, integration, and sibling remove/delete tools). The auto-backup throttle bounds how often that happens: 0 (default) captures on every such call, N>0 at most one snapshot per N minutes per entity. Snapshots are saved as YAML files under /data/ha_mcp_backups/ (override via HAMCP_BACKUP_DIR) and listed, restored, or deleted via the Backups tab in the web settings UI or via ha_manage_backup(scope='edits', ...). For those domains the capture is best-effort — a failure logs a WARNING and the write proceeds. The file and raw-YAML tools are the exception: ha_write_file, ha_delete_file and ha_config_set_yaml treat the snapshot as a precondition, so a write through them is refused while this is off, and a failed capture blocks it. On by default; unchecking opts out of the best-effort captures and takes the file and raw-YAML tools out of service with them. Requires restart to take effect.",
|
|
445
445
|
"addon.auto_backup_throttle_minutes.name": "Auto-backup throttle (minutes)",
|
|
446
446
|
"addon.auto_backup_throttle_minutes.description": "Per-entity throttle window. 0 (default) captures a snapshot on every wrapped write. N>0 captures at most one snapshot per N minutes per entity. Range 0–1440.",
|
|
447
447
|
"addon.auto_backup_retain_per_entity.name": "Auto-backup retention (per entity)",
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"meta": {
|
|
3
|
+
"native_name": "Esperanto",
|
|
4
|
+
"dir": "ltr"
|
|
5
|
+
},
|
|
6
|
+
"messages": {
|
|
7
|
+
"accessibility.storage_blocked": "Via retumilo blokas la konservadon de retejaj datumoj; viaj elektoj validas nur por ĉi tiu seanco.",
|
|
8
|
+
"actions.approve": "Aprobi",
|
|
9
|
+
"actions.deny": "Rifuzi",
|
|
10
|
+
"notice.restart.standalone": "⚠ Ŝanĝoj konservitaj. Restartigu la procezon ha-mcp, poste aktualigu la liston de MCP-iloj en via AI-kliento.",
|
|
11
|
+
"policies.operators.exists": "ekzistas",
|
|
12
|
+
"policies.operators.exists_long": "ekzistas (iu ajn valoro)",
|
|
13
|
+
"policies.operators.eq": "egalas",
|
|
14
|
+
"policies.operators.neq": "NE egalas",
|
|
15
|
+
"policies.operators.in": "estas unu el",
|
|
16
|
+
"policies.operators.not_in": "NE estas unu el",
|
|
17
|
+
"policies.operators.contains": "enhavas",
|
|
18
|
+
"policies.operators.regex": "kongruas kun regula esprimo",
|
|
19
|
+
"policies.operators.gt": "estas pli granda ol",
|
|
20
|
+
"policies.operators.lt": "estas pli malgranda ol",
|
|
21
|
+
"policies.pending.already_decided": "Ĉi tiu aprobo jam estis {decision}, eble de alia langeto aŭ seanco.",
|
|
22
|
+
"policies.pending.decision.approved": "aprobita",
|
|
23
|
+
"policies.pending.decision.denied": "rifuzita",
|
|
24
|
+
"visibility.errors.conflict": "La agordo estis ŝanĝita en alia langeto aŭ seanco. Reŝargu la paĝon, poste reapliku viajn ŝanĝojn."
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -1936,7 +1936,7 @@ const FEATURE_META = {
|
|
|
1936
1936
|
},
|
|
1937
1937
|
enable_beta_features: {
|
|
1938
1938
|
label: "Enable beta features (master)",
|
|
1939
|
-
help: "⚠ DANGER — these tools can PERMANENTLY DAMAGE your Home Assistant installation. They write to your YAML config, your filesystem, install custom components, run arbitrary sandboxed Python, and edit tool docstrings the AI sees. There is no warranty and no support guarantee — you enable them at your OWN RISK. Take a Home Assistant backup before turning this on, and never enable in production without one. Master gate for the
|
|
1939
|
+
help: "⚠ DANGER — these tools can PERMANENTLY DAMAGE your Home Assistant installation. They write to your YAML config, your filesystem, install custom components, run arbitrary sandboxed Python, and edit tool docstrings the AI sees. There is no warranty and no support guarantee — you enable them at your OWN RISK. Take a Home Assistant backup before turning this on, and never enable in production without one. Master gate for the experimental sub-flags below; sub-flags are ignored at runtime while this master is off, even when explicitly set to true. The same toggle is also surfaced in the web settings UI under \"Beta features (dangerous)\" — either surface reflects the other on restart.",
|
|
1940
1940
|
},
|
|
1941
1941
|
enable_yaml_config_editing: {
|
|
1942
1942
|
label: "Enable YAML config editing (beta)",
|
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
"""Structured summarisation of Home Assistant's raw error log.
|
|
2
|
+
|
|
3
|
+
``ha_get_logs(source='error_log')`` returns raw text by default. A busy
|
|
4
|
+
instance produces a 50-200 KB log in which the same handful of errors repeat
|
|
5
|
+
thousands of times, so the raw form can exhaust an agent's context while
|
|
6
|
+
conveying very little. This module collapses it into counted, deduplicated,
|
|
7
|
+
component-grouped issues.
|
|
8
|
+
|
|
9
|
+
Split out of ``tools_utility`` under AGENTS.md § Module Size: the parsing is
|
|
10
|
+
self-contained and shares nothing with the tool plumbing but its input string.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import re
|
|
14
|
+
from typing import Any, NamedTuple
|
|
15
|
+
|
|
16
|
+
# Full HA log line, e.g.
|
|
17
|
+
# "2026-05-27 10:15:23.456 ERROR (MainThread) [homeassistant.components.zha] msg"
|
|
18
|
+
# Distinct from `_LOG_LEVEL_RE` in `tools_utility`, which only sniffs the level
|
|
19
|
+
# out of a line for the raw path's filter.
|
|
20
|
+
# The thread field is matched lazily instead of as `\([^)]*\)`: since Python 3.10
|
|
21
|
+
# an unnamed thread is called "Thread-1 (target_fn)", so the field itself can
|
|
22
|
+
# contain parentheses, and a class that cannot cross them drops every line those
|
|
23
|
+
# threads log.
|
|
24
|
+
_HA_LOG_LINE_RE = re.compile(
|
|
25
|
+
r"^(\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}:\d{2}(?:\.\d+)?)"
|
|
26
|
+
r"\s+(DEBUG|INFO|WARNING|ERROR|CRITICAL)"
|
|
27
|
+
r"\s+\(.*?\)"
|
|
28
|
+
r"\s+\[([^\]]+)\]"
|
|
29
|
+
r"\s+(.+)$",
|
|
30
|
+
re.IGNORECASE,
|
|
31
|
+
)
|
|
32
|
+
_MAX_MESSAGE_LEN = 200
|
|
33
|
+
_COMPONENT_PREFIX_DEPTH = 3
|
|
34
|
+
_CUSTOM_COMPONENT_PREFIX_DEPTH = 2
|
|
35
|
+
_DEFAULT_TOP_N = 20
|
|
36
|
+
_MAX_COMPONENTS = 50
|
|
37
|
+
# Marks a message the 200-char cap cut short, so a truncated entry is never
|
|
38
|
+
# mistaken for the whole message.
|
|
39
|
+
_TRUNCATION_MARK = "…[truncated]"
|
|
40
|
+
# Tie-breaker for issues with equal counts: a same-count CRITICAL outranks an
|
|
41
|
+
# INFO. Ordering only — not a filter, and not tied to VALID_LOG_LEVELS.
|
|
42
|
+
_LEVEL_SEVERITY = {"CRITICAL": 4, "ERROR": 3, "WARNING": 2, "INFO": 1, "DEBUG": 0}
|
|
43
|
+
# Shared by both response paths, because an empty fetch reads as an all-clear on
|
|
44
|
+
# either one: an agent that receives no log content and no warning reports "no
|
|
45
|
+
# errors" to the user. Only the remedy differs, so each path appends its own.
|
|
46
|
+
_EMPTY_FETCH_WARNING = (
|
|
47
|
+
"A running Home Assistant always logs something, so this is an empty or "
|
|
48
|
+
"failed fetch, NOT evidence that the log is clean."
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
# Supervisor-backed installs (add-on, HAOS, supervised) read HA Core's journald
|
|
52
|
+
# stream, where every line is wrapped in ANSI colour codes:
|
|
53
|
+
# "\x1b[31m2026-08-02 08:12:27.290 ERROR (MainThread) [x] msg\x1b[0m"
|
|
54
|
+
# Only container/pip installs read the plain home-assistant.log file.
|
|
55
|
+
# Two independent reasons to strip, not one chain:
|
|
56
|
+
# * the leading code alone is already fatal — _HA_LOG_LINE_RE is ^-anchored
|
|
57
|
+
# and str.strip() does not remove ESC, so the line matches nothing and no
|
|
58
|
+
# `message` is produced at all;
|
|
59
|
+
# * the trailing reset is what a leading-only strip would leave behind: it
|
|
60
|
+
# lands inside `message` and splits the dedup key for one recurring error.
|
|
61
|
+
# Strip both ends, always.
|
|
62
|
+
_ANSI_RE = re.compile(r"\x1b\[[0-9;]*[A-Za-z]")
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _strip_ansi(text: str) -> str:
|
|
66
|
+
"""Remove ANSI escape sequences (journald colour codes) from a log line."""
|
|
67
|
+
return _ANSI_RE.sub("", text)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _normalize_timestamp(timestamp: str) -> str:
|
|
71
|
+
"""Put a matched timestamp on one date/time separator.
|
|
72
|
+
|
|
73
|
+
Every timestamp comparison downstream is lexicographic — the covered
|
|
74
|
+
window, an issue's ``first_seen``/``last_seen`` bounds, and the recency
|
|
75
|
+
tiebreaker. ``_HA_LOG_LINE_RE`` accepts both the space and the ``T`` form
|
|
76
|
+
(case-insensitively), and ``" "`` sorts below every digit while ``"T"``
|
|
77
|
+
sorts above them, so a stream mixing the two forms would corrupt all three
|
|
78
|
+
at once. HA core emits the space form, so normalize onto that: real logs
|
|
79
|
+
come out byte-identical and only the ISO-8601 variant is rewritten.
|
|
80
|
+
|
|
81
|
+
The date is fixed-width in the pattern (``\\d{4}-\\d{2}-\\d{2}``), so the
|
|
82
|
+
separator is always at index 10 — replacing by position also covers the
|
|
83
|
+
lowercase ``t`` that ``re.IGNORECASE`` admits.
|
|
84
|
+
"""
|
|
85
|
+
return timestamp[:10] + " " + timestamp[11:]
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _get_component_prefix(logger_name: str) -> str:
|
|
89
|
+
"""Extract the component prefix (first N dotted segments) of a logger name.
|
|
90
|
+
|
|
91
|
+
A custom integration's identity is ``custom_components.<domain>`` — one
|
|
92
|
+
segment shallower than a core component — so its modules roll up into one
|
|
93
|
+
bucket instead of one per module.
|
|
94
|
+
"""
|
|
95
|
+
parts = logger_name.split(".")
|
|
96
|
+
depth = (
|
|
97
|
+
_CUSTOM_COMPONENT_PREFIX_DEPTH
|
|
98
|
+
if parts[0] == "custom_components"
|
|
99
|
+
else _COMPONENT_PREFIX_DEPTH
|
|
100
|
+
)
|
|
101
|
+
if len(parts) >= depth:
|
|
102
|
+
return ".".join(parts[:depth])
|
|
103
|
+
return logger_name
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _truncate_message(message: str) -> str:
|
|
107
|
+
"""Cap a message for display, marking it when it was cut."""
|
|
108
|
+
if len(message) <= _MAX_MESSAGE_LEN:
|
|
109
|
+
return message
|
|
110
|
+
return message[:_MAX_MESSAGE_LEN] + _TRUNCATION_MARK
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
class _ExtractedLines(NamedTuple):
|
|
114
|
+
"""Tallies from one pass over the raw log; every input line lands in one.
|
|
115
|
+
|
|
116
|
+
``matched`` counts lines that are valid HA log lines *before* ``level`` and
|
|
117
|
+
``search`` run; ``entries`` holds the subset that survived them. The two are
|
|
118
|
+
kept apart because they answer different questions: only ``matched == 0``
|
|
119
|
+
means the parser could not read the log, while ``matched > 0`` with no
|
|
120
|
+
entries just means the filters excluded everything.
|
|
121
|
+
|
|
122
|
+
``total input lines == blank + unparseable + matched`` holds by construction.
|
|
123
|
+
"""
|
|
124
|
+
|
|
125
|
+
entries: list[dict[str, str]]
|
|
126
|
+
matched: int
|
|
127
|
+
unparseable: int
|
|
128
|
+
blank: int
|
|
129
|
+
window_start: str | None
|
|
130
|
+
window_end: str | None
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _extract_log_entries(
|
|
134
|
+
lines: list[str],
|
|
135
|
+
search: str | None = None,
|
|
136
|
+
level: str | None = None,
|
|
137
|
+
) -> _ExtractedLines:
|
|
138
|
+
"""Parse raw log lines into entries plus the per-line tallies around them.
|
|
139
|
+
|
|
140
|
+
Split out from ``_parse_error_log_structured`` to keep both under the repo's
|
|
141
|
+
complexity ceiling. A line counts as unparseable only when it does not match
|
|
142
|
+
the HA log format at all — lines dropped by ``level``/``search`` are simply
|
|
143
|
+
absent from ``entries``, never counted as unparseable.
|
|
144
|
+
"""
|
|
145
|
+
entries: list[dict[str, str]] = []
|
|
146
|
+
matched = 0
|
|
147
|
+
unparseable = 0
|
|
148
|
+
blank = 0
|
|
149
|
+
window_start: str | None = None
|
|
150
|
+
window_end: str | None = None
|
|
151
|
+
needle = search.lower() if search else None
|
|
152
|
+
|
|
153
|
+
for line in lines:
|
|
154
|
+
# Strip ANSI *before* matching: on Supervisor-backed installs the line
|
|
155
|
+
# arrives colour-wrapped. Both ends matter for separate reasons — the
|
|
156
|
+
# leading code alone already defeats the ^-anchor so the line matches
|
|
157
|
+
# nothing, and a leading-only strip would leave the trailing reset
|
|
158
|
+
# inside `message`, splitting the dedup key for one recurring error.
|
|
159
|
+
clean = _strip_ansi(line).strip()
|
|
160
|
+
if not clean:
|
|
161
|
+
blank += 1
|
|
162
|
+
continue
|
|
163
|
+
match = _HA_LOG_LINE_RE.match(clean)
|
|
164
|
+
if not match:
|
|
165
|
+
unparseable += 1
|
|
166
|
+
continue
|
|
167
|
+
matched += 1
|
|
168
|
+
timestamp, log_level, logger_name, message = match.groups()
|
|
169
|
+
timestamp = _normalize_timestamp(timestamp)
|
|
170
|
+
# Widen the covered window on every parseable line, filtered or not:
|
|
171
|
+
# it describes the log slice that was read, not the summary's contents.
|
|
172
|
+
if window_start is None or timestamp < window_start:
|
|
173
|
+
window_start = timestamp
|
|
174
|
+
if window_end is None or timestamp > window_end:
|
|
175
|
+
window_end = timestamp
|
|
176
|
+
log_level = log_level.upper()
|
|
177
|
+
if level and log_level != level:
|
|
178
|
+
continue
|
|
179
|
+
if (
|
|
180
|
+
needle
|
|
181
|
+
and needle not in message.lower()
|
|
182
|
+
and needle not in logger_name.lower()
|
|
183
|
+
):
|
|
184
|
+
continue
|
|
185
|
+
entries.append(
|
|
186
|
+
{
|
|
187
|
+
"timestamp": timestamp,
|
|
188
|
+
"level": log_level,
|
|
189
|
+
"logger": logger_name,
|
|
190
|
+
# Kept whole: the display cap is applied at dedup time, because
|
|
191
|
+
# keying on a capped message merges two errors that differ only
|
|
192
|
+
# past the cap into one issue with a summed count.
|
|
193
|
+
"message": message,
|
|
194
|
+
}
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
return _ExtractedLines(
|
|
198
|
+
entries=entries,
|
|
199
|
+
matched=matched,
|
|
200
|
+
unparseable=unparseable,
|
|
201
|
+
blank=blank,
|
|
202
|
+
window_start=window_start,
|
|
203
|
+
window_end=window_end,
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def _parse_error_log_structured(
|
|
208
|
+
raw_text: str,
|
|
209
|
+
search: str | None = None,
|
|
210
|
+
level: str | None = None,
|
|
211
|
+
top_n: int = _DEFAULT_TOP_N,
|
|
212
|
+
) -> dict[str, Any]:
|
|
213
|
+
"""Parse the raw error log into a deduplicated, component-grouped summary.
|
|
214
|
+
|
|
215
|
+
``source='error_log'`` normally returns raw text. A busy instance produces a
|
|
216
|
+
50-200 KB log in which the same handful of errors repeat thousands of times,
|
|
217
|
+
so the raw form can exhaust an agent's context while conveying very little.
|
|
218
|
+
This collapses identical (level, logger, message) triples into counted
|
|
219
|
+
issues, groups them by component, and returns only the ``top_n`` noisiest.
|
|
220
|
+
|
|
221
|
+
Every input line is accounted for exactly once, because "not in the summary"
|
|
222
|
+
has several causes that must not be conflated:
|
|
223
|
+
|
|
224
|
+
* ``total_raw_lines`` - lines in the input, and the sum of the next three
|
|
225
|
+
* ``blank_lines`` - empty after ANSI stripping
|
|
226
|
+
* ``unparseable_lines`` - not the HA log format at all (traceback bodies,
|
|
227
|
+
continuation lines, format drift)
|
|
228
|
+
* ``matched_lines`` - valid log lines, before ``level``/``search``
|
|
229
|
+
* ``parsed_entries`` - the subset of ``matched_lines`` the filters kept
|
|
230
|
+
|
|
231
|
+
Only ``matched_lines == 0`` says the parser could not read the log; a small
|
|
232
|
+
``parsed_entries`` under a narrow filter is the feature working, not data
|
|
233
|
+
loss. Counts are bounded by the fetched window (``window_start`` ..
|
|
234
|
+
``window_end``): Supervisor-backed installs read a capped journald slice, so
|
|
235
|
+
there the window can be far shorter than the log's full history.
|
|
236
|
+
"""
|
|
237
|
+
lines = raw_text.splitlines() if raw_text else []
|
|
238
|
+
total_raw_lines = len(lines)
|
|
239
|
+
extracted = _extract_log_entries(lines, search=search, level=level)
|
|
240
|
+
parsed = extracted.entries
|
|
241
|
+
|
|
242
|
+
# Dedupe on (level, logger, message), keyed on the FULL message: two errors
|
|
243
|
+
# that differ only past the display cap are distinct issues, and a capped
|
|
244
|
+
# key merges them into one with a summed count. Level belongs in the key
|
|
245
|
+
# too: the same text is logged at different levels, and a level-less key
|
|
246
|
+
# would report a later ERROR under the level of the first line to carry it.
|
|
247
|
+
dedup: dict[tuple[str, str, str], dict[str, Any]] = {}
|
|
248
|
+
for entry in parsed:
|
|
249
|
+
key = (entry["level"], entry["logger"], entry["message"])
|
|
250
|
+
record = dedup.get(key)
|
|
251
|
+
if record is None:
|
|
252
|
+
record = {
|
|
253
|
+
"logger": entry["logger"],
|
|
254
|
+
"level": entry["level"],
|
|
255
|
+
"message": _truncate_message(entry["message"]),
|
|
256
|
+
"count": 0,
|
|
257
|
+
"first_seen": entry["timestamp"],
|
|
258
|
+
"last_seen": entry["timestamp"],
|
|
259
|
+
}
|
|
260
|
+
dedup[key] = record
|
|
261
|
+
record["count"] += 1
|
|
262
|
+
# Bounds, not last-write-wins: a log whose lines are not perfectly
|
|
263
|
+
# ordered would otherwise drag last_seen backwards, which also
|
|
264
|
+
# misplaces the issue in the recency tiebreaker below.
|
|
265
|
+
record["first_seen"] = min(record["first_seen"], entry["timestamp"])
|
|
266
|
+
record["last_seen"] = max(record["last_seen"], entry["timestamp"])
|
|
267
|
+
|
|
268
|
+
# Count first, then severity, then recency. Python's sort is stable, so
|
|
269
|
+
# count alone would return the *oldest* of a tied run — and on a log whose
|
|
270
|
+
# messages embed volatile ids every issue ties at count 1, which quietly
|
|
271
|
+
# turns "top 20 issues" into "the 20 oldest".
|
|
272
|
+
all_issues = sorted(
|
|
273
|
+
dedup.values(),
|
|
274
|
+
key=lambda i: (
|
|
275
|
+
i["count"],
|
|
276
|
+
_LEVEL_SEVERITY.get(i["level"], 0),
|
|
277
|
+
i["last_seen"],
|
|
278
|
+
),
|
|
279
|
+
reverse=True,
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
by_component: dict[str, dict[str, Any]] = {}
|
|
283
|
+
for issue in all_issues:
|
|
284
|
+
prefix = _get_component_prefix(issue["logger"])
|
|
285
|
+
bucket = by_component.setdefault(
|
|
286
|
+
prefix, {"total_occurrences": 0, "issue_count": 0}
|
|
287
|
+
)
|
|
288
|
+
bucket["total_occurrences"] += issue["count"]
|
|
289
|
+
bucket["issue_count"] += 1
|
|
290
|
+
|
|
291
|
+
top_issues = [
|
|
292
|
+
{
|
|
293
|
+
"component": _get_component_prefix(issue["logger"]),
|
|
294
|
+
"logger": issue["logger"],
|
|
295
|
+
"level": issue["level"],
|
|
296
|
+
"message": issue["message"],
|
|
297
|
+
"count": issue["count"],
|
|
298
|
+
"first_seen": issue["first_seen"],
|
|
299
|
+
"last_seen": issue["last_seen"],
|
|
300
|
+
}
|
|
301
|
+
for issue in all_issues[:top_n]
|
|
302
|
+
]
|
|
303
|
+
|
|
304
|
+
# Cap by_component too: a log with thousands of distinct loggers would
|
|
305
|
+
# otherwise make the response unbounded, contradicting the whole premise.
|
|
306
|
+
ranked_components = sorted(
|
|
307
|
+
by_component.items(),
|
|
308
|
+
key=lambda kv: kv[1]["total_occurrences"],
|
|
309
|
+
reverse=True,
|
|
310
|
+
)
|
|
311
|
+
component_table = dict(ranked_components[:_MAX_COMPONENTS])
|
|
312
|
+
|
|
313
|
+
summary: dict[str, Any] = {
|
|
314
|
+
"total_raw_lines": total_raw_lines,
|
|
315
|
+
"blank_lines": extracted.blank,
|
|
316
|
+
"unparseable_lines": extracted.unparseable,
|
|
317
|
+
"matched_lines": extracted.matched,
|
|
318
|
+
"parsed_entries": len(parsed),
|
|
319
|
+
"unique_issues": len(all_issues),
|
|
320
|
+
"components_affected": len(by_component),
|
|
321
|
+
"showing_top_n": min(top_n, len(all_issues)),
|
|
322
|
+
"showing_components": len(component_table),
|
|
323
|
+
# Counts describe this window only. On Supervisor-backed installs the
|
|
324
|
+
# fetch is a capped journald slice, so an issue's count here is not its
|
|
325
|
+
# count since it first occurred.
|
|
326
|
+
"window_start": extracted.window_start,
|
|
327
|
+
"window_end": extracted.window_end,
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
result: dict[str, Any] = {
|
|
331
|
+
"success": True,
|
|
332
|
+
"source": "error_log",
|
|
333
|
+
"structured": True,
|
|
334
|
+
"summary": summary,
|
|
335
|
+
"top_issues": top_issues,
|
|
336
|
+
"by_component": component_table,
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
# An empty summary has three causes and they need different answers, so the
|
|
340
|
+
# branch keys on `matched_lines` — the pre-filter count. Keying on the raw
|
|
341
|
+
# unparseable count instead reports format drift for an ordinary filtered
|
|
342
|
+
# result, because tracebacks alone push a healthy log past 90% unparseable.
|
|
343
|
+
# Degraded-operation notices go in the top-level `warnings` list — the one
|
|
344
|
+
# channel this repo's tools use — never a singular `warning` string.
|
|
345
|
+
if extracted.matched == 0:
|
|
346
|
+
if extracted.unparseable > 0:
|
|
347
|
+
result["warnings"] = [
|
|
348
|
+
f"No log lines could be parsed ({extracted.unparseable} of "
|
|
349
|
+
f"{total_raw_lines} did not match the expected Home Assistant log "
|
|
350
|
+
"format). This summary is NOT evidence that the log is clean - "
|
|
351
|
+
"re-run with structured=False to read the raw text."
|
|
352
|
+
]
|
|
353
|
+
else:
|
|
354
|
+
result["warnings"] = [
|
|
355
|
+
f"No log entries arrived to parse ({total_raw_lines} lines "
|
|
356
|
+
f"fetched, none of them log entries). {_EMPTY_FETCH_WARNING} "
|
|
357
|
+
"Re-run with structured=False to see the raw response."
|
|
358
|
+
]
|
|
359
|
+
elif not parsed:
|
|
360
|
+
result["warnings"] = [
|
|
361
|
+
f"None of the {extracted.matched} parsed log entries matched the "
|
|
362
|
+
"requested filters. The log itself parsed fine, so this reflects "
|
|
363
|
+
"the filters, not a parse failure."
|
|
364
|
+
]
|
|
365
|
+
return result
|