ha-mcp-dev 7.13.0.dev2003__tar.gz → 7.13.0.dev2007__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {ha_mcp_dev-7.13.0.dev2003/src/ha_mcp_dev.egg-info → ha_mcp_dev-7.13.0.dev2007}/PKG-INFO +1 -1
- {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/pyproject.toml +1 -10
- {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/tools/radio/matter.py +7 -0
- {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/tools/radio/zigbee.py +15 -0
- {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/tools/radio/zwave.py +14 -0
- {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/tools/reference_validator.py +58 -35
- {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/tools/tools_areas.py +205 -141
- {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/tools/tools_history.py +83 -57
- {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/tools/tools_search.py +5 -1
- {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/tools/tools_system.py +5 -1
- {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/tools/tools_zones.py +67 -52
- {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/tools/util_helpers.py +7 -2
- {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/transforms/categorized_search.py +102 -79
- {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/visibility/resolver.py +151 -71
- {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007/src/ha_mcp_dev.egg-info}/PKG-INFO +1 -1
- {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/LICENSE +0 -0
- {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/MANIFEST.in +0 -0
- {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/README.md +0 -0
- {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/setup.cfg +0 -0
- {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/__init__.py +0 -0
- {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/__main__.py +0 -0
- {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/_pypi_marker +0 -0
- {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/_version.py +0 -0
- {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/auth/__init__.py +0 -0
- {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/auth/consent_form.py +0 -0
- {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/auth/provider.py +0 -0
- {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/backup_manager.py +0 -0
- {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/browser_landing.py +0 -0
- {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/client/__init__.py +0 -0
- {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/client/rest_client.py +0 -0
- {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/client/supervisor_client.py +0 -0
- {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/client/websocket_client.py +0 -0
- {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/client/websocket_listener.py +0 -0
- {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/config.py +0 -0
- {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/dashboard_screenshot/__init__.py +0 -0
- {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/dashboard_screenshot/capture.py +0 -0
- {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/dashboard_screenshot/content.py +0 -0
- {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/dashboard_screenshot/paths.py +0 -0
- {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/dashboard_screenshot/provision.py +0 -0
- {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/dashboard_screenshot/theme_guard.py +0 -0
- {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/errors.py +0 -0
- {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/llm_exposure.py +0 -0
- {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/policy/__init__.py +0 -0
- {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/policy/approval_queue.py +0 -0
- {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/policy/evaluator.py +0 -0
- {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/policy/handlers.py +0 -0
- {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/policy/middleware.py +0 -0
- {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/policy/model.py +0 -0
- {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/policy/persistence.py +0 -0
- {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/policy/value_sources.py +0 -0
- {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/py.typed +0 -0
- {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/read_only.py +0 -0
- {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/resources/skills-vendor/.claude/settings.json +0 -0
- {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/resources/skills-vendor/.claude-plugin/marketplace.json +0 -0
- {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/resources/skills-vendor/.claude-plugin/plugin.json +0 -0
- {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/resources/skills-vendor/.github/ISSUE_TEMPLATE/skill-rca.md +0 -0
- {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/resources/skills-vendor/.github/pull_request_template.md +0 -0
- {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/resources/skills-vendor/AGENTS.md +0 -0
- {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/resources/skills-vendor/CLAUDE.md +0 -0
- {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/resources/skills-vendor/CONTRIBUTING.md +0 -0
- {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/resources/skills-vendor/LICENSE +0 -0
- {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/resources/skills-vendor/README.md +0 -0
- {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/SKILL.md +0 -0
- {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/evals/evals.json +0 -0
- {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/appdaemon.md +0 -0
- {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/automation-patterns.md +0 -0
- {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/blueprint-guide.md +0 -0
- {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-cards.md +0 -0
- {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-guide.md +0 -0
- {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/device-control.md +0 -0
- {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/domain-docs.md +0 -0
- {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/examples.yaml +0 -0
- {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/helper-selection.md +0 -0
- {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/safe-refactoring.md +0 -0
- {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/scenes.md +0 -0
- {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/template-guidelines.md +0 -0
- {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/yaml-only-integrations.md +0 -0
- {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/server.py +0 -0
- {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/settings_ui/__init__.py +0 -0
- {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/settings_ui/_handlers_advanced.py +0 -0
- {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/settings_ui/_handlers_backups.py +0 -0
- {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/settings_ui/_handlers_fs.py +0 -0
- {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/settings_ui/_handlers_server.py +0 -0
- {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/settings_ui/_handlers_theme.py +0 -0
- {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/settings_ui/_handlers_tools.py +0 -0
- {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/settings_ui/_i18n.py +0 -0
- {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/settings_ui/_persistence.py +0 -0
- {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/settings_ui/_supervisor.py +0 -0
- {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/settings_ui/_theme.py +0 -0
- {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/settings_ui/_tools_meta.py +0 -0
- {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/settings_ui/locales/README.md +0 -0
- {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/settings_ui/locales/de.json +0 -0
- {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/settings_ui/locales/en.json +0 -0
- {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/settings_ui/locales/ru.json +0 -0
- {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/settings_ui/settings.css +0 -0
- {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/settings_ui/settings.html +0 -0
- {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/settings_ui/settings.js +0 -0
- {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/smoke_test.py +0 -0
- {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/stdio_settings_sidecar.py +0 -0
- {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/strict_bps.py +0 -0
- {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/tools/__init__.py +0 -0
- {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/tools/auto_backup.py +0 -0
- {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/tools/backup.py +0 -0
- {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/tools/best_practice_checker.py +0 -0
- {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/tools/component_api.py +0 -0
- {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/tools/component_config_reads.py +0 -0
- {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/tools/component_devices.py +0 -0
- {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/tools/component_registries.py +0 -0
- {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/tools/component_registry_lookup.py +0 -0
- {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/tools/config_entry_flow.py +0 -0
- {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/tools/device_control.py +0 -0
- {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/tools/enhanced.py +0 -0
- {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/tools/helpers.py +0 -0
- {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/tools/radio/__init__.py +0 -0
- {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/tools/radio/base.py +0 -0
- {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/tools/radio/thread.py +0 -0
- {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/tools/registry.py +0 -0
- {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/tools/smart_search/__init__.py +0 -0
- {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/tools/smart_search/_base.py +0 -0
- {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/tools/smart_search/_config.py +0 -0
- {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/tools/smart_search/_deep.py +0 -0
- {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/tools/smart_search/_entities.py +0 -0
- {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/tools/smart_search/_fetch.py +0 -0
- {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/tools/smart_search/_overview.py +0 -0
- {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/tools/smart_search/_scenes.py +0 -0
- {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/tools/smart_search/_scoring.py +0 -0
- {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/tools/tool_search_hint_middleware.py +0 -0
- {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/tools/tools_addons.py +0 -0
- {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/tools/tools_blueprints.py +0 -0
- {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/tools/tools_bug_report.py +0 -0
- {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/tools/tools_calendar.py +0 -0
- {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/tools/tools_camera.py +0 -0
- {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/tools/tools_categories.py +0 -0
- {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/tools/tools_code.py +0 -0
- {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/tools/tools_config_automations.py +0 -0
- {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/tools/tools_config_dashboards.py +0 -0
- {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/tools/tools_config_helpers.py +0 -0
- {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/tools/tools_config_scenes.py +0 -0
- {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/tools/tools_config_scripts.py +0 -0
- {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/tools/tools_dashboard_screenshot.py +0 -0
- {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/tools/tools_dev.py +0 -0
- {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/tools/tools_energy.py +0 -0
- {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/tools/tools_entities.py +0 -0
- {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/tools/tools_filesystem.py +0 -0
- {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/tools/tools_groups.py +0 -0
- {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/tools/tools_hacs.py +0 -0
- {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/tools/tools_integrations.py +0 -0
- {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/tools/tools_labels.py +0 -0
- {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/tools/tools_radio.py +0 -0
- {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/tools/tools_registry.py +0 -0
- {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/tools/tools_resources.py +0 -0
- {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/tools/tools_service.py +0 -0
- {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/tools/tools_services.py +0 -0
- {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/tools/tools_themes.py +0 -0
- {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/tools/tools_todo.py +0 -0
- {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/tools/tools_traces.py +0 -0
- {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/tools/tools_updates.py +0 -0
- {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/tools/tools_utility.py +0 -0
- {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/tools/tools_voice_assistant.py +0 -0
- {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/tools/tools_yaml_config.py +0 -0
- {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/tools/tools_yaml_read.py +0 -0
- {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/tools/validation_middleware.py +0 -0
- {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/transforms/__init__.py +0 -0
- {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/transforms/lite_docstrings.py +0 -0
- {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/transport_security.py +0 -0
- {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/update_check.py +0 -0
- {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/utils/__init__.py +0 -0
- {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/utils/config_hash.py +0 -0
- {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/utils/data_paths.py +0 -0
- {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/utils/domain_handlers.py +0 -0
- {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/utils/fuzzy_search.py +0 -0
- {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/utils/kill_signal_diagnostics.py +0 -0
- {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/utils/operation_manager.py +0 -0
- {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/utils/python_sandbox.py +0 -0
- {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/utils/skill_loader.py +0 -0
- {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/utils/usage_logger.py +0 -0
- {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/visibility/__init__.py +0 -0
- {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/visibility/model.py +0 -0
- {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/visibility/persistence.py +0 -0
- {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp_dev.egg-info/SOURCES.txt +0 -0
- {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp_dev.egg-info/dependency_links.txt +0 -0
- {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp_dev.egg-info/entry_points.txt +0 -0
- {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp_dev.egg-info/requires.txt +0 -0
- {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp_dev.egg-info/top_level.txt +0 -0
- {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/tests/__init__.py +0 -0
- {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/tests/test_constants.py +0 -0
- {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/tests/test_env_manager.py +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "ha-mcp-dev"
|
|
7
|
-
version = "7.13.0.
|
|
7
|
+
version = "7.13.0.dev2007"
|
|
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
|
|
@@ -178,21 +178,12 @@ ignore = [
|
|
|
178
178
|
"src/ha_mcp/backup_manager.py" = ["C901"]
|
|
179
179
|
"src/ha_mcp/tools/auto_backup.py" = ["C901"]
|
|
180
180
|
"src/ha_mcp/tools/backup.py" = ["C901"]
|
|
181
|
-
"src/ha_mcp/tools/radio/matter.py" = ["C901"]
|
|
182
|
-
"src/ha_mcp/tools/radio/zigbee.py" = ["C901"]
|
|
183
|
-
"src/ha_mcp/tools/radio/zwave.py" = ["C901"]
|
|
184
|
-
"src/ha_mcp/tools/reference_validator.py" = ["C901"]
|
|
185
|
-
"src/ha_mcp/tools/tools_areas.py" = ["C901"]
|
|
186
181
|
"src/ha_mcp/tools/tools_config_automations.py" = ["C901"]
|
|
187
182
|
"src/ha_mcp/tools/tools_config_scenes.py" = ["C901"]
|
|
188
183
|
"src/ha_mcp/tools/tools_config_scripts.py" = ["C901"]
|
|
189
184
|
"src/ha_mcp/tools/tools_dev.py" = ["C901"]
|
|
190
|
-
"src/ha_mcp/tools/tools_history.py" = ["C901"]
|
|
191
185
|
"src/ha_mcp/tools/tools_integrations.py" = ["C901"]
|
|
192
186
|
"src/ha_mcp/tools/tools_system.py" = ["C901"]
|
|
193
|
-
"src/ha_mcp/tools/tools_zones.py" = ["C901"]
|
|
194
|
-
"src/ha_mcp/transforms/categorized_search.py" = ["C901"]
|
|
195
|
-
"src/ha_mcp/visibility/resolver.py" = ["C901"]
|
|
196
187
|
"tests/src/e2e/conftest.py" = ["C901"]
|
|
197
188
|
"tests/src/e2e/error_handling/test_network_errors.py" = ["C901"]
|
|
198
189
|
"tests/src/e2e/policy/test_readonly_mode.py" = ["C901"]
|
|
@@ -145,6 +145,13 @@ async def handle(client: Any, action: str, args: dict[str, Any]) -> dict[str, An
|
|
|
145
145
|
# Returns setup_pin_code, setup_manual_code, setup_qr_code.
|
|
146
146
|
return ok("matter", "share_out", commissioning=commissioning)
|
|
147
147
|
|
|
148
|
+
return await _handle_matter_more(client, action, args, device_id)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
async def _handle_matter_more(
|
|
152
|
+
client: Any, action: str, args: dict[str, Any], device_id: Any
|
|
153
|
+
) -> dict[str, Any]:
|
|
154
|
+
"""Continuation of :func:`handle` for the remaining Matter actions."""
|
|
148
155
|
if action == "interview":
|
|
149
156
|
result = await ws_call(
|
|
150
157
|
client,
|
|
@@ -272,6 +272,13 @@ async def handle(client: Any, action: str, args: dict[str, Any]) -> dict[str, An
|
|
|
272
272
|
long_running=True,
|
|
273
273
|
)
|
|
274
274
|
|
|
275
|
+
return await _handle_zigbee_groups(client, action, args, device_id)
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
async def _handle_zigbee_groups(
|
|
279
|
+
client: Any, action: str, args: dict[str, Any], device_id: Any
|
|
280
|
+
) -> dict[str, Any]:
|
|
281
|
+
"""Continuation of :func:`handle` for Zigbee group and binding actions."""
|
|
275
282
|
# --- groups ------------------------------------------------------------ #
|
|
276
283
|
if action == "group_add":
|
|
277
284
|
group = await ws_call(
|
|
@@ -340,6 +347,14 @@ async def handle(client: Any, action: str, args: dict[str, Any]) -> dict[str, An
|
|
|
340
347
|
)
|
|
341
348
|
return ok("zigbee", "unbind", result=result)
|
|
342
349
|
|
|
350
|
+
return await _handle_zigbee_clusters(client, action, args, device_id)
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
async def _handle_zigbee_clusters(
|
|
354
|
+
client: Any, action: str, args: dict[str, Any], device_id: Any
|
|
355
|
+
) -> dict[str, Any]:
|
|
356
|
+
"""Continuation of :func:`handle` for Zigbee cluster-write and network actions."""
|
|
357
|
+
cluster_type = args.get("cluster_type", "in")
|
|
343
358
|
# --- cluster writes (service-only) ------------------------------------- #
|
|
344
359
|
if action == "cluster_write":
|
|
345
360
|
ieee = await _resolve_ieee(client, device_id)
|
|
@@ -174,6 +174,13 @@ async def handle(client: Any, action: str, args: dict[str, Any]) -> dict[str, An
|
|
|
174
174
|
result = await client.call_service("zwave_js", "ping", {"device_id": device_id})
|
|
175
175
|
return ok("zwave", "ping", result=result)
|
|
176
176
|
|
|
177
|
+
return await _handle_zwave_inclusion(client, action, args, device_id)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
async def _handle_zwave_inclusion(
|
|
181
|
+
client: Any, action: str, args: dict[str, Any], device_id: Any
|
|
182
|
+
) -> dict[str, Any]:
|
|
183
|
+
"""Continuation of :func:`handle` for Z-Wave inclusion/exclusion actions."""
|
|
177
184
|
if action == "add":
|
|
178
185
|
entry_id = await _zwave_entry_id(client)
|
|
179
186
|
if not entry_id:
|
|
@@ -289,6 +296,13 @@ async def handle(client: Any, action: str, args: dict[str, Any]) -> dict[str, An
|
|
|
289
296
|
long_running=True,
|
|
290
297
|
)
|
|
291
298
|
|
|
299
|
+
return await _handle_zwave_maintenance(client, action, args, device_id)
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
async def _handle_zwave_maintenance(
|
|
303
|
+
client: Any, action: str, args: dict[str, Any], device_id: Any
|
|
304
|
+
) -> dict[str, Any]:
|
|
305
|
+
"""Continuation of :func:`handle` for Z-Wave node maintenance actions."""
|
|
292
306
|
if action == "reinterview":
|
|
293
307
|
result = await ws_call(
|
|
294
308
|
client,
|
{ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/tools/reference_validator.py
RENAMED
|
@@ -69,6 +69,55 @@ class ValidationWarning(TypedDict):
|
|
|
69
69
|
reason: str
|
|
70
70
|
|
|
71
71
|
|
|
72
|
+
def _extract_service_ref(
|
|
73
|
+
sub_path: str,
|
|
74
|
+
value: str,
|
|
75
|
+
refs: list[ExtractedRef],
|
|
76
|
+
unvalidated_templates: list[int],
|
|
77
|
+
) -> None:
|
|
78
|
+
"""Record a service ref at *sub_path*, counting templates instead."""
|
|
79
|
+
if _is_template(value):
|
|
80
|
+
unvalidated_templates[0] += 1
|
|
81
|
+
else:
|
|
82
|
+
refs.append({"path": sub_path, "value": value, "kind": "service"})
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _extract_entity_refs(
|
|
86
|
+
sub_path: str,
|
|
87
|
+
value: Any,
|
|
88
|
+
refs: list[ExtractedRef],
|
|
89
|
+
unvalidated_templates: list[int],
|
|
90
|
+
) -> bool:
|
|
91
|
+
"""Record entity ref(s) from a str or list *value*; return True if handled.
|
|
92
|
+
|
|
93
|
+
Returns False when *value* is neither a str nor a list so the caller can
|
|
94
|
+
keep recursing into it (an ``entity_id`` mapped to a nested structure).
|
|
95
|
+
"""
|
|
96
|
+
if isinstance(value, str):
|
|
97
|
+
if _is_template(value):
|
|
98
|
+
unvalidated_templates[0] += 1
|
|
99
|
+
else:
|
|
100
|
+
refs.append({"path": sub_path, "value": value, "kind": "entity"})
|
|
101
|
+
return True
|
|
102
|
+
if isinstance(value, list):
|
|
103
|
+
for i, item in enumerate(value):
|
|
104
|
+
if not isinstance(item, str):
|
|
105
|
+
continue
|
|
106
|
+
item_path = f"{sub_path}[{i}]"
|
|
107
|
+
if _is_template(item):
|
|
108
|
+
unvalidated_templates[0] += 1
|
|
109
|
+
else:
|
|
110
|
+
refs.append(
|
|
111
|
+
{
|
|
112
|
+
"path": item_path,
|
|
113
|
+
"value": item,
|
|
114
|
+
"kind": "entity",
|
|
115
|
+
}
|
|
116
|
+
)
|
|
117
|
+
return True
|
|
118
|
+
return False
|
|
119
|
+
|
|
120
|
+
|
|
72
121
|
def extract_refs(config: Any) -> WalkerResult:
|
|
73
122
|
"""Pull every literal service/entity reference out of *config*.
|
|
74
123
|
|
|
@@ -97,43 +146,17 @@ def extract_refs(config: Any) -> WalkerResult:
|
|
|
97
146
|
sub_path = f"{path}.{key}" if path else key
|
|
98
147
|
|
|
99
148
|
if key in _SERVICE_KEYS and isinstance(value, str):
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
149
|
+
_extract_service_ref(sub_path, value, refs, unvalidated_templates)
|
|
150
|
+
continue
|
|
151
|
+
|
|
152
|
+
if key in _ENTITY_KEYS and _extract_entity_refs(
|
|
153
|
+
sub_path, value, refs, unvalidated_templates
|
|
154
|
+
):
|
|
106
155
|
continue
|
|
107
156
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
unvalidated_templates[0] += 1
|
|
112
|
-
else:
|
|
113
|
-
refs.append(
|
|
114
|
-
{"path": sub_path, "value": value, "kind": "entity"}
|
|
115
|
-
)
|
|
116
|
-
continue
|
|
117
|
-
if isinstance(value, list):
|
|
118
|
-
for i, item in enumerate(value):
|
|
119
|
-
if not isinstance(item, str):
|
|
120
|
-
continue
|
|
121
|
-
item_path = f"{sub_path}[{i}]"
|
|
122
|
-
if _is_template(item):
|
|
123
|
-
unvalidated_templates[0] += 1
|
|
124
|
-
else:
|
|
125
|
-
refs.append(
|
|
126
|
-
{
|
|
127
|
-
"path": item_path,
|
|
128
|
-
"value": item,
|
|
129
|
-
"kind": "entity",
|
|
130
|
-
}
|
|
131
|
-
)
|
|
132
|
-
continue
|
|
133
|
-
|
|
134
|
-
# Neither a service nor an entity key: recurse so deeply
|
|
135
|
-
# nested action blocks (choose/if/parallel/repeat) still
|
|
136
|
-
# get walked.
|
|
157
|
+
# Neither a service nor an entity key (or an entity key whose
|
|
158
|
+
# value is neither str nor list): recurse so deeply nested
|
|
159
|
+
# action blocks (choose/if/parallel/repeat) still get walked.
|
|
137
160
|
_walk(value, sub_path)
|
|
138
161
|
|
|
139
162
|
elif isinstance(node, list):
|
|
@@ -34,6 +34,116 @@ from .util_helpers import (
|
|
|
34
34
|
logger = logging.getLogger(__name__)
|
|
35
35
|
|
|
36
36
|
|
|
37
|
+
def _parse_projection_params(
|
|
38
|
+
fields: str | list[str] | None,
|
|
39
|
+
area_fields: str | list[str] | None,
|
|
40
|
+
) -> tuple[list[str] | None, list[str] | None]:
|
|
41
|
+
"""Validate the fields/area_fields projection params, raising on a bad shape."""
|
|
42
|
+
parsed_fields: list[str] | None = None
|
|
43
|
+
if fields is not None:
|
|
44
|
+
try:
|
|
45
|
+
parsed_fields = parse_string_list_param(fields, "fields", allow_csv=True)
|
|
46
|
+
if parsed_fields is not None and len(parsed_fields) == 0:
|
|
47
|
+
raise ValueError("fields must contain at least one key")
|
|
48
|
+
except ValueError as exc:
|
|
49
|
+
raise_tool_error(create_validation_error(str(exc), parameter="fields"))
|
|
50
|
+
parsed_area_fields: list[str] | None = None
|
|
51
|
+
if area_fields is not None:
|
|
52
|
+
try:
|
|
53
|
+
parsed_area_fields = parse_string_list_param(
|
|
54
|
+
area_fields, "area_fields", allow_csv=True
|
|
55
|
+
)
|
|
56
|
+
if parsed_area_fields is not None and len(parsed_area_fields) == 0:
|
|
57
|
+
raise ValueError("area_fields must contain at least one key")
|
|
58
|
+
except ValueError as exc:
|
|
59
|
+
raise_tool_error(create_validation_error(str(exc), parameter="area_fields"))
|
|
60
|
+
return parsed_fields, parsed_area_fields
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _partition_areas_by_floor(
|
|
64
|
+
areas: list[dict[str, Any]], valid_floor_ids: set[Any]
|
|
65
|
+
) -> tuple[
|
|
66
|
+
dict[str, list[dict[str, Any]]],
|
|
67
|
+
list[dict[str, Any]],
|
|
68
|
+
list[dict[str, Any]],
|
|
69
|
+
]:
|
|
70
|
+
"""Partition areas into (floor_map, unassigned_areas, orphaned_areas).
|
|
71
|
+
|
|
72
|
+
Partitions areas into three disjoint sets:
|
|
73
|
+
- nested: floor_id present AND points to a known floor
|
|
74
|
+
- orphaned: floor_id present BUT points to a non-existent floor
|
|
75
|
+
(race between the concurrent reads, or manual
|
|
76
|
+
.storage inconsistency)
|
|
77
|
+
- unassigned: no floor_id at all
|
|
78
|
+
Orphaned is surfaced as a separate key so the LLM can diagnose
|
|
79
|
+
registry drift without introspecting individual area fields.
|
|
80
|
+
Use `is None` rather than falsy-check so that a floor_id of ""
|
|
81
|
+
(valid but unusual) is treated as orphaned if it does not resolve,
|
|
82
|
+
not as unassigned.
|
|
83
|
+
"""
|
|
84
|
+
floor_map: dict[str, list[dict[str, Any]]] = {}
|
|
85
|
+
unassigned_areas: list[dict[str, Any]] = []
|
|
86
|
+
orphaned_areas: list[dict[str, Any]] = []
|
|
87
|
+
for area in areas:
|
|
88
|
+
fid = area.get("floor_id")
|
|
89
|
+
if fid is None:
|
|
90
|
+
unassigned_areas.append(area)
|
|
91
|
+
elif fid in valid_floor_ids:
|
|
92
|
+
floor_map.setdefault(fid, []).append(area)
|
|
93
|
+
else:
|
|
94
|
+
orphaned_areas.append(area)
|
|
95
|
+
return floor_map, unassigned_areas, orphaned_areas
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
# Sort by level ascending; coerce defensively so a malformed
|
|
99
|
+
# string `level` cannot raise TypeError mid-sort and get
|
|
100
|
+
# flattened by the broad `except Exception` in the caller.
|
|
101
|
+
def _floor_sort_key(floor: dict[str, Any]) -> int:
|
|
102
|
+
raw = floor.get("level")
|
|
103
|
+
if raw is None:
|
|
104
|
+
return 0
|
|
105
|
+
try:
|
|
106
|
+
return int(raw)
|
|
107
|
+
except (TypeError, ValueError):
|
|
108
|
+
logger.warning(
|
|
109
|
+
f"Floor {floor.get('floor_id')!r} has non-numeric "
|
|
110
|
+
f"level {raw!r}; treating as 0 for sort"
|
|
111
|
+
)
|
|
112
|
+
return 0
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _validate_cross_kind_params(
|
|
116
|
+
kind: str,
|
|
117
|
+
level: int | None,
|
|
118
|
+
floor_id: str | None,
|
|
119
|
+
picture: str | None,
|
|
120
|
+
) -> None:
|
|
121
|
+
"""Reject params that don't belong to *kind* before building a set message."""
|
|
122
|
+
# Reject cross-kind params loudly so silent intent loss can't happen
|
|
123
|
+
# (e.g., kind='floor' with picture='...' previously dropped the picture
|
|
124
|
+
# without a diagnostic).
|
|
125
|
+
cross_kind_params: list[str] = []
|
|
126
|
+
if kind == "area" and level is not None:
|
|
127
|
+
cross_kind_params.append("level")
|
|
128
|
+
elif kind == "floor":
|
|
129
|
+
if floor_id is not None:
|
|
130
|
+
cross_kind_params.append("floor_id")
|
|
131
|
+
if picture is not None:
|
|
132
|
+
cross_kind_params.append("picture")
|
|
133
|
+
if cross_kind_params:
|
|
134
|
+
raise_tool_error(
|
|
135
|
+
create_error_response(
|
|
136
|
+
ErrorCode.VALIDATION_INVALID_PARAMETER,
|
|
137
|
+
f"Parameter(s) {cross_kind_params} are not valid for kind={kind!r}",
|
|
138
|
+
context={"kind": kind, "invalid_parameters": cross_kind_params},
|
|
139
|
+
suggestions=[
|
|
140
|
+
"For kind='area' use: name, id, floor_id, icon, aliases, picture",
|
|
141
|
+
"For kind='floor' use: name, id, level, icon, aliases",
|
|
142
|
+
],
|
|
143
|
+
)
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
|
|
37
147
|
class AreaTools:
|
|
38
148
|
"""Area and floor management tools for Home Assistant."""
|
|
39
149
|
|
|
@@ -188,28 +298,9 @@ class AreaTools:
|
|
|
188
298
|
"""
|
|
189
299
|
# Validate projection params before any WS round-trips so a bad shape
|
|
190
300
|
# fails fast without burning two registry reads.
|
|
191
|
-
parsed_fields
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
parsed_fields = parse_string_list_param(
|
|
195
|
-
fields, "fields", allow_csv=True
|
|
196
|
-
)
|
|
197
|
-
if parsed_fields is not None and len(parsed_fields) == 0:
|
|
198
|
-
raise ValueError("fields must contain at least one key")
|
|
199
|
-
except ValueError as exc:
|
|
200
|
-
raise_tool_error(create_validation_error(str(exc), parameter="fields"))
|
|
201
|
-
parsed_area_fields: list[str] | None = None
|
|
202
|
-
if area_fields is not None:
|
|
203
|
-
try:
|
|
204
|
-
parsed_area_fields = parse_string_list_param(
|
|
205
|
-
area_fields, "area_fields", allow_csv=True
|
|
206
|
-
)
|
|
207
|
-
if parsed_area_fields is not None and len(parsed_area_fields) == 0:
|
|
208
|
-
raise ValueError("area_fields must contain at least one key")
|
|
209
|
-
except ValueError as exc:
|
|
210
|
-
raise_tool_error(
|
|
211
|
-
create_validation_error(str(exc), parameter="area_fields")
|
|
212
|
-
)
|
|
301
|
+
parsed_fields, parsed_area_fields = _parse_projection_params(
|
|
302
|
+
fields, area_fields
|
|
303
|
+
)
|
|
213
304
|
|
|
214
305
|
progress: dict[str, Any] = {
|
|
215
306
|
"operation": "list_floors_areas",
|
|
@@ -218,31 +309,12 @@ class AreaTools:
|
|
|
218
309
|
try:
|
|
219
310
|
areas, floors = await self._fetch_area_floor_registries(progress)
|
|
220
311
|
|
|
221
|
-
# Partition areas into three disjoint sets:
|
|
222
|
-
# - nested: floor_id present AND points to a known floor
|
|
223
|
-
# - orphaned: floor_id present BUT points to a non-existent floor
|
|
224
|
-
# (race between the concurrent reads, or manual
|
|
225
|
-
# .storage inconsistency)
|
|
226
|
-
# - unassigned: no floor_id at all
|
|
227
|
-
# Orphaned is surfaced as a separate key so the LLM can diagnose
|
|
228
|
-
# registry drift without introspecting individual area fields.
|
|
229
|
-
# Use `is None` rather than falsy-check so that a floor_id of ""
|
|
230
|
-
# (valid but unusual) is treated as orphaned if it does not resolve,
|
|
231
|
-
# not as unassigned.
|
|
232
312
|
valid_floor_ids = {
|
|
233
313
|
f.get("floor_id") for f in floors if f.get("floor_id") is not None
|
|
234
314
|
}
|
|
235
|
-
floor_map
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
for area in areas:
|
|
239
|
-
fid = area.get("floor_id")
|
|
240
|
-
if fid is None:
|
|
241
|
-
unassigned_areas.append(area)
|
|
242
|
-
elif fid in valid_floor_ids:
|
|
243
|
-
floor_map.setdefault(fid, []).append(area)
|
|
244
|
-
else:
|
|
245
|
-
orphaned_areas.append(area)
|
|
315
|
+
floor_map, unassigned_areas, orphaned_areas = _partition_areas_by_floor(
|
|
316
|
+
areas, valid_floor_ids
|
|
317
|
+
)
|
|
246
318
|
progress["phase"] = "partitioned"
|
|
247
319
|
|
|
248
320
|
# Build nested hierarchy, preserving all floor-registry fields for
|
|
@@ -257,22 +329,6 @@ class AreaTools:
|
|
|
257
329
|
for floor in floors
|
|
258
330
|
]
|
|
259
331
|
|
|
260
|
-
# Sort by level ascending; coerce defensively so a malformed
|
|
261
|
-
# string `level` cannot raise TypeError mid-sort and get
|
|
262
|
-
# flattened by the broad `except Exception` below.
|
|
263
|
-
def _floor_sort_key(floor: dict[str, Any]) -> int:
|
|
264
|
-
raw = floor.get("level")
|
|
265
|
-
if raw is None:
|
|
266
|
-
return 0
|
|
267
|
-
try:
|
|
268
|
-
return int(raw)
|
|
269
|
-
except (TypeError, ValueError):
|
|
270
|
-
logger.warning(
|
|
271
|
-
f"Floor {floor.get('floor_id')!r} has non-numeric "
|
|
272
|
-
f"level {raw!r}; treating as 0 for sort"
|
|
273
|
-
)
|
|
274
|
-
return 0
|
|
275
|
-
|
|
276
332
|
topology.sort(key=_floor_sort_key)
|
|
277
333
|
progress["phase"] = "sorted"
|
|
278
334
|
|
|
@@ -408,6 +464,82 @@ class AreaTools:
|
|
|
408
464
|
|
|
409
465
|
return areas_result["result"], floors_result["result"]
|
|
410
466
|
|
|
467
|
+
def _build_area_or_floor_message(
|
|
468
|
+
self,
|
|
469
|
+
kind: str,
|
|
470
|
+
identifier: str | None,
|
|
471
|
+
name: str | None,
|
|
472
|
+
floor_id: str | None,
|
|
473
|
+
level: int | None,
|
|
474
|
+
icon: str | None,
|
|
475
|
+
parsed_aliases: list[str] | None,
|
|
476
|
+
picture: str | None,
|
|
477
|
+
) -> tuple[dict[str, Any], str, str, str, str | None]:
|
|
478
|
+
"""Build the WS message plus (result_key, id_key, operation, name) for a set.
|
|
479
|
+
|
|
480
|
+
``name`` is returned because the create branches narrow it from
|
|
481
|
+
``str | None`` to ``str`` via ``validate_identifier_not_empty``.
|
|
482
|
+
"""
|
|
483
|
+
if kind == "area":
|
|
484
|
+
if identifier:
|
|
485
|
+
message = self._build_area_update_message(
|
|
486
|
+
identifier,
|
|
487
|
+
name,
|
|
488
|
+
floor_id,
|
|
489
|
+
icon,
|
|
490
|
+
parsed_aliases,
|
|
491
|
+
picture,
|
|
492
|
+
)
|
|
493
|
+
operation = "update"
|
|
494
|
+
else:
|
|
495
|
+
# Reassignment narrows ``name`` from ``str | None`` to
|
|
496
|
+
# ``str`` for the build-message call below.
|
|
497
|
+
name = validate_identifier_not_empty(
|
|
498
|
+
name,
|
|
499
|
+
"name",
|
|
500
|
+
message="name is required when creating a new area",
|
|
501
|
+
context={"operation": "create_area"},
|
|
502
|
+
suggestions=["Provide a non-empty name for the new area"],
|
|
503
|
+
)
|
|
504
|
+
message = self._build_area_create_message(
|
|
505
|
+
name,
|
|
506
|
+
floor_id,
|
|
507
|
+
icon,
|
|
508
|
+
parsed_aliases,
|
|
509
|
+
picture,
|
|
510
|
+
)
|
|
511
|
+
operation = "create"
|
|
512
|
+
result_key = "area"
|
|
513
|
+
id_key = "area_id"
|
|
514
|
+
else: # kind == "floor"
|
|
515
|
+
if identifier:
|
|
516
|
+
message = self._build_floor_update_message(
|
|
517
|
+
identifier,
|
|
518
|
+
name,
|
|
519
|
+
level,
|
|
520
|
+
icon,
|
|
521
|
+
parsed_aliases,
|
|
522
|
+
)
|
|
523
|
+
operation = "update"
|
|
524
|
+
else:
|
|
525
|
+
name = validate_identifier_not_empty(
|
|
526
|
+
name,
|
|
527
|
+
"name",
|
|
528
|
+
message="name is required when creating a new floor",
|
|
529
|
+
context={"operation": "create_floor"},
|
|
530
|
+
suggestions=["Provide a non-empty name for the new floor"],
|
|
531
|
+
)
|
|
532
|
+
message = self._build_floor_create_message(
|
|
533
|
+
name,
|
|
534
|
+
level,
|
|
535
|
+
icon,
|
|
536
|
+
parsed_aliases,
|
|
537
|
+
)
|
|
538
|
+
operation = "create"
|
|
539
|
+
result_key = "floor"
|
|
540
|
+
id_key = "floor_id"
|
|
541
|
+
return message, result_key, id_key, operation, name
|
|
542
|
+
|
|
411
543
|
# ============================================================
|
|
412
544
|
# COMBINED SET / REMOVE
|
|
413
545
|
# ============================================================
|
|
@@ -514,29 +646,7 @@ class AreaTools:
|
|
|
514
646
|
)
|
|
515
647
|
)
|
|
516
648
|
|
|
517
|
-
|
|
518
|
-
# (e.g., kind='floor' with picture='...' previously dropped the picture
|
|
519
|
-
# without a diagnostic).
|
|
520
|
-
cross_kind_params: list[str] = []
|
|
521
|
-
if kind == "area" and level is not None:
|
|
522
|
-
cross_kind_params.append("level")
|
|
523
|
-
elif kind == "floor":
|
|
524
|
-
if floor_id is not None:
|
|
525
|
-
cross_kind_params.append("floor_id")
|
|
526
|
-
if picture is not None:
|
|
527
|
-
cross_kind_params.append("picture")
|
|
528
|
-
if cross_kind_params:
|
|
529
|
-
raise_tool_error(
|
|
530
|
-
create_error_response(
|
|
531
|
-
ErrorCode.VALIDATION_INVALID_PARAMETER,
|
|
532
|
-
f"Parameter(s) {cross_kind_params} are not valid for kind={kind!r}",
|
|
533
|
-
context={"kind": kind, "invalid_parameters": cross_kind_params},
|
|
534
|
-
suggestions=[
|
|
535
|
-
"For kind='area' use: name, id, floor_id, icon, aliases, picture",
|
|
536
|
-
"For kind='floor' use: name, id, level, icon, aliases",
|
|
537
|
-
],
|
|
538
|
-
)
|
|
539
|
-
)
|
|
649
|
+
_validate_cross_kind_params(kind, level, floor_id, picture)
|
|
540
650
|
|
|
541
651
|
# ``None`` stays the documented "create-new" sentinel; explicit
|
|
542
652
|
# empty/whitespace would silently route to the ``if id:`` create
|
|
@@ -552,64 +662,18 @@ class AreaTools:
|
|
|
552
662
|
context={"kind": kind},
|
|
553
663
|
)
|
|
554
664
|
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
# Reassignment narrows ``name`` from ``str | None`` to
|
|
568
|
-
# ``str`` for the build-message call below.
|
|
569
|
-
name = validate_identifier_not_empty(
|
|
570
|
-
name,
|
|
571
|
-
"name",
|
|
572
|
-
message="name is required when creating a new area",
|
|
573
|
-
context={"operation": "create_area"},
|
|
574
|
-
suggestions=["Provide a non-empty name for the new area"],
|
|
575
|
-
)
|
|
576
|
-
message = self._build_area_create_message(
|
|
577
|
-
name,
|
|
578
|
-
floor_id,
|
|
579
|
-
icon,
|
|
580
|
-
parsed_aliases,
|
|
581
|
-
picture,
|
|
582
|
-
)
|
|
583
|
-
operation = "create"
|
|
584
|
-
result_key = "area"
|
|
585
|
-
id_key = "area_id"
|
|
586
|
-
else: # kind == "floor"
|
|
587
|
-
if id:
|
|
588
|
-
message = self._build_floor_update_message(
|
|
589
|
-
id,
|
|
590
|
-
name,
|
|
591
|
-
level,
|
|
592
|
-
icon,
|
|
593
|
-
parsed_aliases,
|
|
594
|
-
)
|
|
595
|
-
operation = "update"
|
|
596
|
-
else:
|
|
597
|
-
name = validate_identifier_not_empty(
|
|
598
|
-
name,
|
|
599
|
-
"name",
|
|
600
|
-
message="name is required when creating a new floor",
|
|
601
|
-
context={"operation": "create_floor"},
|
|
602
|
-
suggestions=["Provide a non-empty name for the new floor"],
|
|
603
|
-
)
|
|
604
|
-
message = self._build_floor_create_message(
|
|
605
|
-
name,
|
|
606
|
-
level,
|
|
607
|
-
icon,
|
|
608
|
-
parsed_aliases,
|
|
609
|
-
)
|
|
610
|
-
operation = "create"
|
|
611
|
-
result_key = "floor"
|
|
612
|
-
id_key = "floor_id"
|
|
665
|
+
message, result_key, id_key, operation, name = (
|
|
666
|
+
self._build_area_or_floor_message(
|
|
667
|
+
kind,
|
|
668
|
+
id,
|
|
669
|
+
name,
|
|
670
|
+
floor_id,
|
|
671
|
+
level,
|
|
672
|
+
icon,
|
|
673
|
+
parsed_aliases,
|
|
674
|
+
picture,
|
|
675
|
+
)
|
|
676
|
+
)
|
|
613
677
|
|
|
614
678
|
result = await self._client.send_websocket_message(message)
|
|
615
679
|
|