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.
Files changed (187) hide show
  1. {ha_mcp_dev-7.13.0.dev2003/src/ha_mcp_dev.egg-info → ha_mcp_dev-7.13.0.dev2007}/PKG-INFO +1 -1
  2. {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/pyproject.toml +1 -10
  3. {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/tools/radio/matter.py +7 -0
  4. {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/tools/radio/zigbee.py +15 -0
  5. {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/tools/radio/zwave.py +14 -0
  6. {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/tools/reference_validator.py +58 -35
  7. {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/tools/tools_areas.py +205 -141
  8. {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/tools/tools_history.py +83 -57
  9. {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/tools/tools_search.py +5 -1
  10. {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/tools/tools_system.py +5 -1
  11. {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/tools/tools_zones.py +67 -52
  12. {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/tools/util_helpers.py +7 -2
  13. {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/transforms/categorized_search.py +102 -79
  14. {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/visibility/resolver.py +151 -71
  15. {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007/src/ha_mcp_dev.egg-info}/PKG-INFO +1 -1
  16. {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/LICENSE +0 -0
  17. {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/MANIFEST.in +0 -0
  18. {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/README.md +0 -0
  19. {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/setup.cfg +0 -0
  20. {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/__init__.py +0 -0
  21. {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/__main__.py +0 -0
  22. {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/_pypi_marker +0 -0
  23. {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/_version.py +0 -0
  24. {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/auth/__init__.py +0 -0
  25. {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/auth/consent_form.py +0 -0
  26. {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/auth/provider.py +0 -0
  27. {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/backup_manager.py +0 -0
  28. {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/browser_landing.py +0 -0
  29. {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/client/__init__.py +0 -0
  30. {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/client/rest_client.py +0 -0
  31. {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/client/supervisor_client.py +0 -0
  32. {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/client/websocket_client.py +0 -0
  33. {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/client/websocket_listener.py +0 -0
  34. {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/config.py +0 -0
  35. {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/dashboard_screenshot/__init__.py +0 -0
  36. {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/dashboard_screenshot/capture.py +0 -0
  37. {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/dashboard_screenshot/content.py +0 -0
  38. {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/dashboard_screenshot/paths.py +0 -0
  39. {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/dashboard_screenshot/provision.py +0 -0
  40. {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/dashboard_screenshot/theme_guard.py +0 -0
  41. {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/errors.py +0 -0
  42. {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/llm_exposure.py +0 -0
  43. {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/policy/__init__.py +0 -0
  44. {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/policy/approval_queue.py +0 -0
  45. {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/policy/evaluator.py +0 -0
  46. {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/policy/handlers.py +0 -0
  47. {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/policy/middleware.py +0 -0
  48. {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/policy/model.py +0 -0
  49. {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/policy/persistence.py +0 -0
  50. {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/policy/value_sources.py +0 -0
  51. {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/py.typed +0 -0
  52. {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/read_only.py +0 -0
  53. {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
  54. {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
  55. {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
  56. {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
  57. {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
  58. {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/resources/skills-vendor/AGENTS.md +0 -0
  59. {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/resources/skills-vendor/CLAUDE.md +0 -0
  60. {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/resources/skills-vendor/CONTRIBUTING.md +0 -0
  61. {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/resources/skills-vendor/LICENSE +0 -0
  62. {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/resources/skills-vendor/README.md +0 -0
  63. {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
  64. {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
  65. {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
  66. {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
  67. {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
  68. {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
  69. {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
  70. {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
  71. {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
  72. {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
  73. {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
  74. {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
  75. {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
  76. {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
  77. {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
  78. {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/server.py +0 -0
  79. {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/settings_ui/__init__.py +0 -0
  80. {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/settings_ui/_handlers_advanced.py +0 -0
  81. {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/settings_ui/_handlers_backups.py +0 -0
  82. {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/settings_ui/_handlers_fs.py +0 -0
  83. {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/settings_ui/_handlers_server.py +0 -0
  84. {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/settings_ui/_handlers_theme.py +0 -0
  85. {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/settings_ui/_handlers_tools.py +0 -0
  86. {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/settings_ui/_i18n.py +0 -0
  87. {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/settings_ui/_persistence.py +0 -0
  88. {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/settings_ui/_supervisor.py +0 -0
  89. {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/settings_ui/_theme.py +0 -0
  90. {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/settings_ui/_tools_meta.py +0 -0
  91. {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/settings_ui/locales/README.md +0 -0
  92. {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/settings_ui/locales/de.json +0 -0
  93. {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/settings_ui/locales/en.json +0 -0
  94. {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/settings_ui/locales/ru.json +0 -0
  95. {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/settings_ui/settings.css +0 -0
  96. {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/settings_ui/settings.html +0 -0
  97. {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/settings_ui/settings.js +0 -0
  98. {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/smoke_test.py +0 -0
  99. {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/stdio_settings_sidecar.py +0 -0
  100. {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/strict_bps.py +0 -0
  101. {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/tools/__init__.py +0 -0
  102. {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/tools/auto_backup.py +0 -0
  103. {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/tools/backup.py +0 -0
  104. {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/tools/best_practice_checker.py +0 -0
  105. {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/tools/component_api.py +0 -0
  106. {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/tools/component_config_reads.py +0 -0
  107. {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/tools/component_devices.py +0 -0
  108. {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/tools/component_registries.py +0 -0
  109. {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/tools/component_registry_lookup.py +0 -0
  110. {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/tools/config_entry_flow.py +0 -0
  111. {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/tools/device_control.py +0 -0
  112. {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/tools/enhanced.py +0 -0
  113. {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/tools/helpers.py +0 -0
  114. {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/tools/radio/__init__.py +0 -0
  115. {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/tools/radio/base.py +0 -0
  116. {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/tools/radio/thread.py +0 -0
  117. {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/tools/registry.py +0 -0
  118. {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/tools/smart_search/__init__.py +0 -0
  119. {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/tools/smart_search/_base.py +0 -0
  120. {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/tools/smart_search/_config.py +0 -0
  121. {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/tools/smart_search/_deep.py +0 -0
  122. {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/tools/smart_search/_entities.py +0 -0
  123. {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/tools/smart_search/_fetch.py +0 -0
  124. {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/tools/smart_search/_overview.py +0 -0
  125. {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/tools/smart_search/_scenes.py +0 -0
  126. {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/tools/smart_search/_scoring.py +0 -0
  127. {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
  128. {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/tools/tools_addons.py +0 -0
  129. {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/tools/tools_blueprints.py +0 -0
  130. {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/tools/tools_bug_report.py +0 -0
  131. {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/tools/tools_calendar.py +0 -0
  132. {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/tools/tools_camera.py +0 -0
  133. {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/tools/tools_categories.py +0 -0
  134. {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/tools/tools_code.py +0 -0
  135. {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/tools/tools_config_automations.py +0 -0
  136. {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/tools/tools_config_dashboards.py +0 -0
  137. {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/tools/tools_config_helpers.py +0 -0
  138. {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/tools/tools_config_scenes.py +0 -0
  139. {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/tools/tools_config_scripts.py +0 -0
  140. {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/tools/tools_dashboard_screenshot.py +0 -0
  141. {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/tools/tools_dev.py +0 -0
  142. {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/tools/tools_energy.py +0 -0
  143. {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/tools/tools_entities.py +0 -0
  144. {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/tools/tools_filesystem.py +0 -0
  145. {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/tools/tools_groups.py +0 -0
  146. {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/tools/tools_hacs.py +0 -0
  147. {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/tools/tools_integrations.py +0 -0
  148. {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/tools/tools_labels.py +0 -0
  149. {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/tools/tools_radio.py +0 -0
  150. {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/tools/tools_registry.py +0 -0
  151. {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/tools/tools_resources.py +0 -0
  152. {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/tools/tools_service.py +0 -0
  153. {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/tools/tools_services.py +0 -0
  154. {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/tools/tools_themes.py +0 -0
  155. {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/tools/tools_todo.py +0 -0
  156. {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/tools/tools_traces.py +0 -0
  157. {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/tools/tools_updates.py +0 -0
  158. {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/tools/tools_utility.py +0 -0
  159. {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/tools/tools_voice_assistant.py +0 -0
  160. {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/tools/tools_yaml_config.py +0 -0
  161. {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/tools/tools_yaml_read.py +0 -0
  162. {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/tools/validation_middleware.py +0 -0
  163. {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/transforms/__init__.py +0 -0
  164. {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/transforms/lite_docstrings.py +0 -0
  165. {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/transport_security.py +0 -0
  166. {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/update_check.py +0 -0
  167. {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/utils/__init__.py +0 -0
  168. {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/utils/config_hash.py +0 -0
  169. {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/utils/data_paths.py +0 -0
  170. {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/utils/domain_handlers.py +0 -0
  171. {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/utils/fuzzy_search.py +0 -0
  172. {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/utils/kill_signal_diagnostics.py +0 -0
  173. {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/utils/operation_manager.py +0 -0
  174. {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/utils/python_sandbox.py +0 -0
  175. {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/utils/skill_loader.py +0 -0
  176. {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/utils/usage_logger.py +0 -0
  177. {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/visibility/__init__.py +0 -0
  178. {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/visibility/model.py +0 -0
  179. {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp/visibility/persistence.py +0 -0
  180. {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp_dev.egg-info/SOURCES.txt +0 -0
  181. {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
  182. {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
  183. {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/src/ha_mcp_dev.egg-info/requires.txt +0 -0
  184. {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
  185. {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/tests/__init__.py +0 -0
  186. {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/tests/test_constants.py +0 -0
  187. {ha_mcp_dev-7.13.0.dev2003 → ha_mcp_dev-7.13.0.dev2007}/tests/test_env_manager.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ha-mcp-dev
3
- Version: 7.13.0.dev2003
3
+ Version: 7.13.0.dev2007
4
4
  Summary: Home Assistant MCP Server - Complete control of Home Assistant through MCP
5
5
  Author-email: Julien <github@qc-h.net>
6
6
  License: MIT
@@ -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.dev2003"
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,
@@ -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
- if _is_template(value):
101
- unvalidated_templates[0] += 1
102
- else:
103
- refs.append(
104
- {"path": sub_path, "value": value, "kind": "service"}
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
- if key in _ENTITY_KEYS:
109
- if isinstance(value, str):
110
- if _is_template(value):
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: list[str] | None = None
192
- if fields is not None:
193
- try:
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: dict[str, list[dict[str, Any]]] = {}
236
- unassigned_areas: list[dict[str, Any]] = []
237
- orphaned_areas: list[dict[str, Any]] = []
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
- # Reject cross-kind params loudly so silent intent loss can't happen
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
- if kind == "area":
556
- if id:
557
- message = self._build_area_update_message(
558
- id,
559
- name,
560
- floor_id,
561
- icon,
562
- parsed_aliases,
563
- picture,
564
- )
565
- operation = "update"
566
- else:
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