ha-mcp-dev 7.5.0.dev516__tar.gz → 7.5.0.dev518__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 (111) hide show
  1. {ha_mcp_dev-7.5.0.dev516/src/ha_mcp_dev.egg-info → ha_mcp_dev-7.5.0.dev518}/PKG-INFO +1 -1
  2. {ha_mcp_dev-7.5.0.dev516 → ha_mcp_dev-7.5.0.dev518}/pyproject.toml +1 -1
  3. {ha_mcp_dev-7.5.0.dev516 → ha_mcp_dev-7.5.0.dev518}/src/ha_mcp/tools/tools_config_helpers.py +287 -94
  4. {ha_mcp_dev-7.5.0.dev516 → ha_mcp_dev-7.5.0.dev518/src/ha_mcp_dev.egg-info}/PKG-INFO +1 -1
  5. {ha_mcp_dev-7.5.0.dev516 → ha_mcp_dev-7.5.0.dev518}/LICENSE +0 -0
  6. {ha_mcp_dev-7.5.0.dev516 → ha_mcp_dev-7.5.0.dev518}/MANIFEST.in +0 -0
  7. {ha_mcp_dev-7.5.0.dev516 → ha_mcp_dev-7.5.0.dev518}/README.md +0 -0
  8. {ha_mcp_dev-7.5.0.dev516 → ha_mcp_dev-7.5.0.dev518}/setup.cfg +0 -0
  9. {ha_mcp_dev-7.5.0.dev516 → ha_mcp_dev-7.5.0.dev518}/src/ha_mcp/__init__.py +0 -0
  10. {ha_mcp_dev-7.5.0.dev516 → ha_mcp_dev-7.5.0.dev518}/src/ha_mcp/__main__.py +0 -0
  11. {ha_mcp_dev-7.5.0.dev516 → ha_mcp_dev-7.5.0.dev518}/src/ha_mcp/_pypi_marker +0 -0
  12. {ha_mcp_dev-7.5.0.dev516 → ha_mcp_dev-7.5.0.dev518}/src/ha_mcp/_version.py +0 -0
  13. {ha_mcp_dev-7.5.0.dev516 → ha_mcp_dev-7.5.0.dev518}/src/ha_mcp/auth/__init__.py +0 -0
  14. {ha_mcp_dev-7.5.0.dev516 → ha_mcp_dev-7.5.0.dev518}/src/ha_mcp/auth/consent_form.py +0 -0
  15. {ha_mcp_dev-7.5.0.dev516 → ha_mcp_dev-7.5.0.dev518}/src/ha_mcp/auth/provider.py +0 -0
  16. {ha_mcp_dev-7.5.0.dev516 → ha_mcp_dev-7.5.0.dev518}/src/ha_mcp/client/__init__.py +0 -0
  17. {ha_mcp_dev-7.5.0.dev516 → ha_mcp_dev-7.5.0.dev518}/src/ha_mcp/client/rest_client.py +0 -0
  18. {ha_mcp_dev-7.5.0.dev516 → ha_mcp_dev-7.5.0.dev518}/src/ha_mcp/client/supervisor_client.py +0 -0
  19. {ha_mcp_dev-7.5.0.dev516 → ha_mcp_dev-7.5.0.dev518}/src/ha_mcp/client/websocket_client.py +0 -0
  20. {ha_mcp_dev-7.5.0.dev516 → ha_mcp_dev-7.5.0.dev518}/src/ha_mcp/client/websocket_listener.py +0 -0
  21. {ha_mcp_dev-7.5.0.dev516 → ha_mcp_dev-7.5.0.dev518}/src/ha_mcp/config.py +0 -0
  22. {ha_mcp_dev-7.5.0.dev516 → ha_mcp_dev-7.5.0.dev518}/src/ha_mcp/errors.py +0 -0
  23. {ha_mcp_dev-7.5.0.dev516 → ha_mcp_dev-7.5.0.dev518}/src/ha_mcp/py.typed +0 -0
  24. {ha_mcp_dev-7.5.0.dev516 → ha_mcp_dev-7.5.0.dev518}/src/ha_mcp/resources/skills-vendor/.claude/settings.json +0 -0
  25. {ha_mcp_dev-7.5.0.dev516 → ha_mcp_dev-7.5.0.dev518}/src/ha_mcp/resources/skills-vendor/.claude-plugin/marketplace.json +0 -0
  26. {ha_mcp_dev-7.5.0.dev516 → ha_mcp_dev-7.5.0.dev518}/src/ha_mcp/resources/skills-vendor/.claude-plugin/plugin.json +0 -0
  27. {ha_mcp_dev-7.5.0.dev516 → ha_mcp_dev-7.5.0.dev518}/src/ha_mcp/resources/skills-vendor/.github/ISSUE_TEMPLATE/skill-rca.md +0 -0
  28. {ha_mcp_dev-7.5.0.dev516 → ha_mcp_dev-7.5.0.dev518}/src/ha_mcp/resources/skills-vendor/AGENTS.md +0 -0
  29. {ha_mcp_dev-7.5.0.dev516 → ha_mcp_dev-7.5.0.dev518}/src/ha_mcp/resources/skills-vendor/CLAUDE.md +0 -0
  30. {ha_mcp_dev-7.5.0.dev516 → ha_mcp_dev-7.5.0.dev518}/src/ha_mcp/resources/skills-vendor/CONTRIBUTING.md +0 -0
  31. {ha_mcp_dev-7.5.0.dev516 → ha_mcp_dev-7.5.0.dev518}/src/ha_mcp/resources/skills-vendor/LICENSE +0 -0
  32. {ha_mcp_dev-7.5.0.dev516 → ha_mcp_dev-7.5.0.dev518}/src/ha_mcp/resources/skills-vendor/README.md +0 -0
  33. {ha_mcp_dev-7.5.0.dev516 → ha_mcp_dev-7.5.0.dev518}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/SKILL.md +0 -0
  34. {ha_mcp_dev-7.5.0.dev516 → ha_mcp_dev-7.5.0.dev518}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/evals/evals.json +0 -0
  35. {ha_mcp_dev-7.5.0.dev516 → ha_mcp_dev-7.5.0.dev518}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/automation-patterns.md +0 -0
  36. {ha_mcp_dev-7.5.0.dev516 → ha_mcp_dev-7.5.0.dev518}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-cards.md +0 -0
  37. {ha_mcp_dev-7.5.0.dev516 → ha_mcp_dev-7.5.0.dev518}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-guide.md +0 -0
  38. {ha_mcp_dev-7.5.0.dev516 → ha_mcp_dev-7.5.0.dev518}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/device-control.md +0 -0
  39. {ha_mcp_dev-7.5.0.dev516 → ha_mcp_dev-7.5.0.dev518}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/domain-docs.md +0 -0
  40. {ha_mcp_dev-7.5.0.dev516 → ha_mcp_dev-7.5.0.dev518}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/examples.yaml +0 -0
  41. {ha_mcp_dev-7.5.0.dev516 → ha_mcp_dev-7.5.0.dev518}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/helper-selection.md +0 -0
  42. {ha_mcp_dev-7.5.0.dev516 → ha_mcp_dev-7.5.0.dev518}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/safe-refactoring.md +0 -0
  43. {ha_mcp_dev-7.5.0.dev516 → ha_mcp_dev-7.5.0.dev518}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/template-guidelines.md +0 -0
  44. {ha_mcp_dev-7.5.0.dev516 → ha_mcp_dev-7.5.0.dev518}/src/ha_mcp/server.py +0 -0
  45. {ha_mcp_dev-7.5.0.dev516 → ha_mcp_dev-7.5.0.dev518}/src/ha_mcp/settings_ui.py +0 -0
  46. {ha_mcp_dev-7.5.0.dev516 → ha_mcp_dev-7.5.0.dev518}/src/ha_mcp/smoke_test.py +0 -0
  47. {ha_mcp_dev-7.5.0.dev516 → ha_mcp_dev-7.5.0.dev518}/src/ha_mcp/tools/__init__.py +0 -0
  48. {ha_mcp_dev-7.5.0.dev516 → ha_mcp_dev-7.5.0.dev518}/src/ha_mcp/tools/backup.py +0 -0
  49. {ha_mcp_dev-7.5.0.dev516 → ha_mcp_dev-7.5.0.dev518}/src/ha_mcp/tools/best_practice_checker.py +0 -0
  50. {ha_mcp_dev-7.5.0.dev516 → ha_mcp_dev-7.5.0.dev518}/src/ha_mcp/tools/device_control.py +0 -0
  51. {ha_mcp_dev-7.5.0.dev516 → ha_mcp_dev-7.5.0.dev518}/src/ha_mcp/tools/enhanced.py +0 -0
  52. {ha_mcp_dev-7.5.0.dev516 → ha_mcp_dev-7.5.0.dev518}/src/ha_mcp/tools/helpers.py +0 -0
  53. {ha_mcp_dev-7.5.0.dev516 → ha_mcp_dev-7.5.0.dev518}/src/ha_mcp/tools/reference_validator.py +0 -0
  54. {ha_mcp_dev-7.5.0.dev516 → ha_mcp_dev-7.5.0.dev518}/src/ha_mcp/tools/registry.py +0 -0
  55. {ha_mcp_dev-7.5.0.dev516 → ha_mcp_dev-7.5.0.dev518}/src/ha_mcp/tools/smart_search.py +0 -0
  56. {ha_mcp_dev-7.5.0.dev516 → ha_mcp_dev-7.5.0.dev518}/src/ha_mcp/tools/tools_addons.py +0 -0
  57. {ha_mcp_dev-7.5.0.dev516 → ha_mcp_dev-7.5.0.dev518}/src/ha_mcp/tools/tools_areas.py +0 -0
  58. {ha_mcp_dev-7.5.0.dev516 → ha_mcp_dev-7.5.0.dev518}/src/ha_mcp/tools/tools_blueprints.py +0 -0
  59. {ha_mcp_dev-7.5.0.dev516 → ha_mcp_dev-7.5.0.dev518}/src/ha_mcp/tools/tools_bug_report.py +0 -0
  60. {ha_mcp_dev-7.5.0.dev516 → ha_mcp_dev-7.5.0.dev518}/src/ha_mcp/tools/tools_calendar.py +0 -0
  61. {ha_mcp_dev-7.5.0.dev516 → ha_mcp_dev-7.5.0.dev518}/src/ha_mcp/tools/tools_camera.py +0 -0
  62. {ha_mcp_dev-7.5.0.dev516 → ha_mcp_dev-7.5.0.dev518}/src/ha_mcp/tools/tools_categories.py +0 -0
  63. {ha_mcp_dev-7.5.0.dev516 → ha_mcp_dev-7.5.0.dev518}/src/ha_mcp/tools/tools_code.py +0 -0
  64. {ha_mcp_dev-7.5.0.dev516 → ha_mcp_dev-7.5.0.dev518}/src/ha_mcp/tools/tools_config_automations.py +0 -0
  65. {ha_mcp_dev-7.5.0.dev516 → ha_mcp_dev-7.5.0.dev518}/src/ha_mcp/tools/tools_config_dashboards.py +0 -0
  66. {ha_mcp_dev-7.5.0.dev516 → ha_mcp_dev-7.5.0.dev518}/src/ha_mcp/tools/tools_config_entry_flow.py +0 -0
  67. {ha_mcp_dev-7.5.0.dev516 → ha_mcp_dev-7.5.0.dev518}/src/ha_mcp/tools/tools_config_scenes.py +0 -0
  68. {ha_mcp_dev-7.5.0.dev516 → ha_mcp_dev-7.5.0.dev518}/src/ha_mcp/tools/tools_config_scripts.py +0 -0
  69. {ha_mcp_dev-7.5.0.dev516 → ha_mcp_dev-7.5.0.dev518}/src/ha_mcp/tools/tools_energy.py +0 -0
  70. {ha_mcp_dev-7.5.0.dev516 → ha_mcp_dev-7.5.0.dev518}/src/ha_mcp/tools/tools_entities.py +0 -0
  71. {ha_mcp_dev-7.5.0.dev516 → ha_mcp_dev-7.5.0.dev518}/src/ha_mcp/tools/tools_filesystem.py +0 -0
  72. {ha_mcp_dev-7.5.0.dev516 → ha_mcp_dev-7.5.0.dev518}/src/ha_mcp/tools/tools_groups.py +0 -0
  73. {ha_mcp_dev-7.5.0.dev516 → ha_mcp_dev-7.5.0.dev518}/src/ha_mcp/tools/tools_hacs.py +0 -0
  74. {ha_mcp_dev-7.5.0.dev516 → ha_mcp_dev-7.5.0.dev518}/src/ha_mcp/tools/tools_history.py +0 -0
  75. {ha_mcp_dev-7.5.0.dev516 → ha_mcp_dev-7.5.0.dev518}/src/ha_mcp/tools/tools_integrations.py +0 -0
  76. {ha_mcp_dev-7.5.0.dev516 → ha_mcp_dev-7.5.0.dev518}/src/ha_mcp/tools/tools_labels.py +0 -0
  77. {ha_mcp_dev-7.5.0.dev516 → ha_mcp_dev-7.5.0.dev518}/src/ha_mcp/tools/tools_mcp_component.py +0 -0
  78. {ha_mcp_dev-7.5.0.dev516 → ha_mcp_dev-7.5.0.dev518}/src/ha_mcp/tools/tools_registry.py +0 -0
  79. {ha_mcp_dev-7.5.0.dev516 → ha_mcp_dev-7.5.0.dev518}/src/ha_mcp/tools/tools_resources.py +0 -0
  80. {ha_mcp_dev-7.5.0.dev516 → ha_mcp_dev-7.5.0.dev518}/src/ha_mcp/tools/tools_search.py +0 -0
  81. {ha_mcp_dev-7.5.0.dev516 → ha_mcp_dev-7.5.0.dev518}/src/ha_mcp/tools/tools_service.py +0 -0
  82. {ha_mcp_dev-7.5.0.dev516 → ha_mcp_dev-7.5.0.dev518}/src/ha_mcp/tools/tools_services.py +0 -0
  83. {ha_mcp_dev-7.5.0.dev516 → ha_mcp_dev-7.5.0.dev518}/src/ha_mcp/tools/tools_system.py +0 -0
  84. {ha_mcp_dev-7.5.0.dev516 → ha_mcp_dev-7.5.0.dev518}/src/ha_mcp/tools/tools_todo.py +0 -0
  85. {ha_mcp_dev-7.5.0.dev516 → ha_mcp_dev-7.5.0.dev518}/src/ha_mcp/tools/tools_traces.py +0 -0
  86. {ha_mcp_dev-7.5.0.dev516 → ha_mcp_dev-7.5.0.dev518}/src/ha_mcp/tools/tools_updates.py +0 -0
  87. {ha_mcp_dev-7.5.0.dev516 → ha_mcp_dev-7.5.0.dev518}/src/ha_mcp/tools/tools_utility.py +0 -0
  88. {ha_mcp_dev-7.5.0.dev516 → ha_mcp_dev-7.5.0.dev518}/src/ha_mcp/tools/tools_voice_assistant.py +0 -0
  89. {ha_mcp_dev-7.5.0.dev516 → ha_mcp_dev-7.5.0.dev518}/src/ha_mcp/tools/tools_yaml_config.py +0 -0
  90. {ha_mcp_dev-7.5.0.dev516 → ha_mcp_dev-7.5.0.dev518}/src/ha_mcp/tools/tools_zones.py +0 -0
  91. {ha_mcp_dev-7.5.0.dev516 → ha_mcp_dev-7.5.0.dev518}/src/ha_mcp/tools/util_helpers.py +0 -0
  92. {ha_mcp_dev-7.5.0.dev516 → ha_mcp_dev-7.5.0.dev518}/src/ha_mcp/transforms/__init__.py +0 -0
  93. {ha_mcp_dev-7.5.0.dev516 → ha_mcp_dev-7.5.0.dev518}/src/ha_mcp/transforms/categorized_search.py +0 -0
  94. {ha_mcp_dev-7.5.0.dev516 → ha_mcp_dev-7.5.0.dev518}/src/ha_mcp/transforms/lite_docstrings.py +0 -0
  95. {ha_mcp_dev-7.5.0.dev516 → ha_mcp_dev-7.5.0.dev518}/src/ha_mcp/utils/__init__.py +0 -0
  96. {ha_mcp_dev-7.5.0.dev516 → ha_mcp_dev-7.5.0.dev518}/src/ha_mcp/utils/config_hash.py +0 -0
  97. {ha_mcp_dev-7.5.0.dev516 → ha_mcp_dev-7.5.0.dev518}/src/ha_mcp/utils/data_paths.py +0 -0
  98. {ha_mcp_dev-7.5.0.dev516 → ha_mcp_dev-7.5.0.dev518}/src/ha_mcp/utils/domain_handlers.py +0 -0
  99. {ha_mcp_dev-7.5.0.dev516 → ha_mcp_dev-7.5.0.dev518}/src/ha_mcp/utils/fuzzy_search.py +0 -0
  100. {ha_mcp_dev-7.5.0.dev516 → ha_mcp_dev-7.5.0.dev518}/src/ha_mcp/utils/kill_signal_diagnostics.py +0 -0
  101. {ha_mcp_dev-7.5.0.dev516 → ha_mcp_dev-7.5.0.dev518}/src/ha_mcp/utils/operation_manager.py +0 -0
  102. {ha_mcp_dev-7.5.0.dev516 → ha_mcp_dev-7.5.0.dev518}/src/ha_mcp/utils/python_sandbox.py +0 -0
  103. {ha_mcp_dev-7.5.0.dev516 → ha_mcp_dev-7.5.0.dev518}/src/ha_mcp/utils/usage_logger.py +0 -0
  104. {ha_mcp_dev-7.5.0.dev516 → ha_mcp_dev-7.5.0.dev518}/src/ha_mcp_dev.egg-info/SOURCES.txt +0 -0
  105. {ha_mcp_dev-7.5.0.dev516 → ha_mcp_dev-7.5.0.dev518}/src/ha_mcp_dev.egg-info/dependency_links.txt +0 -0
  106. {ha_mcp_dev-7.5.0.dev516 → ha_mcp_dev-7.5.0.dev518}/src/ha_mcp_dev.egg-info/entry_points.txt +0 -0
  107. {ha_mcp_dev-7.5.0.dev516 → ha_mcp_dev-7.5.0.dev518}/src/ha_mcp_dev.egg-info/requires.txt +0 -0
  108. {ha_mcp_dev-7.5.0.dev516 → ha_mcp_dev-7.5.0.dev518}/src/ha_mcp_dev.egg-info/top_level.txt +0 -0
  109. {ha_mcp_dev-7.5.0.dev516 → ha_mcp_dev-7.5.0.dev518}/tests/__init__.py +0 -0
  110. {ha_mcp_dev-7.5.0.dev516 → ha_mcp_dev-7.5.0.dev518}/tests/test_constants.py +0 -0
  111. {ha_mcp_dev-7.5.0.dev516 → ha_mcp_dev-7.5.0.dev518}/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.5.0.dev516
3
+ Version: 7.5.0.dev518
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.5.0.dev516"
7
+ version = "7.5.0.dev518"
8
8
  description = "Home Assistant MCP Server - Complete control of Home Assistant through MCP"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.13,<3.14"
@@ -867,6 +867,69 @@ def _validate_numeric_range(
867
867
  )
868
868
 
869
869
 
870
+ def _validate_initial_in_options(
871
+ options: Any, initial: Any, helper_type: str = "input_select"
872
+ ) -> None:
873
+ """Reject ``initial`` values not in ``options``.
874
+
875
+ Called from both create and update branches with the resolved values —
876
+ caller-supplied on create, merged with the existing config on update.
877
+ ``initial=None`` is the unset case and passes through. The
878
+ ``isinstance(options, list)`` early-return mirrors the defensive shape
879
+ check in ``_validate_input_select_options`` below — both validators are
880
+ invariant gates, not type contracts; a future non-list caller is
881
+ silently skipped rather than raising a confusing ``TypeError`` on
882
+ ``initial not in options``.
883
+ """
884
+ if not isinstance(options, list) or initial is None:
885
+ return
886
+ if initial not in options:
887
+ raise_tool_error(
888
+ create_error_response(
889
+ ErrorCode.VALIDATION_INVALID_PARAMETER,
890
+ f"initial={initial!r} must be one of options "
891
+ f"{options!r} for {helper_type}.",
892
+ context=_simple_helper_error_context(
893
+ helper_type,
894
+ initial=initial,
895
+ options=options,
896
+ ),
897
+ suggestions=[
898
+ "Pick an `initial` value that's in `options`.",
899
+ "Or omit `initial` to use the default or existing value.",
900
+ ],
901
+ )
902
+ )
903
+
904
+
905
+ def _validate_datetime_has_date_or_time(
906
+ has_date: bool | None, has_time: bool | None
907
+ ) -> None:
908
+ """Reject ``input_datetime`` payloads where both components are False.
909
+
910
+ Treats ``None`` as "not constrained" — only the explicit (False, False)
911
+ case is flagged, since that's what reaches HA as the broken-entity
912
+ payload. Both the create and update branches call this with the
913
+ resolved-after-merge ``has_date`` / ``has_time`` pair.
914
+ """
915
+ if has_date is False and has_time is False:
916
+ raise_tool_error(
917
+ create_error_response(
918
+ ErrorCode.VALIDATION_INVALID_PARAMETER,
919
+ "At least one of has_date or has_time must be True for input_datetime",
920
+ context=_simple_helper_error_context(
921
+ "input_datetime",
922
+ has_date=has_date,
923
+ has_time=has_time,
924
+ ),
925
+ suggestions=[
926
+ "Set has_date=True to keep the date component.",
927
+ "Set has_time=True to keep the time component.",
928
+ ],
929
+ )
930
+ )
931
+
932
+
870
933
  def _validate_input_select_options(options: Any) -> None:
871
934
  """Reject input_select option lists containing duplicates (Bug 17, issue #1150).
872
935
 
@@ -1409,6 +1472,69 @@ async def _apply_registry_updates_to_entity(
1409
1472
  return applied
1410
1473
 
1411
1474
 
1475
+ class HelperResponse(TypedDict, total=False):
1476
+ """Uniform response contract for ``ha_config_set_helper`` (issue #1293).
1477
+
1478
+ Documents the legal key set across all three branches (create, update,
1479
+ flow). ``total=False`` because per-branch fields (entity_id, flow extras,
1480
+ warnings) are conditional. Consumed by ``_helper_response`` below — all
1481
+ return literals in this module funnel through that builder so the shape
1482
+ has a single point of construction.
1483
+ """
1484
+
1485
+ success: bool
1486
+ action: str # "create" | "update"
1487
+ helper_type: str
1488
+ data: dict[str, Any]
1489
+ entity_id: str # absent on flow branch (use entity_ids[] for multi-entity)
1490
+ message: str | None
1491
+ warnings: list[str] # omitted when empty
1492
+ # Flow-helper convenience accessors (only set on the flow branch).
1493
+ method: str
1494
+ entry_id: str | None
1495
+ title: str | None
1496
+ updated: bool
1497
+ entity_ids: list[str]
1498
+ area_id: str | None
1499
+ labels: list[str]
1500
+ category: str
1501
+ applied: list[dict[str, Any]]
1502
+
1503
+
1504
+ def _helper_response(
1505
+ action: str,
1506
+ helper_type: str,
1507
+ *,
1508
+ data: dict[str, Any],
1509
+ entity_id: str | None = None,
1510
+ message: str | None = None,
1511
+ warnings: list[str] | None = None,
1512
+ **extras: Any,
1513
+ ) -> dict[str, Any]:
1514
+ """Single construction point for the ``ha_config_set_helper`` response.
1515
+
1516
+ Enforces the uniform shape from issue #1293: ``success`` → ``action`` →
1517
+ ``helper_type`` → ``data`` → ``entity_id`` (when present) → ``message`` →
1518
+ flow-helper extras → ``warnings`` (only when non-empty). Returning
1519
+ ``dict[str, Any]`` rather than ``HelperResponse`` keeps the call sites
1520
+ free of mypy gymnastics around the dynamic ``**extras`` keys; the
1521
+ TypedDict serves as the readable contract anchor instead.
1522
+ """
1523
+ resp: dict[str, Any] = {
1524
+ "success": True,
1525
+ "action": action,
1526
+ "helper_type": helper_type,
1527
+ "data": data,
1528
+ }
1529
+ if entity_id is not None:
1530
+ resp["entity_id"] = entity_id
1531
+ resp["message"] = message
1532
+ resp.update(extras)
1533
+ if warnings:
1534
+ resp["warnings"] = warnings
1535
+ return resp
1536
+
1537
+
1412
1538
  async def _handle_flow_helper(
1413
1539
  client: Any,
1414
1540
  helper_type: str,
@@ -1562,18 +1688,15 @@ async def _handle_flow_helper(
1562
1688
  helper_id, # type: ignore[arg-type]
1563
1689
  )
1564
1690
 
1691
+ # Cache the flow_result keys read multiple times below (issue #1293
1692
+ # follow-up: reduces three .get() lookups for entry_id and two for title
1693
+ # to one each, and makes the post-flow logic readable as a sequence of
1694
+ # named values rather than repeated dict access). ``.get()`` is kept —
1695
+ # ``create_flow_helper`` propagates HA's optional entry_id via ``.get()``
1696
+ # too (tools_config_entry_flow.py:L700), so a missing key must surface as
1697
+ # None for the ``if entry_id:`` guard below, not raise KeyError.
1565
1698
  entry_id = flow_result.get("entry_id")
1566
- result: dict[str, Any] = {
1567
- "success": True,
1568
- "action": action,
1569
- "helper_type": helper_type,
1570
- "method": "config_flow",
1571
- "entry_id": entry_id,
1572
- "title": flow_result.get("title"),
1573
- "message": flow_result.get("message"),
1574
- }
1575
- if action == "update":
1576
- result["updated"] = True
1699
+ title = flow_result.get("title")
1577
1700
 
1578
1701
  # Resolve all entities for this config entry (multi-entity helpers handled naturally).
1579
1702
  # For create with wait=True, poll briefly for at least one entity to appear —
@@ -1617,7 +1740,22 @@ async def _handle_flow_helper(
1617
1740
  else:
1618
1741
  entities = await _get_entities_for_config_entry(client, entry_id, warnings)
1619
1742
  entity_ids = [e["entity_id"] for e in entities if e.get("entity_id")]
1620
- result["entity_ids"] = entity_ids
1743
+
1744
+ # ``data`` mirrors the HA flow_result payload for cross-action uniformity
1745
+ # with the create/update branches (issue #1293). ``entry_id`` and ``title``
1746
+ # also stay flat as convenience accessors — they are the primary identifiers
1747
+ # callers reach for, and remain heavily used by per-action metadata
1748
+ # consumers throughout the codebase. Per-entity registry-write outcomes
1749
+ # live in the ``applied`` flat array, not nested in ``data``, because one
1750
+ # flow can yield N entities with different per-entity results.
1751
+ extras: dict[str, Any] = {
1752
+ "method": "config_flow",
1753
+ "entry_id": entry_id,
1754
+ "title": title,
1755
+ "entity_ids": entity_ids,
1756
+ }
1757
+ if action == "update":
1758
+ extras["updated"] = True
1621
1759
 
1622
1760
  # Apply registry updates (area_id / labels / category) to every entity.
1623
1761
  # Use `is not None` so an explicit empty value (area_id="" or labels=[])
@@ -1640,17 +1778,21 @@ async def _handle_flow_helper(
1640
1778
  )
1641
1779
  )
1642
1780
  if area_id is not None:
1643
- result["area_id"] = area_id if area_id else None
1781
+ extras["area_id"] = area_id if area_id else None
1644
1782
  if labels_list is not None:
1645
- result["labels"] = labels_list
1783
+ extras["labels"] = labels_list
1646
1784
  if category:
1647
- result["category"] = category
1648
- result["applied"] = applied_per_entity
1649
-
1650
- if warnings:
1651
- result["warnings"] = warnings
1652
-
1653
- return result
1785
+ extras["category"] = category
1786
+ extras["applied"] = applied_per_entity
1787
+
1788
+ return _helper_response(
1789
+ action,
1790
+ helper_type,
1791
+ data={"entry_id": entry_id, "title": title},
1792
+ message=flow_result.get("message"),
1793
+ warnings=warnings,
1794
+ **extras,
1795
+ )
1654
1796
 
1655
1797
 
1656
1798
  def _format_schedule_days(
@@ -2398,28 +2540,12 @@ def register_config_helper_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
2398
2540
  )
2399
2541
  )
2400
2542
  message["options"] = options
2401
- # Bug 4a (issue #1150): if `initial` was passed but isn't
2402
- # one of the options, reject explicitly instead of silently
2403
- # dropping. The previous `if initial and initial in options`
2404
- # check stripped invalid initials with `success: true`.
2543
+ # If `initial` was passed but isn't one of the options,
2544
+ # reject explicitly instead of silently dropping. Shared
2545
+ # with the update branch via the helper so the same
2546
+ # invariant fires on both code paths.
2547
+ _validate_initial_in_options(options, initial)
2405
2548
  if initial is not None:
2406
- if initial not in options:
2407
- raise_tool_error(
2408
- create_error_response(
2409
- ErrorCode.VALIDATION_INVALID_PARAMETER,
2410
- f"initial={initial!r} must be one of options "
2411
- f"{options!r} for input_select.",
2412
- context=_simple_helper_error_context(
2413
- helper_type,
2414
- initial=initial,
2415
- options=options,
2416
- ),
2417
- suggestions=[
2418
- "Pick an `initial` value that's in `options`.",
2419
- "Or omit `initial` so the entity starts unset.",
2420
- ],
2421
- )
2422
- )
2423
2549
  message["initial"] = initial
2424
2550
 
2425
2551
  elif helper_type == "input_number":
@@ -2477,15 +2603,12 @@ def register_config_helper_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
2477
2603
  message["has_date"] = has_date
2478
2604
  message["has_time"] = has_time
2479
2605
 
2480
- # Validate that at least one is True
2481
- if not message["has_date"] and not message["has_time"]:
2482
- raise_tool_error(
2483
- create_error_response(
2484
- ErrorCode.VALIDATION_INVALID_PARAMETER,
2485
- "At least one of has_date or has_time must be True for input_datetime",
2486
- context=_simple_helper_error_context(helper_type),
2487
- )
2488
- )
2606
+ # Validate that at least one is True — shared with the
2607
+ # update branch via the helper so the same invariant
2608
+ # fires on both code paths.
2609
+ _validate_datetime_has_date_or_time(
2610
+ message["has_date"], message["has_time"]
2611
+ )
2489
2612
 
2490
2613
  if initial is not None:
2491
2614
  message["initial"] = initial
@@ -2616,6 +2739,12 @@ def register_config_helper_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
2616
2739
  if not entity_id and helper_data.get("id"):
2617
2740
  entity_id = f"{helper_type}.{helper_data['id']}"
2618
2741
 
2742
+ # Issue #1293: collect warnings in a top-level list rather
2743
+ # than nest them in the payload dict. Aligns this branch
2744
+ # with the flow-helper path and lets callers do
2745
+ # ``result.get("warnings", [])`` uniformly.
2746
+ warnings: list[str] = []
2747
+
2619
2748
  # Wait for entity to be properly registered before proceeding
2620
2749
  wait_bool = coerce_bool_param(wait, "wait", default=True)
2621
2750
  if wait_bool and entity_id:
@@ -2624,11 +2753,11 @@ def register_config_helper_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
2624
2753
  client, entity_id
2625
2754
  )
2626
2755
  if not registered:
2627
- helper_data["warning"] = (
2756
+ warnings.append(
2628
2757
  f"Helper created but {entity_id} not yet queryable. It may take a moment to become available."
2629
2758
  )
2630
2759
  except Exception as e:
2631
- helper_data["warning"] = (
2760
+ warnings.append(
2632
2761
  f"Helper created but verification failed: {e}"
2633
2762
  )
2634
2763
 
@@ -2647,6 +2776,11 @@ def register_config_helper_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
2647
2776
  update_message
2648
2777
  )
2649
2778
  if update_result.get("success"):
2779
+ # Mirror the update branch's icon propagation
2780
+ # (line ~3343) so the create response's ``data``
2781
+ # carries the same registry-write echo set.
2782
+ if icon is not None:
2783
+ helper_data["icon"] = icon if icon else None
2650
2784
  if area_id is not None:
2651
2785
  helper_data["area_id"] = area_id if area_id else None
2652
2786
  if labels is not None:
@@ -2658,29 +2792,39 @@ def register_config_helper_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
2658
2792
  if isinstance(error_detail, dict)
2659
2793
  else str(error_detail)
2660
2794
  )
2661
- helper_data["warning"] = (
2795
+ warnings.append(
2662
2796
  f"Helper created but entity registry update failed: {error_msg}"
2663
2797
  )
2664
2798
 
2665
- # Apply category via shared helper (consistent with automations/scripts)
2799
+ # Apply category via shared helper (consistent with automations/scripts).
2800
+ # Issue #1293: route the success/failure through ``cat_result`` so any
2801
+ # ``category_warning`` lands in the top-level ``warnings`` list instead
2802
+ # of leaking nested into ``helper_data``. Mirrors the precedent in
2803
+ # ``_handle_flow_helper`` (the ``cat_result`` block near the end of
2804
+ # ``_apply_registry_updates_to_entity``).
2666
2805
  if category and entity_id:
2806
+ cat_result: dict[str, Any] = {}
2667
2807
  await apply_entity_category(
2668
2808
  client,
2669
2809
  entity_id,
2670
2810
  category,
2671
2811
  "helpers",
2672
- helper_data,
2812
+ cat_result,
2673
2813
  "helper",
2674
2814
  )
2815
+ if "category" in cat_result:
2816
+ helper_data["category"] = cat_result["category"]
2817
+ elif "category_warning" in cat_result:
2818
+ warnings.append(cat_result["category_warning"])
2675
2819
 
2676
- return {
2677
- "success": True,
2678
- "action": "create",
2679
- "helper_type": helper_type,
2680
- "helper_data": helper_data,
2681
- "entity_id": entity_id,
2682
- "message": f"Successfully created {helper_type}: {name}",
2683
- }
2820
+ return _helper_response(
2821
+ "create",
2822
+ helper_type,
2823
+ data=helper_data,
2824
+ entity_id=entity_id,
2825
+ message=f"Successfully created {helper_type}: {name}",
2826
+ warnings=warnings,
2827
+ )
2684
2828
  else:
2685
2829
  raise_tool_error(
2686
2830
  create_error_response(
@@ -2728,6 +2872,11 @@ def register_config_helper_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
2728
2872
  }
2729
2873
 
2730
2874
  updated_data: dict[str, Any] = {}
2875
+ # Issue #1293: collect warnings in a top-level list for the
2876
+ # update path too, mirroring create + flow-helper branches.
2877
+ # (No re-annotation — the create branch above already defined
2878
+ # ``warnings: list[str]``; mypy treats this as the same binding.)
2879
+ warnings = []
2731
2880
 
2732
2881
  if helper_type == "tag":
2733
2882
  # Tags use their own registry — no entity registry entries.
@@ -2762,14 +2911,19 @@ def register_config_helper_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
2762
2911
 
2763
2912
  # Tags don't have entity registry entries, so return directly
2764
2913
  # without wait_for_entity_registered (they're not entities).
2765
- return {
2766
- "success": True,
2767
- "action": "update",
2768
- "helper_type": helper_type,
2769
- "entity_id": entity_id,
2770
- "updated_data": updated_data,
2771
- "message": f"Successfully updated {helper_type}: {entity_id}",
2772
- }
2914
+ # Issue #1293: same uniform builder as the sibling create/update
2915
+ # branches. No producer appends to ``warnings`` on the tag path
2916
+ # today, but ``_helper_response`` already omits the key when the
2917
+ # list is empty — future warnings flow through the same contract
2918
+ # without further plumbing.
2919
+ return _helper_response(
2920
+ "update",
2921
+ helper_type,
2922
+ data=updated_data,
2923
+ entity_id=entity_id,
2924
+ message=f"Successfully updated {helper_type}: {entity_id}",
2925
+ warnings=warnings,
2926
+ )
2773
2927
 
2774
2928
  elif helper_type in config_store_types:
2775
2929
  # Person and zone: look up unique_id from entity registry
@@ -3048,6 +3202,14 @@ def register_config_helper_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
3048
3202
  if initial is not None
3049
3203
  else existing.get("initial")
3050
3204
  )
3205
+ # Parity with the create-branch guard. Resolves to
3206
+ # (new options, new initial) / (new options, old
3207
+ # initial) / (old options, new initial) — any
3208
+ # combination that excludes initial from the final
3209
+ # options list is caught.
3210
+ _validate_initial_in_options(
3211
+ update_msg["options"], initial_val
3212
+ )
3051
3213
  if initial_val is not None:
3052
3214
  update_msg["initial"] = initial_val
3053
3215
 
@@ -3140,6 +3302,14 @@ def register_config_helper_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
3140
3302
  if has_time is not None
3141
3303
  else existing.get("has_time", False)
3142
3304
  )
3305
+ # Parity with the create-branch guard. A merge
3306
+ # that resolves to (False, False) — caller
3307
+ # disabling the one component the existing entity
3308
+ # had — would otherwise write a broken-entity
3309
+ # payload and surface HA's cryptic generic error.
3310
+ _validate_datetime_has_date_or_time(
3311
+ update_msg["has_date"], update_msg["has_time"]
3312
+ )
3143
3313
  initial_val = (
3144
3314
  initial
3145
3315
  if initial is not None
@@ -3230,30 +3400,49 @@ def register_config_helper_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
3230
3400
  reg_result = await client.send_websocket_message(
3231
3401
  registry_update
3232
3402
  )
3233
- if not reg_result.get("success"):
3403
+ if reg_result.get("success"):
3404
+ # Issue #1293: mirror the create branch by propagating the
3405
+ # registry writes into ``updated_data`` so the response's
3406
+ # ``data`` reflects the post-update state.
3407
+ if icon is not None:
3408
+ updated_data["icon"] = icon if icon else None
3409
+ if area_id is not None:
3410
+ updated_data["area_id"] = area_id if area_id else None
3411
+ if labels is not None:
3412
+ updated_data["labels"] = labels
3413
+ else:
3234
3414
  error_detail = reg_result.get("error", {})
3235
3415
  error_msg = (
3236
3416
  error_detail.get("message", "Unknown error")
3237
3417
  if isinstance(error_detail, dict)
3238
3418
  else str(error_detail)
3239
3419
  )
3240
- logger.warning(
3241
- f"Entity registry update failed for {entity_id}: {error_msg}"
3242
- )
3243
- updated_data["warning"] = (
3420
+ # No ``logger.warning`` here — the create-branch and
3421
+ # flow-helper registry-update failure paths surface this
3422
+ # via ``warnings.append`` only, and the response carries
3423
+ # the message to the caller in the top-level ``warnings``
3424
+ # list. Logging again would double-report.
3425
+ warnings.append(
3244
3426
  f"Config updated but entity registry update failed: {error_msg}"
3245
3427
  )
3246
3428
 
3247
- # Apply category via shared helper
3429
+ # Apply category via shared helper. Issue #1293: route through
3430
+ # ``cat_result`` so any ``category_warning`` lands in the top-level
3431
+ # ``warnings`` list instead of nested in ``updated_data``.
3248
3432
  if category:
3433
+ cat_result = {}
3249
3434
  await apply_entity_category(
3250
3435
  client,
3251
3436
  entity_id,
3252
3437
  category,
3253
3438
  "helpers",
3254
- updated_data,
3439
+ cat_result,
3255
3440
  "helper",
3256
3441
  )
3442
+ if "category" in cat_result:
3443
+ updated_data["category"] = cat_result["category"]
3444
+ elif "category_warning" in cat_result:
3445
+ warnings.append(cat_result["category_warning"])
3257
3446
 
3258
3447
  else:
3259
3448
  # Fallback for unknown/future helper types: entity registry update only
@@ -3287,39 +3476,43 @@ def register_config_helper_tools(mcp: Any, client: Any, **kwargs: Any) -> None:
3287
3476
  )
3288
3477
  )
3289
3478
 
3290
- # Apply category via shared helper
3479
+ # Apply category via shared helper. Issue #1293: same temp-dict
3480
+ # routing as the simple-helper branch above.
3291
3481
  if category:
3482
+ cat_result = {}
3292
3483
  await apply_entity_category(
3293
3484
  client,
3294
3485
  entity_id,
3295
3486
  category,
3296
3487
  "helpers",
3297
- updated_data,
3488
+ cat_result,
3298
3489
  "helper",
3299
3490
  )
3491
+ if "category" in cat_result:
3492
+ updated_data["category"] = cat_result["category"]
3493
+ elif "category_warning" in cat_result:
3494
+ warnings.append(cat_result["category_warning"])
3300
3495
 
3301
3496
  # Wait for entity to reflect the update
3302
3497
  wait_bool = coerce_bool_param(wait, "wait", default=True)
3303
- response: dict[str, Any] = {
3304
- "success": True,
3305
- "action": "update",
3306
- "helper_type": helper_type,
3307
- "entity_id": entity_id,
3308
- "updated_data": updated_data,
3309
- "message": f"Successfully updated {helper_type}: {entity_id}",
3310
- }
3311
3498
  if wait_bool:
3312
3499
  try:
3313
3500
  registered = await wait_for_entity_registered(client, entity_id)
3314
3501
  if not registered:
3315
- response["warning"] = (
3502
+ warnings.append(
3316
3503
  f"Update applied but {entity_id} not yet queryable."
3317
3504
  )
3318
3505
  except Exception as e:
3319
- response["warning"] = (
3320
- f"Update applied but verification failed: {e}"
3321
- )
3322
- return response
3506
+ warnings.append(f"Update applied but verification failed: {e}")
3507
+
3508
+ return _helper_response(
3509
+ "update",
3510
+ helper_type,
3511
+ data=updated_data,
3512
+ entity_id=entity_id,
3513
+ message=f"Successfully updated {helper_type}: {entity_id}",
3514
+ warnings=warnings,
3515
+ )
3323
3516
 
3324
3517
  # This should never be reached since action is either "create" or "update"
3325
3518
  raise_tool_error(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ha-mcp-dev
3
- Version: 7.5.0.dev516
3
+ Version: 7.5.0.dev518
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