ha-mcp-dev 7.4.1.dev439__tar.gz → 7.4.1.dev441__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 (107) hide show
  1. {ha_mcp_dev-7.4.1.dev439/src/ha_mcp_dev.egg-info → ha_mcp_dev-7.4.1.dev441}/PKG-INFO +1 -1
  2. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/pyproject.toml +1 -1
  3. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/client/rest_client.py +217 -24
  4. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/tools/tools_entities.py +277 -74
  5. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/tools/tools_utility.py +228 -33
  6. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441/src/ha_mcp_dev.egg-info}/PKG-INFO +1 -1
  7. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/LICENSE +0 -0
  8. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/MANIFEST.in +0 -0
  9. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/README.md +0 -0
  10. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/setup.cfg +0 -0
  11. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/__init__.py +0 -0
  12. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/__main__.py +0 -0
  13. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/_pypi_marker +0 -0
  14. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/_version.py +0 -0
  15. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/auth/__init__.py +0 -0
  16. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/auth/consent_form.py +0 -0
  17. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/auth/provider.py +0 -0
  18. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/client/__init__.py +0 -0
  19. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/client/websocket_client.py +0 -0
  20. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/client/websocket_listener.py +0 -0
  21. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/config.py +0 -0
  22. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/errors.py +0 -0
  23. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/py.typed +0 -0
  24. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/resources/skills-vendor/.claude/settings.json +0 -0
  25. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/resources/skills-vendor/.claude-plugin/marketplace.json +0 -0
  26. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/resources/skills-vendor/.claude-plugin/plugin.json +0 -0
  27. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/resources/skills-vendor/.github/ISSUE_TEMPLATE/skill-rca.md +0 -0
  28. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/resources/skills-vendor/AGENTS.md +0 -0
  29. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/resources/skills-vendor/CLAUDE.md +0 -0
  30. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/resources/skills-vendor/CONTRIBUTING.md +0 -0
  31. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/resources/skills-vendor/LICENSE +0 -0
  32. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/resources/skills-vendor/README.md +0 -0
  33. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/SKILL.md +0 -0
  34. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/evals/evals.json +0 -0
  35. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/automation-patterns.md +0 -0
  36. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-cards.md +0 -0
  37. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/dashboard-guide.md +0 -0
  38. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/device-control.md +0 -0
  39. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/domain-docs.md +0 -0
  40. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/examples.yaml +0 -0
  41. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/helper-selection.md +0 -0
  42. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/safe-refactoring.md +0 -0
  43. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/resources/skills-vendor/skills/home-assistant-best-practices/references/template-guidelines.md +0 -0
  44. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/server.py +0 -0
  45. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/settings_ui.py +0 -0
  46. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/smoke_test.py +0 -0
  47. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/tools/__init__.py +0 -0
  48. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/tools/backup.py +0 -0
  49. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/tools/best_practice_checker.py +0 -0
  50. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/tools/device_control.py +0 -0
  51. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/tools/enhanced.py +0 -0
  52. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/tools/helpers.py +0 -0
  53. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/tools/reference_validator.py +0 -0
  54. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/tools/registry.py +0 -0
  55. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/tools/smart_search.py +0 -0
  56. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/tools/tools_addons.py +0 -0
  57. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/tools/tools_areas.py +0 -0
  58. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/tools/tools_blueprints.py +0 -0
  59. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/tools/tools_bug_report.py +0 -0
  60. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/tools/tools_calendar.py +0 -0
  61. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/tools/tools_camera.py +0 -0
  62. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/tools/tools_categories.py +0 -0
  63. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/tools/tools_config_automations.py +0 -0
  64. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/tools/tools_config_dashboards.py +0 -0
  65. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/tools/tools_config_entry_flow.py +0 -0
  66. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/tools/tools_config_helpers.py +0 -0
  67. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/tools/tools_config_scripts.py +0 -0
  68. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/tools/tools_energy.py +0 -0
  69. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/tools/tools_filesystem.py +0 -0
  70. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/tools/tools_groups.py +0 -0
  71. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/tools/tools_hacs.py +0 -0
  72. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/tools/tools_history.py +0 -0
  73. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/tools/tools_integrations.py +0 -0
  74. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/tools/tools_labels.py +0 -0
  75. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/tools/tools_mcp_component.py +0 -0
  76. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/tools/tools_registry.py +0 -0
  77. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/tools/tools_resources.py +0 -0
  78. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/tools/tools_search.py +0 -0
  79. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/tools/tools_service.py +0 -0
  80. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/tools/tools_services.py +0 -0
  81. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/tools/tools_system.py +0 -0
  82. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/tools/tools_todo.py +0 -0
  83. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/tools/tools_traces.py +0 -0
  84. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/tools/tools_updates.py +0 -0
  85. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/tools/tools_voice_assistant.py +0 -0
  86. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/tools/tools_yaml_config.py +0 -0
  87. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/tools/tools_zones.py +0 -0
  88. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/tools/util_helpers.py +0 -0
  89. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/transforms/__init__.py +0 -0
  90. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/transforms/categorized_search.py +0 -0
  91. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/utils/__init__.py +0 -0
  92. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/utils/config_hash.py +0 -0
  93. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/utils/data_paths.py +0 -0
  94. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/utils/domain_handlers.py +0 -0
  95. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/utils/fuzzy_search.py +0 -0
  96. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/utils/kill_signal_diagnostics.py +0 -0
  97. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/utils/operation_manager.py +0 -0
  98. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/utils/python_sandbox.py +0 -0
  99. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp/utils/usage_logger.py +0 -0
  100. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp_dev.egg-info/SOURCES.txt +0 -0
  101. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp_dev.egg-info/dependency_links.txt +0 -0
  102. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp_dev.egg-info/entry_points.txt +0 -0
  103. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp_dev.egg-info/requires.txt +0 -0
  104. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/src/ha_mcp_dev.egg-info/top_level.txt +0 -0
  105. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/tests/__init__.py +0 -0
  106. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/tests/test_constants.py +0 -0
  107. {ha_mcp_dev-7.4.1.dev439 → ha_mcp_dev-7.4.1.dev441}/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.4.1.dev439
3
+ Version: 7.4.1.dev441
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.4.1.dev439"
7
+ version = "7.4.1.dev441"
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"
@@ -5,11 +5,13 @@ Home Assistant HTTP client with authentication and error handling.
5
5
  import asyncio
6
6
  import json
7
7
  import logging
8
+ import os
8
9
  import ssl
9
10
  from typing import Any
10
11
 
11
12
  import httpx
12
13
 
14
+ from .._version import is_running_in_addon
13
15
  from ..config import get_global_settings
14
16
 
15
17
 
@@ -26,6 +28,7 @@ def _is_ssl_error(exc: BaseException) -> bool:
26
28
  cur = cur.__cause__ or cur.__context__
27
29
  return False
28
30
 
31
+
29
32
  logger = logging.getLogger(__name__)
30
33
 
31
34
 
@@ -33,17 +36,14 @@ class HomeAssistantError(Exception):
33
36
  """Base exception for Home Assistant API errors."""
34
37
 
35
38
 
36
-
37
39
  class HomeAssistantConnectionError(HomeAssistantError):
38
40
  """Connection error to Home Assistant."""
39
41
 
40
42
 
41
-
42
43
  class HomeAssistantAuthError(HomeAssistantError):
43
44
  """Authentication error with Home Assistant."""
44
45
 
45
46
 
46
-
47
47
  class HomeAssistantAPIError(HomeAssistantError):
48
48
  """API error from Home Assistant."""
49
49
 
@@ -125,7 +125,7 @@ class HomeAssistantClient:
125
125
 
126
126
  logger.info(f"Initialized Home Assistant client for {self.base_url}")
127
127
 
128
- async def __aenter__(self) -> 'HomeAssistantClient':
128
+ async def __aenter__(self) -> "HomeAssistantClient":
129
129
  """Async context manager entry."""
130
130
  return self
131
131
 
@@ -192,7 +192,9 @@ class HomeAssistantClient:
192
192
  except httpx.HTTPError as e:
193
193
  raise HomeAssistantConnectionError(f"HTTP error: {e}") from e
194
194
 
195
- async def _request(self, method: str, endpoint: str, **kwargs: Any) -> dict[str, Any]:
195
+ async def _request(
196
+ self, method: str, endpoint: str, **kwargs: Any
197
+ ) -> dict[str, Any]:
196
198
  """
197
199
  Make authenticated request to Home Assistant API and parse JSON body.
198
200
 
@@ -267,8 +269,11 @@ class HomeAssistantClient:
267
269
  return await self._request("POST", f"/states/{entity_id}", json=payload)
268
270
 
269
271
  async def call_service(
270
- self, domain: str, service: str, data: dict[str, Any] | None = None,
271
- return_response: bool = False
272
+ self,
273
+ domain: str,
274
+ service: str,
275
+ data: dict[str, Any] | None = None,
276
+ return_response: bool = False,
272
277
  ) -> list[dict[str, Any]] | dict[str, Any]:
273
278
  """
274
279
  Call Home Assistant service.
@@ -284,7 +289,9 @@ class HomeAssistantClient:
284
289
  Service response data - list of affected states normally, or dict with
285
290
  service response if return_response=True
286
291
  """
287
- logger.debug(f"Calling service {domain}.{service} (return_response={return_response})")
292
+ logger.debug(
293
+ f"Calling service {domain}.{service} (return_response={return_response})"
294
+ )
288
295
 
289
296
  payload = data or {}
290
297
 
@@ -294,7 +301,10 @@ class HomeAssistantClient:
294
301
  params["return_response"] = "true"
295
302
 
296
303
  result = await self._request(
297
- "POST", f"/services/{domain}/{service}", json=payload, params=params if params else None
304
+ "POST",
305
+ f"/services/{domain}/{service}",
306
+ json=payload,
307
+ params=params if params else None,
298
308
  )
299
309
 
300
310
  # When return_response is True, HA returns a dict with service_response key
@@ -366,7 +376,9 @@ class HomeAssistantClient:
366
376
  Returns:
367
377
  Logbook entries
368
378
  """
369
- logger.debug(f"Fetching logbook entries for entity: {entity_id}, start: {start_time}, end: {end_time}")
379
+ logger.debug(
380
+ f"Fetching logbook entries for entity: {entity_id}, start: {start_time}, end: {end_time}"
381
+ )
370
382
 
371
383
  # Build endpoint - start_time goes in URL path if provided
372
384
  if start_time:
@@ -428,27 +440,66 @@ class HomeAssistantClient:
428
440
  return await self._request("POST", "/config/core/check_config")
429
441
 
430
442
  async def get_error_log(self) -> str:
431
- """Get Home Assistant error log."""
432
- logger.debug("Fetching error log")
443
+ """Get Home Assistant error log.
444
+
445
+ Branch on ``is_running_in_addon()``: inside the add-on container,
446
+ HA Core's ``bootstrap.py`` sets ``err_log_path = None`` when the
447
+ ``SUPERVISOR`` env var is present, so ``hass.data[DATA_LOGGING]``
448
+ is never populated and the ``APIErrorLog`` view is not registered
449
+ — ``/api/error_log`` returns 404 by-design on HA OS / Supervised.
450
+ Route to ``_supervisor_logs_get("core")`` on this branch: same
451
+ content (HA Core's container log) via a different transport
452
+ (Supervisor REST). On non-addon installs keep the
453
+ ``/api/error_log`` proxy path.
454
+
455
+ Same root cause and fix shape as ``get_addon_logs`` — see #1116.
456
+
457
+ Raises:
458
+ HomeAssistantAuthError: 401, or empty ``SUPERVISOR_TOKEN`` on
459
+ the addon branch.
460
+ HomeAssistantAPIError: 403 (role too low — addon needs
461
+ ``hassio_role: manager``), 404, other non-2xx.
462
+ HomeAssistantConnectionError: Network, timeout, or transport
463
+ error.
464
+ """
465
+ if is_running_in_addon():
466
+ logger.debug("Fetching error log via Supervisor direct (core service)")
467
+ return await self._supervisor_logs_get("core")
468
+
469
+ logger.debug("Fetching error log via HA Core proxy")
433
470
  response = await self._request("GET", "/error_log")
434
471
  return response if isinstance(response, str) else str(response)
435
472
 
436
473
  async def get_addon_logs(self, slug: str) -> str:
437
- """Fetch an add-on's container logs via HA Core's Supervisor REST proxy.
474
+ """Fetch an add-on's container logs.
475
+
476
+ Branch on ``is_running_in_addon()`` (which keys off ``SUPERVISOR_TOKEN``
477
+ in env): inside the add-on container goes directly to the Supervisor
478
+ REST API at ``http://supervisor/addons/{slug}/logs`` with the
479
+ Supervisor token. The HA Core proxy at
480
+ ``/api/hassio/addons/{slug}/logs`` rejects this token+path combination
481
+ on current HA Core releases (see #1116) — the direct path bypasses
482
+ HA Core entirely and is the documented Supervisor contract.
438
483
 
439
- Uses `/api/hassio/addons/{slug}/logs`, which HA Core proxies to
440
- Supervisor and returns as `text/plain`. This avoids the
441
- `supervisor/api` websocket path that tries to JSON-decode the text
442
- body and always fails (see #950).
484
+ On non-addon installs (Docker, pyinstaller, pip pointing at a normal
485
+ HA URL), falls back to the HA Core proxy path. That path requires an
486
+ admin LLA but works fine when not invoked from the add-on container.
487
+
488
+ Both branches return ``text/plain`` log content.
443
489
 
444
490
  Raises:
445
- HomeAssistantAuthError: 401 from HA Core.
446
- HomeAssistantAPIError: Non-2xx response (e.g. 404 unknown slug,
447
- 400 addon not installed). `status_code` is set so callers
448
- can map to specific suggestions.
491
+ HomeAssistantAuthError: 401 response, or ``SUPERVISOR_TOKEN`` empty
492
+ at call time on the addon branch.
493
+ HomeAssistantAPIError: 403 (role too low addon needs hassio_role
494
+ ``manager``), 404 (unknown slug), or other non-2xx. The
495
+ ``status_code`` attribute lets callers map to specific
496
+ suggestions.
449
497
  HomeAssistantConnectionError: Network, timeout, or transport error.
450
498
  """
451
- logger.debug(f"Fetching addon logs for slug={slug}")
499
+ if is_running_in_addon():
500
+ return await self._get_addon_logs_via_supervisor(slug)
501
+
502
+ logger.debug(f"Fetching addon logs for slug={slug} via HA Core proxy")
452
503
  response = await self._raw_request(
453
504
  "GET",
454
505
  f"/hassio/addons/{slug}/logs",
@@ -456,6 +507,146 @@ class HomeAssistantClient:
456
507
  )
457
508
  return response.text
458
509
 
510
+ async def _supervisor_logs_get(self, path: str) -> str:
511
+ """Fetch ``text/plain`` logs from a Supervisor REST endpoint.
512
+
513
+ ``path`` is everything between ``http://supervisor/`` and ``/logs``:
514
+
515
+ - ``"addons/<slug>"`` for add-on container logs
516
+ - ``"<service>"`` (where service ∈ {supervisor, host, core, dns, audio,
517
+ multicast, observer}) for system-service logs
518
+
519
+ Bypasses ``HomeAssistantClient.httpx_client`` because the Supervisor
520
+ endpoint uses a different base URL (``http://supervisor``) and a
521
+ different token (``SUPERVISOR_TOKEN``) than HA Core REST. Both
522
+ endpoints require the addon's ``hassio_role`` to be ``manager`` (not
523
+ ``default``); a ``default`` role gets a 403 here — see #1116.
524
+
525
+ Raises:
526
+ HomeAssistantAuthError: ``SUPERVISOR_TOKEN`` absent at call time,
527
+ or 401 from Supervisor.
528
+ HomeAssistantAPIError: 403 (role too low — distinct branch with
529
+ role hint), 404, other 4xx/5xx. Tries to parse Supervisor's
530
+ ``{"result":"error","message":"..."}`` JSON envelope before
531
+ falling back to text body / reason phrase / placeholder.
532
+ HomeAssistantConnectionError: Timeout or transport error, with
533
+ distinct messages so callers can tell them apart.
534
+ """
535
+ token = os.environ.get("SUPERVISOR_TOKEN", "")
536
+ if not token:
537
+ # The is_running_in_addon() gate already keys off SUPERVISOR_TOKEN
538
+ # being truthy, so a direct caller landing here without one is a
539
+ # detection/config mismatch — fail-fast with a distinct message
540
+ # so operators don't read it as "token rejected".
541
+ raise HomeAssistantAuthError(
542
+ f"Supervisor token absent at call time for /{path}/logs "
543
+ "(addon-mode gate fired but SUPERVISOR_TOKEN env var not set)"
544
+ )
545
+
546
+ url = f"http://supervisor/{path}/logs"
547
+ logger.debug("Fetching %s via Supervisor direct", url)
548
+
549
+ try:
550
+ async with httpx.AsyncClient(
551
+ timeout=httpx.Timeout(self.timeout),
552
+ # `verify` is a no-op for plain http://supervisor, but kept
553
+ # for symmetry with the other two direct-Supervisor httpx
554
+ # clients (#1128 establishes the 3-site convention).
555
+ verify=self.verify_ssl,
556
+ ) as client:
557
+ response = await client.get(
558
+ url,
559
+ headers={
560
+ "Authorization": f"Bearer {token}",
561
+ "Accept": "text/plain",
562
+ },
563
+ )
564
+ except httpx.TimeoutException as e:
565
+ raise HomeAssistantConnectionError(
566
+ f"Timeout fetching /{path}/logs from Supervisor: {e}"
567
+ ) from e
568
+ except httpx.HTTPError as e:
569
+ raise HomeAssistantConnectionError(
570
+ f"Transport error fetching /{path}/logs from Supervisor: {e}"
571
+ ) from e
572
+
573
+ if response.status_code == 401:
574
+ raise HomeAssistantAuthError(f"Invalid Supervisor token for /{path}/logs")
575
+ if response.status_code == 403:
576
+ # Distinct from 401: token is valid but addon's hassio_role isn't
577
+ # high enough. Most-likely cause for this exact endpoint at the
578
+ # time #1116 surfaced (default → manager bump in addon config.yaml
579
+ # is the same-PR companion fix).
580
+ logger.warning(
581
+ "Supervisor returned 403 for /%s/logs — addon hassio_role may "
582
+ "be too low (need 'manager')",
583
+ path,
584
+ )
585
+ raise HomeAssistantAPIError(
586
+ f"Supervisor forbids /{path}/logs (403) — addon's hassio_role "
587
+ "may be 'default'; need 'manager' or higher",
588
+ status_code=403,
589
+ response_data={"path": path},
590
+ )
591
+ if response.status_code >= 400:
592
+ text_body = response.text
593
+ # Supervisor returns {"result":"error","message":"..."} JSON on
594
+ # some 4xx paths. Try parsing that first so the user sees the
595
+ # human message instead of a JSON blob; then fall back to the
596
+ # text body, then reason_phrase, then a placeholder.
597
+ message = ""
598
+ try:
599
+ envelope = json.loads(text_body) if text_body else None
600
+ if isinstance(envelope, dict):
601
+ msg = envelope.get("message")
602
+ if isinstance(msg, str) and msg:
603
+ message = msg
604
+ except json.JSONDecodeError:
605
+ pass
606
+ if not message:
607
+ message = text_body.strip() or response.reason_phrase or "<empty body>"
608
+ logger.warning(
609
+ "Supervisor returned %s for /%s/logs: %s",
610
+ response.status_code,
611
+ path,
612
+ message,
613
+ )
614
+ raise HomeAssistantAPIError(
615
+ f"API error: {response.status_code} - {message}",
616
+ status_code=response.status_code,
617
+ response_data={"message": text_body, "path": path},
618
+ )
619
+ return response.text
620
+
621
+ async def _get_addon_logs_via_supervisor(self, slug: str) -> str:
622
+ """Fetch add-on container logs directly from Supervisor's REST API.
623
+
624
+ Distinct from ``tools_bug_report._fetch_addon_logs``: that helper is
625
+ hardcoded to ``/addons/self/logs`` and silently swallows failures
626
+ (it's an aux-data fetch for bug reports, fine to skip on error). This
627
+ helper takes arbitrary slugs and surfaces failures as exceptions
628
+ because callers (``ha_get_logs(source="supervisor", slug=...)``) need
629
+ them. Both endpoints require ``hassio_role: manager``.
630
+
631
+ Delegates to ``_supervisor_logs_get`` so error handling stays in
632
+ lockstep with ``_get_system_service_logs``.
633
+ """
634
+ return await self._supervisor_logs_get(f"addons/{slug}")
635
+
636
+ async def _get_system_service_logs(self, service: str) -> str:
637
+ """Fetch HA system-service logs directly from Supervisor's REST API.
638
+
639
+ Hits ``http://supervisor/{service}/logs``. ``service`` must be one of
640
+ the seven Supervisor-managed services: ``supervisor``, ``host``,
641
+ ``core``, ``dns``, ``audio``, ``multicast``, ``observer``. Caller is
642
+ responsible for validating ``service`` against the allowed set; this
643
+ helper does no validation and will raise ``HomeAssistantAPIError`` on
644
+ any unknown path (404 from Supervisor).
645
+
646
+ Requires ``hassio_role: manager`` like the addon-logs path.
647
+ """
648
+ return await self._supervisor_logs_get(service)
649
+
459
650
  async def test_connection(self) -> tuple[bool, str | None]:
460
651
  """
461
652
  Test connection to Home Assistant.
@@ -920,7 +1111,9 @@ class HomeAssistantClient:
920
1111
  await asyncio.sleep(retry_delay)
921
1112
  continue
922
1113
  else:
923
- logger.error(f"WebSocket 403 error after {max_retries} attempts: {error_str}")
1114
+ logger.error(
1115
+ f"WebSocket 403 error after {max_retries} attempts: {error_str}"
1116
+ )
924
1117
  return {
925
1118
  "success": False,
926
1119
  "error": f"WebSocket request blocked (403 Forbidden): {error_str}",
@@ -1018,7 +1211,7 @@ class HomeAssistantClient:
1018
1211
  except Exception:
1019
1212
  logger.debug(
1020
1213
  f"Entity registry lookup failed for {entity_id}, using bare id: {bare_id}",
1021
- exc_info=True # Log full traceback for better debugging
1214
+ exc_info=True, # Log full traceback for better debugging
1022
1215
  )
1023
1216
  return bare_id
1024
1217