ha-mcp-dev 7.7.0.dev685__tar.gz → 7.7.0.dev687__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 (142) hide show
  1. {ha_mcp_dev-7.7.0.dev685/src/ha_mcp_dev.egg-info → ha_mcp_dev-7.7.0.dev687}/PKG-INFO +4 -4
  2. {ha_mcp_dev-7.7.0.dev685 → ha_mcp_dev-7.7.0.dev687}/README.md +3 -3
  3. {ha_mcp_dev-7.7.0.dev685 → ha_mcp_dev-7.7.0.dev687}/pyproject.toml +1 -1
  4. {ha_mcp_dev-7.7.0.dev685 → ha_mcp_dev-7.7.0.dev687}/src/ha_mcp/settings.css +162 -20
  5. {ha_mcp_dev-7.7.0.dev685 → ha_mcp_dev-7.7.0.dev687}/src/ha_mcp/settings.js +276 -0
  6. {ha_mcp_dev-7.7.0.dev685 → ha_mcp_dev-7.7.0.dev687}/src/ha_mcp/settings_ui.py +384 -3
  7. {ha_mcp_dev-7.7.0.dev685 → ha_mcp_dev-7.7.0.dev687}/src/ha_mcp/stdio_settings_sidecar.py +14 -0
  8. {ha_mcp_dev-7.7.0.dev685 → ha_mcp_dev-7.7.0.dev687}/src/ha_mcp/tools/tools_filesystem.py +8 -5
  9. {ha_mcp_dev-7.7.0.dev685 → ha_mcp_dev-7.7.0.dev687}/src/ha_mcp/tools/tools_system.py +48 -2
  10. ha_mcp_dev-7.7.0.dev687/src/ha_mcp/tools/tools_themes.py +191 -0
  11. {ha_mcp_dev-7.7.0.dev685 → ha_mcp_dev-7.7.0.dev687}/src/ha_mcp/tools/tools_yaml_config.py +33 -7
  12. {ha_mcp_dev-7.7.0.dev685 → ha_mcp_dev-7.7.0.dev687}/src/ha_mcp/tools/util_helpers.py +17 -0
  13. {ha_mcp_dev-7.7.0.dev685 → ha_mcp_dev-7.7.0.dev687/src/ha_mcp_dev.egg-info}/PKG-INFO +4 -4
  14. {ha_mcp_dev-7.7.0.dev685 → ha_mcp_dev-7.7.0.dev687}/src/ha_mcp_dev.egg-info/SOURCES.txt +1 -0
  15. {ha_mcp_dev-7.7.0.dev685 → ha_mcp_dev-7.7.0.dev687}/LICENSE +0 -0
  16. {ha_mcp_dev-7.7.0.dev685 → ha_mcp_dev-7.7.0.dev687}/MANIFEST.in +0 -0
  17. {ha_mcp_dev-7.7.0.dev685 → ha_mcp_dev-7.7.0.dev687}/setup.cfg +0 -0
  18. {ha_mcp_dev-7.7.0.dev685 → ha_mcp_dev-7.7.0.dev687}/src/ha_mcp/__init__.py +0 -0
  19. {ha_mcp_dev-7.7.0.dev685 → ha_mcp_dev-7.7.0.dev687}/src/ha_mcp/__main__.py +0 -0
  20. {ha_mcp_dev-7.7.0.dev685 → ha_mcp_dev-7.7.0.dev687}/src/ha_mcp/_pypi_marker +0 -0
  21. {ha_mcp_dev-7.7.0.dev685 → ha_mcp_dev-7.7.0.dev687}/src/ha_mcp/_version.py +0 -0
  22. {ha_mcp_dev-7.7.0.dev685 → ha_mcp_dev-7.7.0.dev687}/src/ha_mcp/auth/__init__.py +0 -0
  23. {ha_mcp_dev-7.7.0.dev685 → ha_mcp_dev-7.7.0.dev687}/src/ha_mcp/auth/consent_form.py +0 -0
  24. {ha_mcp_dev-7.7.0.dev685 → ha_mcp_dev-7.7.0.dev687}/src/ha_mcp/auth/provider.py +0 -0
  25. {ha_mcp_dev-7.7.0.dev685 → ha_mcp_dev-7.7.0.dev687}/src/ha_mcp/backup_manager.py +0 -0
  26. {ha_mcp_dev-7.7.0.dev685 → ha_mcp_dev-7.7.0.dev687}/src/ha_mcp/client/__init__.py +0 -0
  27. {ha_mcp_dev-7.7.0.dev685 → ha_mcp_dev-7.7.0.dev687}/src/ha_mcp/client/rest_client.py +0 -0
  28. {ha_mcp_dev-7.7.0.dev685 → ha_mcp_dev-7.7.0.dev687}/src/ha_mcp/client/supervisor_client.py +0 -0
  29. {ha_mcp_dev-7.7.0.dev685 → ha_mcp_dev-7.7.0.dev687}/src/ha_mcp/client/websocket_client.py +0 -0
  30. {ha_mcp_dev-7.7.0.dev685 → ha_mcp_dev-7.7.0.dev687}/src/ha_mcp/client/websocket_listener.py +0 -0
  31. {ha_mcp_dev-7.7.0.dev685 → ha_mcp_dev-7.7.0.dev687}/src/ha_mcp/config.py +0 -0
  32. {ha_mcp_dev-7.7.0.dev685 → ha_mcp_dev-7.7.0.dev687}/src/ha_mcp/dashboard_screenshot/__init__.py +0 -0
  33. {ha_mcp_dev-7.7.0.dev685 → ha_mcp_dev-7.7.0.dev687}/src/ha_mcp/dashboard_screenshot/capture.py +0 -0
  34. {ha_mcp_dev-7.7.0.dev685 → ha_mcp_dev-7.7.0.dev687}/src/ha_mcp/dashboard_screenshot/provision.py +0 -0
  35. {ha_mcp_dev-7.7.0.dev685 → ha_mcp_dev-7.7.0.dev687}/src/ha_mcp/errors.py +0 -0
  36. {ha_mcp_dev-7.7.0.dev685 → ha_mcp_dev-7.7.0.dev687}/src/ha_mcp/policy/__init__.py +0 -0
  37. {ha_mcp_dev-7.7.0.dev685 → ha_mcp_dev-7.7.0.dev687}/src/ha_mcp/policy/approval_queue.py +0 -0
  38. {ha_mcp_dev-7.7.0.dev685 → ha_mcp_dev-7.7.0.dev687}/src/ha_mcp/policy/evaluator.py +0 -0
  39. {ha_mcp_dev-7.7.0.dev685 → ha_mcp_dev-7.7.0.dev687}/src/ha_mcp/policy/handlers.py +0 -0
  40. {ha_mcp_dev-7.7.0.dev685 → ha_mcp_dev-7.7.0.dev687}/src/ha_mcp/policy/middleware.py +0 -0
  41. {ha_mcp_dev-7.7.0.dev685 → ha_mcp_dev-7.7.0.dev687}/src/ha_mcp/policy/model.py +0 -0
  42. {ha_mcp_dev-7.7.0.dev685 → ha_mcp_dev-7.7.0.dev687}/src/ha_mcp/policy/persistence.py +0 -0
  43. {ha_mcp_dev-7.7.0.dev685 → ha_mcp_dev-7.7.0.dev687}/src/ha_mcp/policy/value_sources.py +0 -0
  44. {ha_mcp_dev-7.7.0.dev685 → ha_mcp_dev-7.7.0.dev687}/src/ha_mcp/py.typed +0 -0
  45. {ha_mcp_dev-7.7.0.dev685 → ha_mcp_dev-7.7.0.dev687}/src/ha_mcp/read_only.py +0 -0
  46. {ha_mcp_dev-7.7.0.dev685 → ha_mcp_dev-7.7.0.dev687}/src/ha_mcp/resources/skills-vendor/.claude/settings.json +0 -0
  47. {ha_mcp_dev-7.7.0.dev685 → ha_mcp_dev-7.7.0.dev687}/src/ha_mcp/resources/skills-vendor/.claude-plugin/marketplace.json +0 -0
  48. {ha_mcp_dev-7.7.0.dev685 → ha_mcp_dev-7.7.0.dev687}/src/ha_mcp/resources/skills-vendor/.claude-plugin/plugin.json +0 -0
  49. {ha_mcp_dev-7.7.0.dev685 → ha_mcp_dev-7.7.0.dev687}/src/ha_mcp/resources/skills-vendor/.github/ISSUE_TEMPLATE/skill-rca.md +0 -0
  50. {ha_mcp_dev-7.7.0.dev685 → ha_mcp_dev-7.7.0.dev687}/src/ha_mcp/resources/skills-vendor/.github/pull_request_template.md +0 -0
  51. {ha_mcp_dev-7.7.0.dev685 → ha_mcp_dev-7.7.0.dev687}/src/ha_mcp/resources/skills-vendor/AGENTS.md +0 -0
  52. {ha_mcp_dev-7.7.0.dev685 → ha_mcp_dev-7.7.0.dev687}/src/ha_mcp/resources/skills-vendor/CLAUDE.md +0 -0
  53. {ha_mcp_dev-7.7.0.dev685 → ha_mcp_dev-7.7.0.dev687}/src/ha_mcp/resources/skills-vendor/CONTRIBUTING.md +0 -0
  54. {ha_mcp_dev-7.7.0.dev685 → ha_mcp_dev-7.7.0.dev687}/src/ha_mcp/resources/skills-vendor/LICENSE +0 -0
  55. {ha_mcp_dev-7.7.0.dev685 → ha_mcp_dev-7.7.0.dev687}/src/ha_mcp/resources/skills-vendor/README.md +0 -0
  56. {ha_mcp_dev-7.7.0.dev685 → ha_mcp_dev-7.7.0.dev687}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/SKILL.md +0 -0
  57. {ha_mcp_dev-7.7.0.dev685 → ha_mcp_dev-7.7.0.dev687}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/evals/evals.json +0 -0
  58. {ha_mcp_dev-7.7.0.dev685 → ha_mcp_dev-7.7.0.dev687}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/automation-patterns.md +0 -0
  59. {ha_mcp_dev-7.7.0.dev685 → ha_mcp_dev-7.7.0.dev687}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-cards.md +0 -0
  60. {ha_mcp_dev-7.7.0.dev685 → ha_mcp_dev-7.7.0.dev687}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-guide.md +0 -0
  61. {ha_mcp_dev-7.7.0.dev685 → ha_mcp_dev-7.7.0.dev687}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/device-control.md +0 -0
  62. {ha_mcp_dev-7.7.0.dev685 → ha_mcp_dev-7.7.0.dev687}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/domain-docs.md +0 -0
  63. {ha_mcp_dev-7.7.0.dev685 → ha_mcp_dev-7.7.0.dev687}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/examples.yaml +0 -0
  64. {ha_mcp_dev-7.7.0.dev685 → ha_mcp_dev-7.7.0.dev687}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/helper-selection.md +0 -0
  65. {ha_mcp_dev-7.7.0.dev685 → ha_mcp_dev-7.7.0.dev687}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/safe-refactoring.md +0 -0
  66. {ha_mcp_dev-7.7.0.dev685 → ha_mcp_dev-7.7.0.dev687}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/template-guidelines.md +0 -0
  67. {ha_mcp_dev-7.7.0.dev685 → ha_mcp_dev-7.7.0.dev687}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/yaml-only-integrations.md +0 -0
  68. {ha_mcp_dev-7.7.0.dev685 → ha_mcp_dev-7.7.0.dev687}/src/ha_mcp/server.py +0 -0
  69. {ha_mcp_dev-7.7.0.dev685 → ha_mcp_dev-7.7.0.dev687}/src/ha_mcp/smoke_test.py +0 -0
  70. {ha_mcp_dev-7.7.0.dev685 → ha_mcp_dev-7.7.0.dev687}/src/ha_mcp/tools/__init__.py +0 -0
  71. {ha_mcp_dev-7.7.0.dev685 → ha_mcp_dev-7.7.0.dev687}/src/ha_mcp/tools/auto_backup.py +0 -0
  72. {ha_mcp_dev-7.7.0.dev685 → ha_mcp_dev-7.7.0.dev687}/src/ha_mcp/tools/backup.py +0 -0
  73. {ha_mcp_dev-7.7.0.dev685 → ha_mcp_dev-7.7.0.dev687}/src/ha_mcp/tools/best_practice_checker.py +0 -0
  74. {ha_mcp_dev-7.7.0.dev685 → ha_mcp_dev-7.7.0.dev687}/src/ha_mcp/tools/config_entry_flow.py +0 -0
  75. {ha_mcp_dev-7.7.0.dev685 → ha_mcp_dev-7.7.0.dev687}/src/ha_mcp/tools/device_control.py +0 -0
  76. {ha_mcp_dev-7.7.0.dev685 → ha_mcp_dev-7.7.0.dev687}/src/ha_mcp/tools/enhanced.py +0 -0
  77. {ha_mcp_dev-7.7.0.dev685 → ha_mcp_dev-7.7.0.dev687}/src/ha_mcp/tools/helpers.py +0 -0
  78. {ha_mcp_dev-7.7.0.dev685 → ha_mcp_dev-7.7.0.dev687}/src/ha_mcp/tools/reference_validator.py +0 -0
  79. {ha_mcp_dev-7.7.0.dev685 → ha_mcp_dev-7.7.0.dev687}/src/ha_mcp/tools/registry.py +0 -0
  80. {ha_mcp_dev-7.7.0.dev685 → ha_mcp_dev-7.7.0.dev687}/src/ha_mcp/tools/smart_search/__init__.py +0 -0
  81. {ha_mcp_dev-7.7.0.dev685 → ha_mcp_dev-7.7.0.dev687}/src/ha_mcp/tools/smart_search/_base.py +0 -0
  82. {ha_mcp_dev-7.7.0.dev685 → ha_mcp_dev-7.7.0.dev687}/src/ha_mcp/tools/smart_search/_config.py +0 -0
  83. {ha_mcp_dev-7.7.0.dev685 → ha_mcp_dev-7.7.0.dev687}/src/ha_mcp/tools/smart_search/_deep.py +0 -0
  84. {ha_mcp_dev-7.7.0.dev685 → ha_mcp_dev-7.7.0.dev687}/src/ha_mcp/tools/smart_search/_entities.py +0 -0
  85. {ha_mcp_dev-7.7.0.dev685 → ha_mcp_dev-7.7.0.dev687}/src/ha_mcp/tools/smart_search/_fetch.py +0 -0
  86. {ha_mcp_dev-7.7.0.dev685 → ha_mcp_dev-7.7.0.dev687}/src/ha_mcp/tools/smart_search/_overview.py +0 -0
  87. {ha_mcp_dev-7.7.0.dev685 → ha_mcp_dev-7.7.0.dev687}/src/ha_mcp/tools/smart_search/_scenes.py +0 -0
  88. {ha_mcp_dev-7.7.0.dev685 → ha_mcp_dev-7.7.0.dev687}/src/ha_mcp/tools/smart_search/_scoring.py +0 -0
  89. {ha_mcp_dev-7.7.0.dev685 → ha_mcp_dev-7.7.0.dev687}/src/ha_mcp/tools/tools_addons.py +0 -0
  90. {ha_mcp_dev-7.7.0.dev685 → ha_mcp_dev-7.7.0.dev687}/src/ha_mcp/tools/tools_areas.py +0 -0
  91. {ha_mcp_dev-7.7.0.dev685 → ha_mcp_dev-7.7.0.dev687}/src/ha_mcp/tools/tools_blueprints.py +0 -0
  92. {ha_mcp_dev-7.7.0.dev685 → ha_mcp_dev-7.7.0.dev687}/src/ha_mcp/tools/tools_bug_report.py +0 -0
  93. {ha_mcp_dev-7.7.0.dev685 → ha_mcp_dev-7.7.0.dev687}/src/ha_mcp/tools/tools_calendar.py +0 -0
  94. {ha_mcp_dev-7.7.0.dev685 → ha_mcp_dev-7.7.0.dev687}/src/ha_mcp/tools/tools_camera.py +0 -0
  95. {ha_mcp_dev-7.7.0.dev685 → ha_mcp_dev-7.7.0.dev687}/src/ha_mcp/tools/tools_categories.py +0 -0
  96. {ha_mcp_dev-7.7.0.dev685 → ha_mcp_dev-7.7.0.dev687}/src/ha_mcp/tools/tools_code.py +0 -0
  97. {ha_mcp_dev-7.7.0.dev685 → ha_mcp_dev-7.7.0.dev687}/src/ha_mcp/tools/tools_config_automations.py +0 -0
  98. {ha_mcp_dev-7.7.0.dev685 → ha_mcp_dev-7.7.0.dev687}/src/ha_mcp/tools/tools_config_dashboards.py +0 -0
  99. {ha_mcp_dev-7.7.0.dev685 → ha_mcp_dev-7.7.0.dev687}/src/ha_mcp/tools/tools_config_helpers.py +0 -0
  100. {ha_mcp_dev-7.7.0.dev685 → ha_mcp_dev-7.7.0.dev687}/src/ha_mcp/tools/tools_config_scenes.py +0 -0
  101. {ha_mcp_dev-7.7.0.dev685 → ha_mcp_dev-7.7.0.dev687}/src/ha_mcp/tools/tools_config_scripts.py +0 -0
  102. {ha_mcp_dev-7.7.0.dev685 → ha_mcp_dev-7.7.0.dev687}/src/ha_mcp/tools/tools_dashboard_screenshot.py +0 -0
  103. {ha_mcp_dev-7.7.0.dev685 → ha_mcp_dev-7.7.0.dev687}/src/ha_mcp/tools/tools_energy.py +0 -0
  104. {ha_mcp_dev-7.7.0.dev685 → ha_mcp_dev-7.7.0.dev687}/src/ha_mcp/tools/tools_entities.py +0 -0
  105. {ha_mcp_dev-7.7.0.dev685 → ha_mcp_dev-7.7.0.dev687}/src/ha_mcp/tools/tools_groups.py +0 -0
  106. {ha_mcp_dev-7.7.0.dev685 → ha_mcp_dev-7.7.0.dev687}/src/ha_mcp/tools/tools_hacs.py +0 -0
  107. {ha_mcp_dev-7.7.0.dev685 → ha_mcp_dev-7.7.0.dev687}/src/ha_mcp/tools/tools_history.py +0 -0
  108. {ha_mcp_dev-7.7.0.dev685 → ha_mcp_dev-7.7.0.dev687}/src/ha_mcp/tools/tools_integrations.py +0 -0
  109. {ha_mcp_dev-7.7.0.dev685 → ha_mcp_dev-7.7.0.dev687}/src/ha_mcp/tools/tools_labels.py +0 -0
  110. {ha_mcp_dev-7.7.0.dev685 → ha_mcp_dev-7.7.0.dev687}/src/ha_mcp/tools/tools_mcp_component.py +0 -0
  111. {ha_mcp_dev-7.7.0.dev685 → ha_mcp_dev-7.7.0.dev687}/src/ha_mcp/tools/tools_registry.py +0 -0
  112. {ha_mcp_dev-7.7.0.dev685 → ha_mcp_dev-7.7.0.dev687}/src/ha_mcp/tools/tools_resources.py +0 -0
  113. {ha_mcp_dev-7.7.0.dev685 → ha_mcp_dev-7.7.0.dev687}/src/ha_mcp/tools/tools_search.py +0 -0
  114. {ha_mcp_dev-7.7.0.dev685 → ha_mcp_dev-7.7.0.dev687}/src/ha_mcp/tools/tools_service.py +0 -0
  115. {ha_mcp_dev-7.7.0.dev685 → ha_mcp_dev-7.7.0.dev687}/src/ha_mcp/tools/tools_services.py +0 -0
  116. {ha_mcp_dev-7.7.0.dev685 → ha_mcp_dev-7.7.0.dev687}/src/ha_mcp/tools/tools_todo.py +0 -0
  117. {ha_mcp_dev-7.7.0.dev685 → ha_mcp_dev-7.7.0.dev687}/src/ha_mcp/tools/tools_traces.py +0 -0
  118. {ha_mcp_dev-7.7.0.dev685 → ha_mcp_dev-7.7.0.dev687}/src/ha_mcp/tools/tools_updates.py +0 -0
  119. {ha_mcp_dev-7.7.0.dev685 → ha_mcp_dev-7.7.0.dev687}/src/ha_mcp/tools/tools_utility.py +0 -0
  120. {ha_mcp_dev-7.7.0.dev685 → ha_mcp_dev-7.7.0.dev687}/src/ha_mcp/tools/tools_voice_assistant.py +0 -0
  121. {ha_mcp_dev-7.7.0.dev685 → ha_mcp_dev-7.7.0.dev687}/src/ha_mcp/tools/tools_zones.py +0 -0
  122. {ha_mcp_dev-7.7.0.dev685 → ha_mcp_dev-7.7.0.dev687}/src/ha_mcp/tools/validation_middleware.py +0 -0
  123. {ha_mcp_dev-7.7.0.dev685 → ha_mcp_dev-7.7.0.dev687}/src/ha_mcp/transforms/__init__.py +0 -0
  124. {ha_mcp_dev-7.7.0.dev685 → ha_mcp_dev-7.7.0.dev687}/src/ha_mcp/transforms/categorized_search.py +0 -0
  125. {ha_mcp_dev-7.7.0.dev685 → ha_mcp_dev-7.7.0.dev687}/src/ha_mcp/transforms/lite_docstrings.py +0 -0
  126. {ha_mcp_dev-7.7.0.dev685 → ha_mcp_dev-7.7.0.dev687}/src/ha_mcp/utils/__init__.py +0 -0
  127. {ha_mcp_dev-7.7.0.dev685 → ha_mcp_dev-7.7.0.dev687}/src/ha_mcp/utils/config_hash.py +0 -0
  128. {ha_mcp_dev-7.7.0.dev685 → ha_mcp_dev-7.7.0.dev687}/src/ha_mcp/utils/data_paths.py +0 -0
  129. {ha_mcp_dev-7.7.0.dev685 → ha_mcp_dev-7.7.0.dev687}/src/ha_mcp/utils/domain_handlers.py +0 -0
  130. {ha_mcp_dev-7.7.0.dev685 → ha_mcp_dev-7.7.0.dev687}/src/ha_mcp/utils/fuzzy_search.py +0 -0
  131. {ha_mcp_dev-7.7.0.dev685 → ha_mcp_dev-7.7.0.dev687}/src/ha_mcp/utils/kill_signal_diagnostics.py +0 -0
  132. {ha_mcp_dev-7.7.0.dev685 → ha_mcp_dev-7.7.0.dev687}/src/ha_mcp/utils/operation_manager.py +0 -0
  133. {ha_mcp_dev-7.7.0.dev685 → ha_mcp_dev-7.7.0.dev687}/src/ha_mcp/utils/python_sandbox.py +0 -0
  134. {ha_mcp_dev-7.7.0.dev685 → ha_mcp_dev-7.7.0.dev687}/src/ha_mcp/utils/skill_loader.py +0 -0
  135. {ha_mcp_dev-7.7.0.dev685 → ha_mcp_dev-7.7.0.dev687}/src/ha_mcp/utils/usage_logger.py +0 -0
  136. {ha_mcp_dev-7.7.0.dev685 → ha_mcp_dev-7.7.0.dev687}/src/ha_mcp_dev.egg-info/dependency_links.txt +0 -0
  137. {ha_mcp_dev-7.7.0.dev685 → ha_mcp_dev-7.7.0.dev687}/src/ha_mcp_dev.egg-info/entry_points.txt +0 -0
  138. {ha_mcp_dev-7.7.0.dev685 → ha_mcp_dev-7.7.0.dev687}/src/ha_mcp_dev.egg-info/requires.txt +0 -0
  139. {ha_mcp_dev-7.7.0.dev685 → ha_mcp_dev-7.7.0.dev687}/src/ha_mcp_dev.egg-info/top_level.txt +0 -0
  140. {ha_mcp_dev-7.7.0.dev685 → ha_mcp_dev-7.7.0.dev687}/tests/__init__.py +0 -0
  141. {ha_mcp_dev-7.7.0.dev685 → ha_mcp_dev-7.7.0.dev687}/tests/test_constants.py +0 -0
  142. {ha_mcp_dev-7.7.0.dev685 → ha_mcp_dev-7.7.0.dev687}/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.7.0.dev685
3
+ Version: 7.7.0.dev687
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
@@ -38,7 +38,7 @@ Dynamic: license-file
38
38
  <!-- mcp-name: io.github.homeassistant-ai/ha-mcp -->
39
39
 
40
40
  <p align="center">
41
- <img src="https://img.shields.io/badge/tools-83-blue" alt="95+ Tools">
41
+ <img src="https://img.shields.io/badge/tools-84-blue" alt="95+ Tools">
42
42
  <a href="https://github.com/homeassistant-ai/ha-mcp/releases"><img src="https://img.shields.io/github/v/release/homeassistant-ai/ha-mcp" alt="Release"></a>
43
43
  <a href="https://github.com/homeassistant-ai/ha-mcp/actions/workflows/e2e-tests.yml"><img src="https://img.shields.io/github/actions/workflow/status/homeassistant-ai/ha-mcp/e2e-tests.yml?branch=master&label=E2E%20Tests" alt="E2E Tests"></a>
44
44
  <a href="LICENSE.md"><img src="https://img.shields.io/github/license/homeassistant-ai/ha-mcp.svg" alt="License"></a>
@@ -209,7 +209,7 @@ Spend less time configuring, more time enjoying your smart home.
209
209
  <details>
210
210
  <!-- TOOLS_TABLE_START -->
211
211
 
212
- <summary><b>Complete Tool List (83 tools)</b></summary>
212
+ <summary><b>Complete Tool List (84 tools)</b></summary>
213
213
 
214
214
  | Category | Tools |
215
215
  |----------|-------|
@@ -236,7 +236,7 @@ Spend less time configuring, more time enjoying your smart home.
236
236
  | **Scripts** | `ha_config_get_script`, `ha_config_remove_script`, `ha_config_set_script` |
237
237
  | **Search & Discovery** | `ha_get_overview`, `ha_get_state`, `ha_search` |
238
238
  | **Service & Device Control** | `ha_bulk_control`, `ha_call_event`, `ha_call_service`, `ha_get_operation_status`, `ha_list_services` |
239
- | **System** | `ha_config_set_yaml` *(beta)*, `ha_get_updates`, `ha_manage_backup`, `ha_manage_custom_tool` *(beta)*, `ha_reload_core`, `ha_restart` |
239
+ | **System** | `ha_config_set_yaml` *(beta)*, `ha_get_updates`, `ha_manage_backup`, `ha_manage_custom_tool` *(beta)*, `ha_manage_theme`, `ha_reload_core`, `ha_restart` |
240
240
  | **Todo Lists** | `ha_get_todo`, `ha_remove_todo_item`, `ha_set_todo_item` |
241
241
  | **Utilities** | `ha_eval_template`, `ha_install_mcp_tools` *(beta)*, `ha_report_issue` |
242
242
  | **Zones** | `ha_get_zone`, `ha_remove_zone`, `ha_set_zone` |
@@ -8,7 +8,7 @@
8
8
  <!-- mcp-name: io.github.homeassistant-ai/ha-mcp -->
9
9
 
10
10
  <p align="center">
11
- <img src="https://img.shields.io/badge/tools-83-blue" alt="95+ Tools">
11
+ <img src="https://img.shields.io/badge/tools-84-blue" alt="95+ Tools">
12
12
  <a href="https://github.com/homeassistant-ai/ha-mcp/releases"><img src="https://img.shields.io/github/v/release/homeassistant-ai/ha-mcp" alt="Release"></a>
13
13
  <a href="https://github.com/homeassistant-ai/ha-mcp/actions/workflows/e2e-tests.yml"><img src="https://img.shields.io/github/actions/workflow/status/homeassistant-ai/ha-mcp/e2e-tests.yml?branch=master&label=E2E%20Tests" alt="E2E Tests"></a>
14
14
  <a href="LICENSE.md"><img src="https://img.shields.io/github/license/homeassistant-ai/ha-mcp.svg" alt="License"></a>
@@ -179,7 +179,7 @@ Spend less time configuring, more time enjoying your smart home.
179
179
  <details>
180
180
  <!-- TOOLS_TABLE_START -->
181
181
 
182
- <summary><b>Complete Tool List (83 tools)</b></summary>
182
+ <summary><b>Complete Tool List (84 tools)</b></summary>
183
183
 
184
184
  | Category | Tools |
185
185
  |----------|-------|
@@ -206,7 +206,7 @@ Spend less time configuring, more time enjoying your smart home.
206
206
  | **Scripts** | `ha_config_get_script`, `ha_config_remove_script`, `ha_config_set_script` |
207
207
  | **Search & Discovery** | `ha_get_overview`, `ha_get_state`, `ha_search` |
208
208
  | **Service & Device Control** | `ha_bulk_control`, `ha_call_event`, `ha_call_service`, `ha_get_operation_status`, `ha_list_services` |
209
- | **System** | `ha_config_set_yaml` *(beta)*, `ha_get_updates`, `ha_manage_backup`, `ha_manage_custom_tool` *(beta)*, `ha_reload_core`, `ha_restart` |
209
+ | **System** | `ha_config_set_yaml` *(beta)*, `ha_get_updates`, `ha_manage_backup`, `ha_manage_custom_tool` *(beta)*, `ha_manage_theme`, `ha_reload_core`, `ha_restart` |
210
210
  | **Todo Lists** | `ha_get_todo`, `ha_remove_todo_item`, `ha_set_todo_item` |
211
211
  | **Utilities** | `ha_eval_template`, `ha_install_mcp_tools` *(beta)*, `ha_report_issue` |
212
212
  | **Zones** | `ha_get_zone`, `ha_remove_zone`, `ha_set_zone` |
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "ha-mcp-dev"
7
- version = "7.7.0.dev685"
7
+ version = "7.7.0.dev687"
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"
@@ -1,6 +1,25 @@
1
1
 
2
2
  :root {
3
+ /* Native form controls / scrollbars follow the scheme (MDN: color-scheme);
4
+ the light block below flips it. */
5
+ color-scheme: dark;
6
+ /* Accent used AS TEXT on tinted chips (a11y options): the base accent
7
+ (#0a84ff) doesn't meet contrast requirements, so text gets its own
8
+ readable shade. */
9
+ --accent-text: #66b3ff;
3
10
  --bg: #1c1c1e; --surface: #2c2c2e; --surface-hover: #3a3a3c;
11
+ /* Semantic tokens for surfaces that previously hard-coded per-scheme
12
+ hex pairs (review on #1574: tokenize so presets and custom colors
13
+ can retune them). Values unchanged from the old literals. */
14
+ --ok-bg: #0d3b1e;
15
+ --badge-readonly-bg: #1a2a3a; --badge-readonly-text: #6cb4ff;
16
+ --badge-destructive-bg: #3a1a1a; --badge-destructive-text: #ff6b6b;
17
+ --badge-mandatory-bg: #1a3a1a; --badge-mandatory-text: #6bff6b;
18
+ --danger-border: #7a1a1a; --danger-text: #ff9090; --danger-hover-bg: #2a0e0e;
19
+ --danger-soft-bg: #3a1a1a; --danger-soft-text: #ff6b6b;
20
+ --field-locked-bg: #2a2520; --field-locked-text: #f4b860;
21
+ --diff-add: #6bff6b; --diff-rem: #ff6b6b; --diff-hdr: #6cb4ff;
22
+ --code-bg: #111; --save-note-bg: rgba(255, 152, 0, 0.08);
4
23
  --text: #f5f5f7; --text-secondary: #98989d; --accent: #0a84ff;
5
24
  --accent-hover: #409cff; --danger: #ff453a; --success: #30d158;
6
25
  --warning: #ffd60a; --border: #38383a; --disabled-bg: #1a1a1c;
@@ -19,13 +38,15 @@
19
38
  .header h1 { font-size: 1.5rem; font-weight: 600; }
20
39
  .status { font-size: 0.85rem; padding: 4px 12px; border-radius: 12px;
21
40
  background: var(--surface); color: var(--text-secondary); }
22
- .status.saved { background: #0d3b1e; color: var(--success); }
41
+ .status.saved { background: var(--ok-bg); color: var(--success); }
23
42
  .search { width: 100%; padding: 10px 16px; border-radius: 10px; border: 1px solid var(--border);
24
43
  background: var(--surface); color: var(--text); font-size: 0.95rem; margin-bottom: 16px;
25
44
  outline: none; }
26
45
  .search:focus { border-color: var(--accent); }
27
- .readonly-notice { background: #1a2a3a; border: 1px solid #1a4a7a; border-radius: 10px;
28
- padding: 12px 16px; margin-bottom: 16px; font-size: 0.85rem; color: #6cb4ff; }
46
+ .readonly-notice { background: color-mix(in srgb, var(--accent) 12%, var(--surface));
47
+ border: 1px solid color-mix(in srgb, var(--accent) 40%, var(--surface));
48
+ border-left: 4px solid var(--accent); border-radius: 10px;
49
+ padding: 12px 16px; margin-bottom: 16px; font-size: 0.85rem; color: var(--text); }
29
50
  .group { background: var(--surface); border-radius: 12px; margin-bottom: 8px;
30
51
  overflow: hidden; border: 1px solid var(--border); }
31
52
  .group-header { display: flex; align-items: center; justify-content: space-between;
@@ -52,9 +73,9 @@
52
73
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
53
74
  .badge { display: inline-block; font-size: 0.7rem; padding: 1px 6px;
54
75
  border-radius: 4px; margin-left: 6px; font-weight: 500; }
55
- .badge.readonly { background: #1a2a3a; color: #6cb4ff; }
56
- .badge.destructive { background: #3a1a1a; color: #ff6b6b; }
57
- .badge.mandatory { background: #1a3a1a; color: #6bff6b; }
76
+ .badge.readonly { background: var(--badge-readonly-bg); color: var(--badge-readonly-text); }
77
+ .badge.destructive { background: var(--badge-destructive-bg); color: var(--badge-destructive-text); }
78
+ .badge.mandatory { background: var(--badge-mandatory-bg); color: var(--badge-mandatory-text); }
58
79
  .tool-toggles { display: flex; gap: 16px; align-items: center; }
59
80
  .toggle-group { display: flex; flex-direction: column; align-items: center; gap: 2px;
60
81
  font-size: 0.7rem; color: var(--text-secondary); }
@@ -73,11 +94,15 @@
73
94
  .summary { display: flex; gap: 16px; padding: 8px 0; margin-bottom: 16px;
74
95
  font-size: 0.85rem; color: var(--text-secondary); flex-wrap: wrap; }
75
96
  .summary span { background: var(--surface); padding: 4px 12px; border-radius: 8px; }
76
- .pin-notice { background: #3a2e1a; border: 1px solid #7a5a1a; border-radius: 10px;
77
- padding: 10px 16px; margin-bottom: 12px; font-size: 0.85rem; color: #ffd680; display: none; }
97
+ .pin-notice { background: color-mix(in srgb, var(--warning) 12%, var(--surface));
98
+ border: 1px solid color-mix(in srgb, var(--warning) 40%, var(--surface));
99
+ border-left: 4px solid var(--warning); border-radius: 10px;
100
+ padding: 10px 16px; margin-bottom: 12px; font-size: 0.85rem; color: var(--text); display: none; }
78
101
  .pin-notice.show { display: block; }
79
- .restart-notice { background: #3a1a1a; border: 1px solid #7a1a1a; border-radius: 10px;
80
- padding: 12px 16px; margin-bottom: 12px; font-size: 0.9rem; color: #ff9090;
102
+ .restart-notice { background: color-mix(in srgb, var(--danger) 12%, var(--surface));
103
+ border: 1px solid color-mix(in srgb, var(--danger) 40%, var(--surface));
104
+ border-left: 4px solid var(--danger); border-radius: 10px;
105
+ padding: 12px 16px; margin-bottom: 12px; font-size: 0.9rem; color: var(--text);
81
106
  font-weight: 500; display: none; align-items: center; justify-content: space-between; gap: 12px; }
82
107
  .restart-notice.show { display: flex; }
83
108
  .restart-notice-text { flex: 1; }
@@ -91,9 +116,9 @@
91
116
  obviously not a routine click. Matches the restart-notice red
92
117
  family so the danger semantic reads even without label text. */
93
118
  .danger-btn { padding: 7px 14px; border-radius: 8px;
94
- border: 1px solid #7a1a1a; background: transparent; color: #ff9090;
119
+ border: 1px solid var(--danger-border); background: transparent; color: var(--danger-text);
95
120
  font-weight: 600; cursor: pointer; font-size: 0.8rem; flex-shrink: 0; }
96
- .danger-btn:hover { background: #2a0e0e; }
121
+ .danger-btn:hover { background: var(--danger-hover-bg); }
97
122
  .danger-btn:disabled { opacity: 0.5; cursor: not-allowed; }
98
123
  /* Tabs — generic structure other tabs can stack onto without
99
124
  touching existing markup. New tabs add a button to .tabs and
@@ -113,7 +138,7 @@
113
138
  .backup-filters button { padding: 8px 12px; border-radius: 8px; border: none;
114
139
  background: var(--surface); color: var(--text); font-size: 0.85rem; cursor: pointer; }
115
140
  .backup-filters button:hover { background: var(--surface-hover); }
116
- .backup-filters button.danger { background: #3a1a1a; color: #ff6b6b; }
141
+ .backup-filters button.danger { background: var(--danger-soft-bg); color: var(--danger-soft-text); }
117
142
  .backup-state { background: var(--surface); border-radius: 10px; padding: 10px 16px; margin-bottom: 12px;
118
143
  font-size: 0.85rem; color: var(--text-secondary); display: flex; gap: 16px; flex-wrap: wrap; }
119
144
  .backup-state span { display: inline-block; }
@@ -139,7 +164,7 @@
139
164
  .backup-field-control input[type="number"] { width: 120px; padding: 6px 10px;
140
165
  border-radius: 6px; border: 1px solid var(--border); background: var(--bg); color: var(--text); }
141
166
  .backup-field-control input[type="number"]:disabled { opacity: 0.55; cursor: not-allowed; }
142
- .backup-field-locked { background: #2a2520; color: #f4b860; font-size: 0.78rem;
167
+ .backup-field-locked { background: var(--field-locked-bg); color: var(--field-locked-text); font-size: 0.78rem;
143
168
  padding: 2px 8px; border-radius: 999px; }
144
169
  .backup-field-help { font-size: 0.75rem; color: var(--text-secondary); flex-basis: 100%; margin-left: 200px; }
145
170
  .backup-config-actions { display: flex; align-items: center; gap: 12px; margin-top: 10px;
@@ -162,9 +187,9 @@
162
187
  .modal-body { flex: 1; overflow: auto; padding: 16px; }
163
188
  .modal-body pre { background: var(--surface); padding: 12px; border-radius: 8px;
164
189
  font-size: 0.8rem; overflow: auto; line-height: 1.4; }
165
- .diff-add { color: #6bff6b; }
166
- .diff-rem { color: #ff6b6b; }
167
- .diff-hdr { color: #6cb4ff; }
190
+ .diff-add { color: var(--diff-add); }
191
+ .diff-rem { color: var(--diff-rem); }
192
+ .diff-hdr { color: var(--diff-hdr); }
168
193
  /* Server-settings rows (#863). One row per FEATURE_FLAG_FIELDS
169
194
  entry. Locked rows (env / addon origin) get the dim treatment +
170
195
  a small inline note pointing at the env var to adjust. */
@@ -177,7 +202,7 @@
177
202
  .feature-name { font-size: 0.9rem; font-weight: 500; }
178
203
  .feature-help { font-size: 0.75rem; color: var(--text-secondary); margin-top: 2px;
179
204
  line-height: 1.4; }
180
- .feature-help code { background: #111; padding: 1px 5px; border-radius: 4px;
205
+ .feature-help code { background: var(--code-bg); padding: 1px 5px; border-radius: 4px;
181
206
  font-size: 0.72rem; }
182
207
  .feature-locked-note { font-size: 0.72rem; color: var(--warning); margin-top: 4px;
183
208
  font-style: italic; }
@@ -243,7 +268,7 @@
243
268
  because most surfaces have many of them; the Save row gets a
244
269
  dedicated, larger primary style so users don't miss the action.
245
270
  Duplicated at top and bottom so scrolling either way reaches it. */
246
- .adv-save-note { background: rgba(255, 152, 0, 0.08);
271
+ .adv-save-note { background: var(--save-note-bg);
247
272
  border-left: 3px solid var(--warning); padding: 10px 14px;
248
273
  border-radius: 6px; margin: 12px 0; color: var(--text);
249
274
  font-size: 0.85rem; line-height: 1.4; }
@@ -267,7 +292,7 @@
267
292
  .adv-name { font-size: 0.9rem; font-weight: 500; }
268
293
  .adv-help { font-size: 0.75rem; color: var(--text-secondary); margin-top: 2px;
269
294
  line-height: 1.4; }
270
- .adv-help code { background: #111; padding: 1px 5px; border-radius: 4px;
295
+ .adv-help code { background: var(--code-bg); padding: 1px 5px; border-radius: 4px;
271
296
  font-size: 0.72rem; }
272
297
  .adv-locked-note { font-size: 0.72rem; color: var(--warning); margin-top: 4px;
273
298
  font-style: italic; }
@@ -329,3 +354,120 @@
329
354
  color: var(--text); font-size: 0.85rem; margin: 0 6px; }
330
355
  .policy-save-status { margin-left: 10px; font-size: 0.8rem;
331
356
  color: var(--text-secondary); }
357
+ /* Light color-scheme override (#1572). Selector is :root[data-theme="light"]
358
+ — the inline head script in settings_ui.py sets data-theme to "light",
359
+ "dark", or auto-resolved from prefers-color-scheme before this CSS
360
+ evaluates, so the user can override the OS preference via the header
361
+ toggle. Inverts surface + text variables and provides explicit light
362
+ pairs for every hand-picked dark hex in the dark base palette (notices,
363
+ status badges, danger button, diff colors, inline code, slider track).
364
+ Each foreground/background pair is targeted at WCAG AA or better, with
365
+ AAA on body and primary surfaces — the contrast bar for low-vision
366
+ readers, not the typical-user default. */
367
+ :root[data-theme="light"] {
368
+ color-scheme: light;
369
+ --accent-text: #0051a3;
370
+ --bg: #f5f5f7; --surface: #ffffff; --surface-hover: #ebebed;
371
+ --text: #1d1d1f; --text-secondary: #525252; --accent: #0066cc;
372
+ --accent-hover: #0051a3; --danger: #cc0000; --success: #1a7f3e;
373
+ --warning: #a25c00; --border: #d2d2d7; --disabled-bg: #ebebed;
374
+ }
375
+ :root[data-theme="light"] {
376
+ --ok-bg: #d4edda;
377
+ --badge-readonly-bg: #d4e7fc; --badge-readonly-text: #0049a8;
378
+ --badge-destructive-bg: #fbe5e5; --badge-destructive-text: #a30000;
379
+ --badge-mandatory-bg: #d4edda; --badge-mandatory-text: #155724;
380
+ --danger-border: #c95151; --danger-text: #a30000; --danger-hover-bg: #fbe5e5;
381
+ --danger-soft-bg: #fbe5e5; --danger-soft-text: #a30000;
382
+ --field-locked-bg: #fff4d6; --field-locked-text: #6b4500;
383
+ --diff-add: #155724; --diff-rem: #a30000; --diff-hdr: #0049a8;
384
+ --code-bg: #f0f0f0; --save-note-bg: rgba(255, 152, 0, 0.18);
385
+ }
386
+ :root[data-theme="light"] .slider { background: #909090; }
387
+ /* Switch knob inherits var(--text) as background; in light mode that's
388
+ near-black, which disappears on the blue active track and clashes on
389
+ the gray idle track. Use --surface (white) so the knob stays light,
390
+ plus a subtle drop shadow for definition (WCAG 1.4.11 wants ≥3:1 for
391
+ adjacent UI components; the idle track sits just above that line
392
+ against a white knob, so the shadow is reinforcement, not the gate). */
393
+ :root[data-theme="light"] .slider::before { background: var(--surface);
394
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.25); }
395
+
396
+ /* Theme control — visible "Theme" label + small <select> in the header.
397
+ Picks up the surface palette so it fits each scheme automatically. */
398
+ .theme-control { display: inline-flex; align-items: center; gap: 6px; }
399
+ .theme-control-label { font-size: 0.8rem; color: var(--text-secondary); }
400
+ .theme-toggle { background: var(--surface); color: var(--text); border: 1px solid var(--border);
401
+ border-radius: 8px; padding: 4px 8px; font-size: 0.8rem; cursor: pointer;
402
+ font-family: inherit; outline: none; }
403
+ .theme-toggle:focus { border-color: var(--accent); }
404
+
405
+ /* Accessibility tab (#1572). Each section is one knob the user can flip;
406
+ options render as radio chips with a clear active state. Layout uses
407
+ rem-based gaps so font-size scaling pulls them along with the labels. */
408
+ .a11y-section { background: var(--surface); border: 1px solid var(--border); border-radius: 12px;
409
+ padding: 16px; margin-bottom: 12px; }
410
+ .a11y-section-title { font-size: 0.95rem; font-weight: 600; margin-bottom: 4px; }
411
+ .a11y-section-help { font-size: 0.8rem; color: var(--text-secondary); margin-bottom: 12px; }
412
+ .a11y-options { display: flex; flex-wrap: wrap; gap: 8px; }
413
+ /* The option groups are native fieldsets (group name in a hidden legend,
414
+ announced by screen readers); strip the default chrome. */
415
+ fieldset.a11y-options { border: 0; padding: 0; margin: 0; min-inline-size: auto; }
416
+ .visually-hidden { position: absolute; width: 1px; height: 1px; padding: 0;
417
+ margin: -1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; border: 0; }
418
+ .a11y-option { display: inline-flex; align-items: center; gap: 6px; padding: 6px 12px;
419
+ border: 1px solid var(--border); border-radius: 8px; cursor: pointer; font-size: 0.85rem;
420
+ background: var(--bg); user-select: none; }
421
+ .a11y-option:hover { background: var(--surface-hover); }
422
+ .a11y-option:has(input:checked) { border-color: var(--accent); color: var(--accent-text);
423
+ background: color-mix(in srgb, var(--accent) 8%, var(--bg)); }
424
+ .a11y-option input { accent-color: var(--accent); }
425
+
426
+ /* Preset chips (#1574 review): one-click theme presets modeled on
427
+ Firefox Reader View's Colors menu. Each chip shows a miniature
428
+ swatch of its actual scheme, so users see the choice instead of
429
+ having to read it — the swatch colors are fixed previews by design. */
430
+ .a11y-preset-chip { width: 22px; height: 22px; border-radius: 6px; display: inline-flex;
431
+ align-items: center; justify-content: center; font-size: 0.7rem; font-weight: 600;
432
+ border: 1px solid var(--border); flex-shrink: 0; }
433
+ .a11y-preset-chip[data-chip="dark"] { background: #1c1c1e; color: #f5f5f7; }
434
+ .a11y-preset-chip[data-chip="light"] { background: #f5f5f7; color: #1d1d1f; }
435
+ .a11y-preset-chip[data-chip="auto"] { background: linear-gradient(135deg, #1c1c1e 50%, #f5f5f7 50%); color: #8e8e93; }
436
+ .a11y-preset-chip[data-chip="paper"] { background: #f4ede0; color: #1d1d1f; }
437
+ .a11y-preset-chip[data-chip="gray"] { background: #d4d6da; color: #1d1d1f; }
438
+ .a11y-preset-chip[data-chip="contrast"] { background: #ffffff; color: #000000; font-weight: 800; border-color: #000000; }
439
+
440
+ /* Custom color swatches — native pickers so the OS provides the visual
441
+ selection UI; no hex typing required. */
442
+ .a11y-swatches { display: flex; flex-wrap: wrap; gap: 12px; }
443
+ .a11y-swatch { display: inline-flex; align-items: center; gap: 8px; font-size: 0.85rem; }
444
+ .a11y-swatch input[type="color"] { width: 40px; height: 28px; border: 1px solid var(--border);
445
+ border-radius: 8px; padding: 2px; background: var(--surface); cursor: pointer; }
446
+ .a11y-contrast-warning { color: var(--warning); font-size: 0.8rem; margin-top: 10px; }
447
+
448
+ /* Light-mode background shade picker (#1572). "off-white" is the baseline
449
+ light palette already defined above (--bg: #f5f5f7). "paper" warms the
450
+ background toward a low-blue-light cream; "pure" maximises brightness
451
+ for users who prefer it. Only the page background flips — surfaces
452
+ (cards, notices, badges) keep their own palette so contrast pairs the
453
+ light/dark CSS above hand-tuned stay intact. */
454
+ :root[data-theme="light"][data-shade="paper"] { --bg: #f4ede0; }
455
+ :root[data-theme="light"][data-shade="gray"] { --bg: #d4d6da; }
456
+ :root[data-theme="light"][data-shade="pure"] { --bg: #ffffff; }
457
+
458
+ /* High-contrast tier (#1572). Set via the Accessibility panel
459
+ (data-contrast="high"), targeting the WCAG 2.2 1.4.6 AAA threshold (7:1
460
+ for body text). The dark scheme pushes foregrounds toward pure white and
461
+ tightens borders; the light scheme pushes toward pure black. Body weight
462
+ bumps to 500 so the strokes carry the extra contrast (a known APCA lever
463
+ for low-vision readers). Scope: lifts the four primary text/UI
464
+ variables — notices and badges (.pin-notice, .readonly-notice,
465
+ .danger-btn, diff hues) keep their own hand-tuned palette so they read as
466
+ the same callout in both tiers. */
467
+ :root[data-contrast="high"] body { font-weight: 500; }
468
+ :root[data-contrast="high"][data-theme="dark"] {
469
+ --text: #ffffff; --text-secondary: #e0e0e0; --border: #6c6c70; --accent: #4ca8ff;
470
+ }
471
+ :root[data-contrast="high"][data-theme="light"] {
472
+ --text: #000000; --text-secondary: #1a1a1a; --border: #6c6c70; --accent: #003d99;
473
+ }
@@ -3013,3 +3013,279 @@ loadFsCustomPaths();
3013
3013
  }
3014
3014
  } catch (_) { /* best-effort */ }
3015
3015
  })();
3016
+
3017
+ // #1572 accessibility prefs — bidirectional binding between the header
3018
+ // theme toggle, the Accessibility tab controls, and the underlying
3019
+ // localStorage / <html> attributes. The anti-FOUC head script already set
3020
+ // the initial attributes; this module keeps them in sync for the rest of
3021
+ // the session and persists user changes. The block from `const PREFS` to
3022
+ // `const APPLY` must stay logically identical to the copy in
3023
+ // site/src/layouts/Layout.astro (comments and formatting may differ) —
3024
+ // enforced by tests/src/unit/test_anti_fouc_parity.py.
3025
+ (function bindAccessibilityPrefs() {
3026
+ const root = document.documentElement;
3027
+ const mql = window.matchMedia('(prefers-color-scheme: light)');
3028
+ const PREFS = {
3029
+ theme: { key: 'ha-mcp-theme', default: 'auto' },
3030
+ fontSize: { key: 'ha-mcp-font-size', default: '100' },
3031
+ contrast: { key: 'ha-mcp-contrast', default: 'normal' },
3032
+ shade: { key: 'ha-mcp-shade', default: 'off-white' },
3033
+ custom: { key: 'ha-mcp-custom-colors', default: '' },
3034
+ };
3035
+ // One-click presets (Firefox Reader View shape, #1574 review): each sets
3036
+ // the full (theme, shade, contrast) triple; the checked chip is derived
3037
+ // back from the stored triple, so hand-edited combos simply match none.
3038
+ const PRESETS = {
3039
+ dark: { theme: 'dark', shade: 'off-white', contrast: 'normal' },
3040
+ light: { theme: 'light', shade: 'off-white', contrast: 'normal' },
3041
+ auto: { theme: 'auto', shade: 'off-white', contrast: 'normal' },
3042
+ paper: { theme: 'light', shade: 'paper', contrast: 'normal' },
3043
+ gray: { theme: 'light', shade: 'gray', contrast: 'normal' },
3044
+ contrast: { theme: 'light', shade: 'pure', contrast: 'high' },
3045
+ };
3046
+ const read = (p) => {
3047
+ try { return localStorage.getItem(PREFS[p].key) || PREFS[p].default; }
3048
+ catch (_) { return PREFS[p].default; }
3049
+ };
3050
+ const write = (p, v) => {
3051
+ let stored = true;
3052
+ try { localStorage.setItem(PREFS[p].key, v); } catch (_) { stored = false; }
3053
+ // Surface-specific follow-up (server sync on the settings UI,
3054
+ // blocked-storage note on both) lives outside this mirrored core.
3055
+ if (window.__haMcpPrefsHook) window.__haMcpPrefsHook(p, v, stored);
3056
+ };
3057
+
3058
+ // Apply functions mirror what the anti-FOUC head script does, so the
3059
+ // runtime path and the pre-paint path stay observably identical.
3060
+ const applyTheme = (pref) => {
3061
+ const resolved = pref === 'auto' ? (mql.matches ? 'light' : 'dark') : pref;
3062
+ root.setAttribute('data-theme', resolved);
3063
+ };
3064
+ const applyFontSize = (pct) => {
3065
+ const n = parseInt(pct, 10);
3066
+ if (isNaN(n) || n <= 100 || n > 150) root.style.fontSize = '';
3067
+ else root.style.fontSize = (16 * n / 100) + 'px';
3068
+ };
3069
+ const applyContrast = (tier) => {
3070
+ if (tier === 'high') root.setAttribute('data-contrast', 'high');
3071
+ else root.removeAttribute('data-contrast');
3072
+ };
3073
+ const applyShade = (shade) => {
3074
+ if (shade && shade !== 'off-white') root.setAttribute('data-shade', shade);
3075
+ else root.removeAttribute('data-shade');
3076
+ };
3077
+ const HEX_RE = /^#[0-9a-fA-F]{6}$/;
3078
+ const channels = (hex) =>
3079
+ parseInt(hex.slice(1, 3), 16) + ' ' + parseInt(hex.slice(3, 5), 16) + ' ' + parseInt(hex.slice(5, 7), 16);
3080
+ // Custom colors layer on top of any preset as inline styles (inline beats
3081
+ // stylesheet rules, so the cascade is: preset CSS -> user custom). Both
3082
+ // surfaces' variable names are written; names a surface does not use are
3083
+ // inert. Returns the parsed object so callers can reuse it.
3084
+ const CUSTOM_VARS = {
3085
+ bg: { hex: ['--bg'], chan: ['--surface-0'] },
3086
+ text: { hex: ['--text', '--text-primary'], chan: [] },
3087
+ accent: { hex: ['--accent', '--accent-text'], chan: ['--brand'] },
3088
+ };
3089
+ const applyCustom = (raw) => {
3090
+ for (const part in CUSTOM_VARS) {
3091
+ CUSTOM_VARS[part].hex.concat(CUSTOM_VARS[part].chan).forEach((name) => root.style.removeProperty(name));
3092
+ }
3093
+ let custom = {};
3094
+ try { custom = JSON.parse(raw || '{}') || {}; } catch (_) { return {}; }
3095
+ for (const part in CUSTOM_VARS) {
3096
+ const v = custom[part];
3097
+ if (typeof v !== 'string' || !HEX_RE.test(v)) continue;
3098
+ CUSTOM_VARS[part].hex.forEach((name) => root.style.setProperty(name, v));
3099
+ CUSTOM_VARS[part].chan.forEach((name) => root.style.setProperty(name, channels(v)));
3100
+ }
3101
+ return custom;
3102
+ };
3103
+ const APPLY = { theme: applyTheme, fontSize: applyFontSize, contrast: applyContrast, shade: applyShade, custom: applyCustom };
3104
+
3105
+ // #1574 review: localStorage is the synchronous store the anti-FOUC
3106
+ // script reads at paint time, but it is origin-scoped and the stdio
3107
+ // sidecar binds a fresh random port (= fresh empty origin) per session.
3108
+ // This hook therefore (a) mirrors every change to the server
3109
+ // (./api/settings/theme -> theme_prefs.json), which seeds the next
3110
+ // fresh origin via the server-prefs head script, and (b) surfaces a
3111
+ // blocked localStorage (private mode) once instead of silently losing
3112
+ // the choices on reload. Debounced so color-picker drags don't flood
3113
+ // the endpoint; best-effort because the browser copy already applied.
3114
+ const storageNote = document.getElementById('a11y-storage-note');
3115
+ const pendingPrefs = {};
3116
+ let prefsSyncTimer = null;
3117
+ window.__haMcpPrefsHook = (pref, value, stored) => {
3118
+ if (!stored && storageNote) storageNote.hidden = false;
3119
+ pendingPrefs[pref] = value;
3120
+ clearTimeout(prefsSyncTimer);
3121
+ prefsSyncTimer = setTimeout(() => {
3122
+ const body = JSON.stringify(pendingPrefs);
3123
+ for (const k in pendingPrefs) delete pendingPrefs[k];
3124
+ fetch('./api/settings/theme', {
3125
+ method: 'POST',
3126
+ headers: { 'Content-Type': 'application/json' },
3127
+ body,
3128
+ }).then((resp) => {
3129
+ // A non-2xx means client and server disagree about valid values —
3130
+ // an implementation bug worth a console trail, not a user error.
3131
+ if (!resp.ok) console.warn('ha-mcp: theme prefs not persisted:', resp.status);
3132
+ }).catch(() => { /* offline / sidecar gone — localStorage still has it */ });
3133
+ }, 400);
3134
+ };
3135
+
3136
+ const setPref = (pref, value) => {
3137
+ write(pref, value);
3138
+ APPLY[pref](value);
3139
+ };
3140
+ const applyPreset = (name) => {
3141
+ const p = PRESETS[name];
3142
+ if (!p) return;
3143
+ setPref('theme', p.theme);
3144
+ setPref('shade', p.shade);
3145
+ setPref('contrast', p.contrast);
3146
+ };
3147
+ const reflectPreset = () => {
3148
+ const theme = read('theme'), shade = read('shade'), contrast = read('contrast');
3149
+ let active = '';
3150
+ for (const name in PRESETS) {
3151
+ const p = PRESETS[name];
3152
+ if (p.theme === theme && p.shade === shade && p.contrast === contrast) { active = name; break; }
3153
+ }
3154
+ document.querySelectorAll('input[type="radio"][name="a11y-preset"]').forEach((r) => {
3155
+ r.checked = (r.value === active);
3156
+ });
3157
+ };
3158
+ const reflectRadio = (name, value) => {
3159
+ document.querySelectorAll('input[type="radio"][name="' + name + '"]').forEach((r) => {
3160
+ r.checked = (r.value === value);
3161
+ });
3162
+ };
3163
+ // WCAG 2.x relative-luminance contrast for the custom-color warning.
3164
+ const luminance = (hex) => {
3165
+ const f = (i) => {
3166
+ const c = parseInt(hex.slice(i, i + 2), 16) / 255;
3167
+ return c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
3168
+ };
3169
+ return 0.2126 * f(1) + 0.7152 * f(3) + 0.0722 * f(5);
3170
+ };
3171
+ const updateContrastWarning = (custom) => {
3172
+ const warn = document.getElementById('a11y-contrast-warning');
3173
+ if (!warn) return;
3174
+ const comparable = HEX_RE.test(custom.bg || '') && HEX_RE.test(custom.text || '');
3175
+ if (!comparable) { warn.hidden = true; return; }
3176
+ const l1 = luminance(custom.bg), l2 = luminance(custom.text);
3177
+ warn.hidden = (Math.max(l1, l2) + 0.05) / (Math.min(l1, l2) + 0.05) >= 4.5;
3178
+ };
3179
+
3180
+ // Header <select> for theme — quick Dark/Light/Auto access.
3181
+ const headerToggle = document.getElementById('themeToggle');
3182
+ if (headerToggle) {
3183
+ headerToggle.value = read('theme');
3184
+ headerToggle.addEventListener('change', () => {
3185
+ setPref('theme', headerToggle.value);
3186
+ reflectPreset();
3187
+ refreshSwatches();
3188
+ });
3189
+ }
3190
+
3191
+ // Re-apply when the OS preference flips while in Auto.
3192
+ mql.addEventListener('change', () => {
3193
+ if (read('theme') === 'auto') {
3194
+ applyTheme('auto');
3195
+ refreshSwatches();
3196
+ }
3197
+ });
3198
+
3199
+ reflectRadio('a11y-font-size', read('fontSize'));
3200
+ reflectPreset();
3201
+ // Initialize each swatch from the saved custom value, falling back to
3202
+ // the theme's current computed color so the wells show reality instead
3203
+ // of an arbitrary black default.
3204
+ const cssColorToHex = (value) => {
3205
+ const v = (value || '').trim();
3206
+ if (HEX_RE.test(v)) return v;
3207
+ let m = v.match(/^rgba?\((\d+)[, ]+(\d+)[, ]+(\d+)/);
3208
+ if (!m) m = v.match(/^(\d+) (\d+) (\d+)$/);
3209
+ if (!m) return '';
3210
+ return '#' + [m[1], m[2], m[3]].map((n) => (+n).toString(16).padStart(2, '0')).join('');
3211
+ };
3212
+ const currentColorFor = (part) => {
3213
+ const body = getComputedStyle(document.body);
3214
+ const rootStyle = getComputedStyle(root);
3215
+ if (part === 'bg') return cssColorToHex(body.backgroundColor);
3216
+ if (part === 'text') return cssColorToHex(body.color);
3217
+ return cssColorToHex(rootStyle.getPropertyValue('--accent')) ||
3218
+ cssColorToHex(rootStyle.getPropertyValue('--brand'));
3219
+ };
3220
+ const customInputs = document.querySelectorAll('input[type="color"][data-custom]');
3221
+ // Sync the swatch wells with reality: a stored custom value wins,
3222
+ // otherwise the active theme's computed color. Re-run after anything
3223
+ // that changes the active palette (preset click, header toggle, OS
3224
+ // auto-flip, clear, reset) so the wells never keep showing a previous
3225
+ // theme's colors.
3226
+ const refreshSwatches = () => {
3227
+ let custom = {};
3228
+ try { custom = JSON.parse(read('custom') || '{}') || {}; } catch (_) { /* ignore */ }
3229
+ customInputs.forEach((inp) => {
3230
+ const v = custom[inp.dataset.custom];
3231
+ const fallback = currentColorFor(inp.dataset.custom);
3232
+ if (HEX_RE.test(v || '')) inp.value = v;
3233
+ else if (fallback) inp.value = fallback;
3234
+ });
3235
+ };
3236
+ updateContrastWarning(applyCustom(read('custom')));
3237
+ refreshSwatches();
3238
+ customInputs.forEach((inp) => {
3239
+ inp.addEventListener('input', () => {
3240
+ let custom = {};
3241
+ try { custom = JSON.parse(read('custom') || '{}') || {}; } catch (_) { /* start fresh */ }
3242
+ custom[inp.dataset.custom] = inp.value;
3243
+ write('custom', JSON.stringify(custom));
3244
+ applyCustom(read('custom'));
3245
+ updateContrastWarning(custom);
3246
+ });
3247
+ });
3248
+ const clearBtn = document.getElementById('a11y-custom-clear');
3249
+ if (clearBtn) {
3250
+ clearBtn.addEventListener('click', () => {
3251
+ write('custom', '');
3252
+ updateContrastWarning(applyCustom(''));
3253
+ refreshSwatches();
3254
+ });
3255
+ }
3256
+
3257
+ // Scope the change listener to the Accessibility panel rather than the
3258
+ // document: the settings page carries dozens of unrelated inputs (tool
3259
+ // toggles, server fields, backup forms) and a document-level listener
3260
+ // would fire on every one of them just to bail the filter.
3261
+ const a11yPanel = document.getElementById('panel-accessibility');
3262
+ if (a11yPanel) {
3263
+ a11yPanel.addEventListener('change', (e) => {
3264
+ const t = e.target;
3265
+ if (!(t instanceof HTMLInputElement) || t.type !== 'radio') return;
3266
+ if (t.name === 'a11y-preset') {
3267
+ applyPreset(t.value);
3268
+ if (headerToggle) headerToggle.value = read('theme');
3269
+ refreshSwatches();
3270
+ } else if (t.name === 'a11y-font-size') {
3271
+ setPref('fontSize', t.value);
3272
+ }
3273
+ });
3274
+ }
3275
+
3276
+ const resetBtn = document.getElementById('a11y-reset');
3277
+ if (resetBtn) {
3278
+ resetBtn.addEventListener('click', () => {
3279
+ setPref('theme', PREFS.theme.default);
3280
+ setPref('shade', PREFS.shade.default);
3281
+ setPref('contrast', PREFS.contrast.default);
3282
+ setPref('fontSize', PREFS.fontSize.default);
3283
+ write('custom', '');
3284
+ updateContrastWarning(applyCustom(''));
3285
+ reflectRadio('a11y-font-size', PREFS.fontSize.default);
3286
+ reflectPreset();
3287
+ if (headerToggle) headerToggle.value = PREFS.theme.default;
3288
+ refreshSwatches();
3289
+ });
3290
+ }
3291
+ })();